CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
deps.test.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 8098672 | 1 | /** |
| 2 | * Block J1 — Dependency graph tests. | |
| 3 | * | |
| 4 | * Parser smokes for each supported ecosystem + auth smokes for routes. | |
| 5 | */ | |
| 6 | ||
| 7 | import { describe, it, expect } from "bun:test"; | |
| 8 | import app from "../app"; | |
| 9 | import { | |
| 10 | parsePackageJson, | |
| 11 | parseRequirementsTxt, | |
| 12 | parsePyprojectToml, | |
| 13 | parseGoMod, | |
| 14 | parseCargoToml, | |
| 15 | parseGemfile, | |
| 16 | parseComposerJson, | |
| 17 | parseManifest, | |
| 18 | isManifestPath, | |
| 19 | __internal, | |
| 20 | } from "../lib/deps"; | |
| 21 | ||
| 22 | describe("deps — isManifestPath", () => { | |
| 23 | it("recognises supported manifests", () => { | |
| 24 | expect(isManifestPath("package.json")).toBe(true); | |
| 25 | expect(isManifestPath("frontend/package.json")).toBe(true); | |
| 26 | expect(isManifestPath("requirements.txt")).toBe(true); | |
| 27 | expect(isManifestPath("pyproject.toml")).toBe(true); | |
| 28 | expect(isManifestPath("go.mod")).toBe(true); | |
| 29 | expect(isManifestPath("Cargo.toml")).toBe(true); | |
| 30 | expect(isManifestPath("Gemfile")).toBe(true); | |
| 31 | expect(isManifestPath("composer.json")).toBe(true); | |
| 32 | }); | |
| 33 | ||
| 34 | it("rejects unrelated files", () => { | |
| 35 | expect(isManifestPath("README.md")).toBe(false); | |
| 36 | expect(isManifestPath("src/index.ts")).toBe(false); | |
| 37 | expect(isManifestPath("package-lock.json")).toBe(false); | |
| 38 | }); | |
| 39 | }); | |
| 40 | ||
| 41 | describe("deps — parsePackageJson", () => { | |
| 42 | it("extracts dependencies and devDependencies", () => { | |
| 43 | const deps = parsePackageJson( | |
| 44 | JSON.stringify({ | |
| 45 | dependencies: { hono: "^4.0.0", "drizzle-orm": "0.30.0" }, | |
| 46 | devDependencies: { typescript: "^5.0.0" }, | |
| 47 | peerDependencies: { react: ">=18" }, | |
| 48 | }) | |
| 49 | ); | |
| 50 | expect(deps).toHaveLength(4); | |
| 51 | const hono = deps.find((d) => d.name === "hono")!; | |
| 52 | expect(hono.ecosystem).toBe("npm"); | |
| 53 | expect(hono.versionSpec).toBe("^4.0.0"); | |
| 54 | expect(hono.isDev).toBe(false); | |
| 55 | const ts = deps.find((d) => d.name === "typescript")!; | |
| 56 | expect(ts.isDev).toBe(true); | |
| 57 | }); | |
| 58 | ||
| 59 | it("returns empty on bad JSON", () => { | |
| 60 | expect(parsePackageJson("not json")).toEqual([]); | |
| 61 | }); | |
| 62 | ||
| 63 | it("returns empty when no deps", () => { | |
| 64 | expect(parsePackageJson(JSON.stringify({ name: "x", version: "1" }))).toEqual( | |
| 65 | [] | |
| 66 | ); | |
| 67 | }); | |
| 68 | }); | |
| 69 | ||
| 70 | describe("deps — parseRequirementsTxt", () => { | |
| 71 | it("parses canonical form", () => { | |
| 72 | const deps = parseRequirementsTxt( | |
| 73 | `# comment | |
| 74 | requests==2.28.0 | |
| 75 | Flask>=2.0,<3.0 | |
| 76 | numpy | |
| 77 | pytest # inline comment` | |
| 78 | ); | |
| 79 | const names = deps.map((d) => d.name); | |
| 80 | expect(names).toContain("requests"); | |
| 81 | expect(names).toContain("Flask"); | |
| 82 | expect(names).toContain("numpy"); | |
| 83 | expect(names).toContain("pytest"); | |
| 84 | expect(deps.find((d) => d.name === "requests")!.versionSpec).toBe( | |
| 85 | "==2.28.0" | |
| 86 | ); | |
| 87 | }); | |
| 88 | ||
| 89 | it("skips blank lines and editable installs", () => { | |
| 90 | const deps = parseRequirementsTxt( | |
| 91 | ` | |
| 92 | # just a comment | |
| 93 | -e git+https://example.com/foo.git#egg=foo | |
| 94 | --index-url https://pypi.org/simple/ | |
| 95 | ` | |
| 96 | ); | |
| 97 | expect(deps).toHaveLength(0); | |
| 98 | }); | |
| 99 | ||
| 100 | it("handles extras syntax", () => { | |
| 101 | const deps = parseRequirementsTxt("requests[security]==2.0"); | |
| 102 | expect(deps[0].name).toBe("requests"); | |
| 103 | }); | |
| 104 | }); | |
| 105 | ||
| 106 | describe("deps — parsePyprojectToml", () => { | |
| 107 | it("extracts project.dependencies", () => { | |
| 108 | const deps = parsePyprojectToml(` | |
| 109 | [project] | |
| 110 | name = "myapp" | |
| 111 | dependencies = [ | |
| 112 | "requests>=2.0", | |
| 113 | "flask" | |
| 114 | ] | |
| 115 | `); | |
| 116 | const names = deps.map((d) => d.name).sort(); | |
| 117 | expect(names).toContain("requests"); | |
| 118 | expect(names).toContain("flask"); | |
| 119 | }); | |
| 120 | ||
| 121 | it("extracts optional-dependencies as dev", () => { | |
| 122 | const deps = parsePyprojectToml(` | |
| 123 | [project.optional-dependencies] | |
| 124 | dev = ["pytest", "black"] | |
| 125 | `); | |
| 126 | expect(deps.some((d) => d.name === "pytest" && d.isDev)).toBe(true); | |
| 127 | }); | |
| 128 | }); | |
| 129 | ||
| 130 | describe("deps — parseGoMod", () => { | |
| 131 | it("parses require block", () => { | |
| 132 | const deps = parseGoMod(` | |
| 133 | module example.com/foo | |
| 134 | ||
| 135 | go 1.21 | |
| 136 | ||
| 137 | require ( | |
| 138 | github.com/gorilla/mux v1.8.0 | |
| 139 | github.com/jackc/pgx/v5 v5.4.3 // indirect | |
| 140 | ) | |
| 141 | `); | |
| 142 | expect(deps.length).toBeGreaterThanOrEqual(2); | |
| 143 | expect(deps.find((d) => d.name === "github.com/gorilla/mux")!.versionSpec) | |
| 144 | .toBe("v1.8.0"); | |
| 145 | }); | |
| 146 | }); | |
| 147 | ||
| 148 | describe("deps — parseCargoToml", () => { | |
| 149 | it("parses [dependencies] and [dev-dependencies]", () => { | |
| 150 | const deps = parseCargoToml(` | |
| 151 | [package] | |
| 152 | name = "myapp" | |
| 153 | ||
| 154 | [dependencies] | |
| 155 | serde = "1.0" | |
| 156 | tokio = { version = "1.35", features = ["full"] } | |
| 157 | ||
| 158 | [dev-dependencies] | |
| 159 | criterion = "0.5" | |
| 160 | `); | |
| 161 | const serde = deps.find((d) => d.name === "serde")!; | |
| 162 | expect(serde.versionSpec).toBe("1.0"); | |
| 163 | expect(serde.isDev).toBe(false); | |
| 164 | const tokio = deps.find((d) => d.name === "tokio")!; | |
| 165 | expect(tokio.versionSpec).toBe("1.35"); | |
| 166 | const criterion = deps.find((d) => d.name === "criterion")!; | |
| 167 | expect(criterion.isDev).toBe(true); | |
| 168 | }); | |
| 169 | }); | |
| 170 | ||
| 171 | describe("deps — parseGemfile", () => { | |
| 172 | it("parses gem lines", () => { | |
| 173 | const deps = parseGemfile(` | |
| 174 | source "https://rubygems.org" | |
| 175 | ||
| 176 | gem "rails", "7.0.0" | |
| 177 | gem "puma" | |
| 178 | ||
| 179 | group :development, :test do | |
| 180 | gem "rspec" | |
| 181 | end | |
| 182 | `); | |
| 183 | const rails = deps.find((d) => d.name === "rails")!; | |
| 184 | expect(rails.versionSpec).toBe("7.0.0"); | |
| 185 | const rspec = deps.find((d) => d.name === "rspec")!; | |
| 186 | expect(rspec.isDev).toBe(true); | |
| 187 | }); | |
| 188 | }); | |
| 189 | ||
| 190 | describe("deps — parseComposerJson", () => { | |
| 191 | it("parses require + require-dev, skips php", () => { | |
| 192 | const deps = parseComposerJson( | |
| 193 | JSON.stringify({ | |
| 194 | require: { php: ">=8.0", "laravel/framework": "^10.0" }, | |
| 195 | "require-dev": { phpunit: "^10.0" }, | |
| 196 | }) | |
| 197 | ); | |
| 198 | expect(deps.find((d) => d.name === "php")).toBeUndefined(); | |
| 199 | expect(deps.find((d) => d.name === "laravel/framework")).toBeDefined(); | |
| 200 | expect(deps.find((d) => d.name === "phpunit")!.isDev).toBe(true); | |
| 201 | }); | |
| 202 | }); | |
| 203 | ||
| 204 | describe("deps — parseManifest (dispatch)", () => { | |
| 205 | it("routes by basename", () => { | |
| 206 | expect( | |
| 207 | parseManifest("frontend/package.json", '{"dependencies":{"x":"1"}}') | |
| 208 | ).toHaveLength(1); | |
| 209 | expect(parseManifest("some/go.mod", "require foo v1")).toHaveLength(1); | |
| 210 | expect(parseManifest("README.md", "# hi")).toHaveLength(0); | |
| 211 | }); | |
| 212 | ||
| 213 | it("swallows parser errors and returns empty", () => { | |
| 214 | expect(parseManifest("package.json", "not-json")).toEqual([]); | |
| 215 | }); | |
| 216 | }); | |
| 217 | ||
| 218 | describe("deps — TOML helpers", () => { | |
| 219 | const { splitTomlSections, splitTomlArrayItems, pythonRequirementToDep } = | |
| 220 | __internal; | |
| 221 | ||
| 222 | it("splits sections by header", () => { | |
| 223 | const s = splitTomlSections(`[a] | |
| 224 | x = 1 | |
| 225 | ||
| 226 | [b] | |
| 227 | y = 2`); | |
| 228 | expect(s["a"].trim()).toContain("x = 1"); | |
| 229 | expect(s["b"].trim()).toContain("y = 2"); | |
| 230 | }); | |
| 231 | ||
| 232 | it("splits array items respecting quotes", () => { | |
| 233 | expect(splitTomlArrayItems('"a, b", "c", "d"')).toEqual([ | |
| 234 | '"a, b"', | |
| 235 | '"c"', | |
| 236 | '"d"', | |
| 237 | ]); | |
| 238 | }); | |
| 239 | ||
| 240 | it("parses Python requirement specifiers", () => { | |
| 241 | const d = pythonRequirementToDep("requests>=2.0", false)!; | |
| 242 | expect(d.name).toBe("requests"); | |
| 243 | expect(d.versionSpec).toBe(">=2.0"); | |
| 244 | }); | |
| 245 | }); | |
| 246 | ||
| 247 | describe("deps — route auth", () => { | |
| 248 | it("GET /:owner/:repo/dependencies for unknown repo → 404 or 500", async () => { | |
| 249 | const res = await app.request("/nobody/missing/dependencies"); | |
| 250 | // Route is mounted — either 404 (repo missing) or 500 (no DB in test env) | |
| 251 | expect([404, 500]).toContain(res.status); | |
| 252 | }); | |
| 253 | ||
| 254 | it("POST /:owner/:repo/dependencies/reindex without auth → 302 /login", async () => { | |
| 255 | const res = await app.request("/alice/repo/dependencies/reindex", { | |
| 256 | method: "POST", | |
| 257 | }); | |
| 258 | expect(res.status).toBe(302); | |
| 259 | expect(res.headers.get("location") || "").toContain("/login"); | |
| 260 | }); | |
| 261 | }); |