g1t/services/context/src/catalog.test.ts
| 1 | import { test } from "node:test"; |
| 2 | import assert from "node:assert/strict"; |
| 3 | |
| 4 | import { assemble, authorsOf } from "./assemble.ts"; |
| 5 | import { extract } from "./extract.ts"; |
| 6 | import { composeRunContext, HEADER } from "./runcontext.ts"; |
| 7 | import { evaluate } from "./scorecards.ts"; |
| 8 | import { granted } from "../../../packages/contracts/src/access.ts"; |
| 9 | import { indexFilter, memoryReadable, merge, readable, allowedKinds, countVisible, projectReadable, runMemoryReadable } from "./visibility.ts"; |
| 10 | |
| 11 | const project = { |
| 12 | id: "prj_1", |
| 13 | workspace: "acme", |
| 14 | slug: "web", |
| 15 | name: "web", |
| 16 | description: "The storefront.", |
| 17 | private: true, |
| 18 | repoId: "rep_1", |
| 19 | repo: { namespace: "acme", name: "web" }, |
| 20 | rootDir: "", |
| 21 | defaultBranch: "main", |
| 22 | }; |
| 23 | const ctx = { project: "web", siblings: ["package-lock.json"] }; |
| 24 | |
| 25 | test("a project's entities and relations come from its files and surroundings", () => { |
| 26 | const files = [ |
| 27 | { path: "package.json", facts: extract("package.json", JSON.stringify({ name: "@acme/web", scripts: { test: "vitest" }, dependencies: { "@acme/ui": "*" } }), ctx) }, |
| 28 | { path: "README.md", facts: extract("README.md", "# Web\n\nThe storefront.", ctx) }, |
| 29 | { path: "wrangler.jsonc", facts: extract("wrangler.jsonc", '{"name":"web","routes":["shop.acme.com/*"]}', ctx) }, |
| 30 | { path: ".g1t/workflows/ci.yml", facts: extract(".g1t/workflows/ci.yml", "run: npm test", ctx) }, |
| 31 | ]; |
| 32 | const built = assemble(project, files, { |
| 33 | owners: ["ana"], |
| 34 | dependsOn: [{ slug: "api", as: "API_URL" }], |
| 35 | deploy: { enabled: true, production: { url: "https://web--acme.g1t.page", commit: "abcdef1234", deployedAt: "2026-10-01T00:00:00Z" }, previews: 2, latest: null }, |
| 36 | integrations: [{ id: "int_1", provider: "sentry", name: "Sentry", kind: "alerts", repo: "acme/web" }], |
| 37 | }); |
| 38 | const keys = built.entities.map((entity) => `${entity.kind}:${entity.key}`); |
| 39 | for (const key of ["project:web", "owner:ana", "language:javascript", "package:npm:@acme/web", "doc:web:README.md", "api:web:worker:web", "app:web", "environment:web/production", "environment:web/preview"]) { |
| 40 | assert.ok(keys.includes(key), `${key} in ${keys.join(", ")}`); |
| 41 | } |
| 42 | const relations = built.relations.map((r) => `${r.from.kind}:${r.from.key} ${r.kind} ${r.to.kind}:${r.to.key}`); |
| 43 | for (const relation of [ |
| 44 | "project:web depends_on project:api", |
| 45 | "project:web owned_by owner:ana", |
| 46 | "project:web documented_by doc:web:README.md", |
| 47 | "project:web exposes package:npm:@acme/web", |
| 48 | "package:npm:@acme/web depends_on package:npm:@acme/ui", |
| 49 | "app:web deploys_to environment:web/production", |
| 50 | "app:web exposes api:web:worker:web", |
| 51 | "project:web uses language:javascript", |
| 52 | "project:web uses integration:int_1", |
| 53 | ]) { |
| 54 | assert.ok(relations.includes(relation), relation); |
| 55 | } |
| 56 | assert.equal(built.tests, true); |
| 57 | const entry = built.entities.find((entity) => entity.kind === "project")!; |
| 58 | assert.match(entry.summary!, /The storefront\. Written in JavaScript\. .*Uses api\. Owned by ana\./); |
| 59 | assert.equal(entry.data.productionUrl, "https://web--acme.g1t.page"); |
| 60 | }); |
| 61 | |
| 62 | test("the same inputs build the same catalog", () => { |
| 63 | const files = [{ path: "go.mod", facts: extract("go.mod", "module x/y\n", ctx) }]; |
| 64 | const around = { owners: [], dependsOn: [], deploy: null, integrations: [] }; |
| 65 | assert.deepEqual(assemble(project, files, around), assemble(project, files, around)); |
| 66 | // Without deployments there is no app or environment. |
| 67 | assert.ok(!assemble(project, files, around).entities.some((entity) => entity.kind === "app" || entity.kind === "environment")); |
| 68 | }); |
| 69 | |
| 70 | test("owners are the members who wrote a fifth or more of the recent commits", () => { |
| 71 | const commit = (name: string, email = `${name}@example.com`) => ({ author: { name, email } }); |
| 72 | const commits = [...Array(6)].map(() => commit("Ana")).concat([commit("someone", "bo@acme.com"), commit("bot"), commit("bot"), commit("cy")]); |
| 73 | assert.deepEqual(authorsOf(commits, ["ana", "bo", "cy"]), ["ana"]); |
| 74 | assert.deepEqual(authorsOf([commit("x", "bo@acme.com")], ["bo"]), ["bo"]); |
| 75 | assert.deepEqual(authorsOf([], ["ana"]), []); |
| 76 | }); |
| 77 | |
| 78 | test("scorecards pass, fail with a fix, or do not apply", () => { |
| 79 | const rules = evaluate({ |
| 80 | name: "web", |
| 81 | owners: [], |
| 82 | docs: ["README.md"], |
| 83 | tests: false, |
| 84 | testCommand: "npm test", |
| 85 | deploy: { enabled: true, production: null, latest: { kind: "production", status: "failed", error: "build failed" } }, |
| 86 | secretFindings: null, |
| 87 | }); |
| 88 | const by = Object.fromEntries(rules.map((rule) => [rule.rule, rule])); |
| 89 | assert.equal(by.has_owner.status, "fail"); |
| 90 | assert.deepEqual(by.has_owner.fix?.checks, ["grep -q '^owners:' .g1t/project.yml"]); |
| 91 | assert.equal(by.has_readme.status, "pass"); |
| 92 | assert.equal(by.has_readme.fix, null); |
| 93 | assert.equal(by.has_agents_md.fix?.title, "Add an AGENTS.md to web"); |
| 94 | assert.match(by.tests_in_ci.fix!.body, /`npm test`/); |
| 95 | assert.equal(by.production_green.status, "fail"); |
| 96 | assert.match(by.production_green.detail, /build failed/); |
| 97 | assert.equal(by.no_secret_findings.status, "na"); |
| 98 | const quiet = evaluate({ name: "lib", owners: ["ana"], docs: ["readme.md", "CLAUDE.md"], tests: true, testCommand: null, deploy: null, secretFindings: 0 }); |
| 99 | assert.deepEqual(quiet.map((rule) => rule.status), ["pass", "pass", "pass", "pass", "na", "pass"]); |
| 100 | }); |
| 101 | |
| 102 | test("the run context marks its sources and keeps to its budget", () => { |
| 103 | const input = { |
| 104 | projects: [ |
| 105 | { |
| 106 | slug: "web", |
| 107 | name: "web", |
| 108 | repo: "acme/web", |
| 109 | rootDir: "", |
| 110 | languages: ["TypeScript"], |
| 111 | packages: ["@acme/web"], |
| 112 | testCommands: ["npm test"], |
| 113 | owners: ["ana"], |
| 114 | dependsOn: [{ slug: "api", as: "API_URL", url: "https://api--acme.g1t.page" }], |
| 115 | usedBy: [], |
| 116 | environments: [{ name: "Production", url: "https://web--acme.g1t.page", status: "ready" }], |
| 117 | docs: ["README.md"], |
| 118 | }, |
| 119 | ], |
| 120 | memories: [{ id: "mem_1", kind: "gotcha", text: "Tests need TZ=UTC.", source: "AGENTS.md" }], |
| 121 | decisions: [{ id: "mem_2", kind: "decision", text: "Decided in #12: keep v1 webhooks.", source: "#12" }], |
| 122 | budget: 4000, |
| 123 | }; |
| 124 | const { text, sources } = composeRunContext(input); |
| 125 | assert.ok(text!.startsWith(HEADER)); |
| 126 | assert.match(text!, /Project web \(acme\/web\) \[source: catalog\]:/); |
| 127 | assert.match(text!, /Uses: api at https:\/\/api--acme\.g1t\.page \(its address is in API_URL\)/); |
| 128 | assert.match(text!, /\[gotcha\] Tests need TZ=UTC\. \[source: AGENTS\.md\]/); |
| 129 | assert.match(text!, /Recent decisions:\n- Decided in #12: keep v1 webhooks\. \[source: #12\]/); |
| 130 | assert.deepEqual(sources.sort(), ["catalog:web", "memory:mem_1", "memory:mem_2"]); |
| 131 | const tight = composeRunContext({ ...input, budget: HEADER.length + 120 }); |
| 132 | assert.ok(tight.text!.length <= HEADER.length + 120); |
| 133 | assert.deepEqual(composeRunContext({ projects: [], memories: [], decisions: [], budget: 4000 }), { text: null, sources: [] }); |
| 134 | }); |
| 135 | |
| 136 | test("search reads one workspace, and only what a reader may see", () => { |
| 137 | const member = { workspace: "acme", member: true, full: true, visible: new Set<string>() }; |
| 138 | const outsider = { workspace: "acme", member: false, full: false, visible: new Set(["site"]) }; |
| 139 | assert.deepEqual(indexFilter(member, {}), { workspace: "acme" }); |
| 140 | assert.deepEqual(indexFilter(outsider, { project: "site", kinds: ["memory", "doc"] }), { workspace: "acme", private: false, project: "site", kind: { $in: ["doc"] } }); |
| 141 | assert.ok(!(allowedKinds(outsider) ?? []).includes("memory")); |
| 142 | const row = { workspace: "acme", kind: "doc", project: "site", private: false }; |
| 143 | assert.ok(readable(row, outsider)); |
| 144 | assert.ok(!readable({ ...row, workspace: "other" }, member), "never another workspace"); |
| 145 | assert.ok(!readable({ ...row, project: "billing", private: true }, outsider), "a private project not listed for them"); |
| 146 | assert.ok(!readable({ ...row, kind: "memory" }, outsider), "memory is for members"); |
| 147 | assert.ok(!readable({ ...row, project: "made-private-since" }, outsider)); |
| 148 | assert.ok(readable({ ...row, kind: "memory", private: true }, member)); |
| 149 | }); |
| 150 | |
| 151 | test("a member with no base permission sees only the private projects granted to them", () => { |
| 152 | const user = { id: "u", username: "u", kind: "user" as const, workspaces: [{ slug: "acme", role: "member" as const, base_permission: "none" as const }], grants: [{ repo_id: "repo_api", workspace: "acme", role: "read" as const }] }; |
| 153 | assert.equal(granted(user, { id: "", namespace: "acme", isPrivate: true }), null, "does not read every repository"); |
| 154 | // What the projects service lists for them: public ones, and the one granted. |
| 155 | const reader = { workspace: "acme", member: true, full: false, visible: new Set(["site", "api"]), privateVisible: true, repos: new Set(["acme/site", "acme/api"]) }; |
| 156 | const row = { workspace: "acme", kind: "issue", project: "api", private: true }; |
| 157 | assert.ok(readable(row, reader), "the granted private project"); |
| 158 | assert.ok(!readable({ ...row, project: "billing" }, reader), "another private project"); |
| 159 | assert.ok(!readable({ ...row, project: "" }, reader), "a private row with no project"); |
| 160 | assert.ok(readable({ ...row, project: "site", private: false }, reader)); |
| 161 | assert.ok(readable({ ...row, kind: "memory", project: "" }, reader), "workspace memory is for every member"); |
| 162 | assert.ok(!readable({ ...row, kind: "memory", project: "billing" }, reader)); |
| 163 | assert.ok(memoryReadable(null, reader)); |
| 164 | assert.ok(memoryReadable({ namespace: "Acme", name: "API" }, reader)); |
| 165 | assert.ok(!memoryReadable({ namespace: "acme", name: "billing" }, reader)); |
| 166 | assert.ok(!memoryReadable(null, { ...reader, member: false }), "memory is for members"); |
| 167 | assert.deepEqual(indexFilter(reader, {}), { workspace: "acme" }, "private rows are checked one by one"); |
| 168 | assert.equal(indexFilter({ ...reader, member: false, privateVisible: false }, {}).private, false); |
| 169 | }); |
| 170 | |
| 171 | test("the hub's counts are over what the viewer may read", () => { |
| 172 | const rows = [ |
| 173 | { kind: "project", project: "site", private: 0, n: 1 }, |
| 174 | { kind: "project", project: "api", private: 1, n: 1 }, |
| 175 | { kind: "project", project: "billing", private: 1, n: 1 }, |
| 176 | { kind: "doc", project: "billing", private: 1, n: 7 }, |
| 177 | { kind: "language", project: null, private: 0, n: 3 }, |
| 178 | ]; |
| 179 | const full = { workspace: "acme", member: true, full: true, visible: new Set<string>() }; |
| 180 | assert.deepEqual(countVisible(rows, full), { project: 3, doc: 7, language: 3 }); |
| 181 | const none = { workspace: "acme", member: true, full: false, visible: new Set(["site", "api"]), privateVisible: true }; |
| 182 | assert.deepEqual(countVisible(rows, none), { project: 2, language: 3 }, "billing is not theirs"); |
| 183 | }); |
| 184 | |
| 185 | test("an agent run is told only what the person it acts for may read", () => { |
| 186 | const web = { namespace: "acme", name: "web" }; |
| 187 | const workspaceMemory = { scope: "workspace", repo: null }; |
| 188 | const webMemory = { scope: "project", repo: web }; |
| 189 | const billingMemory = { scope: "project", repo: { namespace: "acme", name: "billing" } }; |
| 190 | // The workspace's own step: everything. |
| 191 | assert.ok(runMemoryReadable(workspaceMemory, null, "acme/web")); |
| 192 | assert.ok(projectReadable("billing", null)); |
| 193 | // A member who reads everything. |
| 194 | const member = { workspace: "acme", member: true, full: true, visible: new Set<string>() }; |
| 195 | assert.ok(runMemoryReadable(workspaceMemory, member, "acme/web")); |
| 196 | assert.ok(runMemoryReadable(billingMemory, member, "acme/web")); |
| 197 | // An outside collaborator with Write on acme/web. |
| 198 | const outside = { workspace: "acme", member: false, full: false, visible: new Set(["web", "site"]), repos: new Set(["acme/web", "acme/site"]) }; |
| 199 | assert.ok(!runMemoryReadable(workspaceMemory, outside, "acme/web"), "never the workspace's memory"); |
| 200 | assert.ok(runMemoryReadable(webMemory, outside, "Acme/Web"), "the project's memory"); |
| 201 | assert.ok(!runMemoryReadable(billingMemory, outside, "acme/web")); |
| 202 | assert.ok(!runMemoryReadable({ scope: "project", repo: { namespace: "acme", name: "site" } }, outside, "acme/web"), "only the run's own project"); |
| 203 | assert.ok(projectReadable("site", outside)); |
| 204 | assert.ok(!projectReadable("billing", outside), "a dependency they cannot read is not named"); |
| 205 | }); |
| 206 | |
| 207 | test("semantic hits come first, without repeats", () => { |
| 208 | const hit = (id: string, score: number) => ({ kind: "doc" as const, id, title: id, snippet: "", project: null, url: null, score, source: "doc", by: null, updatedAt: null }); |
| 209 | assert.deepEqual( |
| 210 | merge([hit("a", 0.5), hit("b", 0.9)], [hit("a", 0.2), hit("c", 0.2)], 10).map((h) => h.id), |
| 211 | ["b", "a", "c"], |
| 212 | ); |
| 213 | assert.equal(merge([hit("a", 1)], [hit("b", 1)], 1).length, 1); |
| 214 | }); |