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 { indexFilter, merge, readable, allowedKinds } from "./visibility.ts"; |
| 9 | |
| 10 | const project = { |
| 11 | id: "prj_1", |
| 12 | workspace: "acme", |
| 13 | slug: "web", |
| 14 | name: "web", |
| 15 | description: "The storefront.", |
| 16 | private: true, |
| 17 | repoId: "rep_1", |
| 18 | repo: { namespace: "acme", name: "web" }, |
| 19 | rootDir: "", |
| 20 | defaultBranch: "main", |
| 21 | }; |
| 22 | const ctx = { project: "web", siblings: ["package-lock.json"] }; |
| 23 | |
| 24 | test("a project's entities and relations come from its files and surroundings", () => { |
| 25 | const files = [ |
| 26 | { path: "package.json", facts: extract("package.json", JSON.stringify({ name: "@acme/web", scripts: { test: "vitest" }, dependencies: { "@acme/ui": "*" } }), ctx) }, |
| 27 | { path: "README.md", facts: extract("README.md", "# Web\n\nThe storefront.", ctx) }, |
| 28 | { path: "wrangler.jsonc", facts: extract("wrangler.jsonc", '{"name":"web","routes":["shop.acme.com/*"]}', ctx) }, |
| 29 | { path: ".g1t/workflows/ci.yml", facts: extract(".g1t/workflows/ci.yml", "run: npm test", ctx) }, |
| 30 | ]; |
| 31 | const built = assemble(project, files, { |
| 32 | owners: ["ana"], |
| 33 | dependsOn: [{ slug: "api", as: "API_URL" }], |
| 34 | deploy: { enabled: true, production: { url: "https://web--acme.g1t.page", commit: "abcdef1234", deployedAt: "2026-10-01T00:00:00Z" }, previews: 2, latest: null }, |
| 35 | integrations: [{ id: "int_1", provider: "sentry", name: "Sentry", kind: "alerts", repo: "acme/web" }], |
| 36 | }); |
| 37 | const keys = built.entities.map((entity) => `${entity.kind}:${entity.key}`); |
| 38 | 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"]) { |
| 39 | assert.ok(keys.includes(key), `${key} in ${keys.join(", ")}`); |
| 40 | } |
| 41 | const relations = built.relations.map((r) => `${r.from.kind}:${r.from.key} ${r.kind} ${r.to.kind}:${r.to.key}`); |
| 42 | for (const relation of [ |
| 43 | "project:web depends_on project:api", |
| 44 | "project:web owned_by owner:ana", |
| 45 | "project:web documented_by doc:web:README.md", |
| 46 | "project:web exposes package:npm:@acme/web", |
| 47 | "package:npm:@acme/web depends_on package:npm:@acme/ui", |
| 48 | "app:web deploys_to environment:web/production", |
| 49 | "app:web exposes api:web:worker:web", |
| 50 | "project:web uses language:javascript", |
| 51 | "project:web uses integration:int_1", |
| 52 | ]) { |
| 53 | assert.ok(relations.includes(relation), relation); |
| 54 | } |
| 55 | assert.equal(built.tests, true); |
| 56 | const entry = built.entities.find((entity) => entity.kind === "project")!; |
| 57 | assert.match(entry.summary!, /The storefront\. Written in JavaScript\. .*Uses api\. Owned by ana\./); |
| 58 | assert.equal(entry.data.productionUrl, "https://web--acme.g1t.page"); |
| 59 | }); |
| 60 | |
| 61 | test("the same inputs build the same catalog", () => { |
| 62 | const files = [{ path: "go.mod", facts: extract("go.mod", "module x/y\n", ctx) }]; |
| 63 | const around = { owners: [], dependsOn: [], deploy: null, integrations: [] }; |
| 64 | assert.deepEqual(assemble(project, files, around), assemble(project, files, around)); |
| 65 | // Without deployments there is no app or environment. |
| 66 | assert.ok(!assemble(project, files, around).entities.some((entity) => entity.kind === "app" || entity.kind === "environment")); |
| 67 | }); |
| 68 | |
| 69 | test("owners are the members who wrote a fifth or more of the recent commits", () => { |
| 70 | const commit = (name: string, email = `${name}@example.com`) => ({ author: { name, email } }); |
| 71 | const commits = [...Array(6)].map(() => commit("Ana")).concat([commit("someone", "bo@acme.com"), commit("bot"), commit("bot"), commit("cy")]); |
| 72 | assert.deepEqual(authorsOf(commits, ["ana", "bo", "cy"]), ["ana"]); |
| 73 | assert.deepEqual(authorsOf([commit("x", "bo@acme.com")], ["bo"]), ["bo"]); |
| 74 | assert.deepEqual(authorsOf([], ["ana"]), []); |
| 75 | }); |
| 76 | |
| 77 | test("scorecards pass, fail with a fix, or do not apply", () => { |
| 78 | const rules = evaluate({ |
| 79 | name: "web", |
| 80 | owners: [], |
| 81 | docs: ["README.md"], |
| 82 | tests: false, |
| 83 | testCommand: "npm test", |
| 84 | deploy: { enabled: true, production: null, latest: { kind: "production", status: "failed", error: "build failed" } }, |
| 85 | secretFindings: null, |
| 86 | }); |
| 87 | const by = Object.fromEntries(rules.map((rule) => [rule.rule, rule])); |
| 88 | assert.equal(by.has_owner.status, "fail"); |
| 89 | assert.deepEqual(by.has_owner.fix?.checks, ["grep -q '^owners:' .g1t/project.yml"]); |
| 90 | assert.equal(by.has_readme.status, "pass"); |
| 91 | assert.equal(by.has_readme.fix, null); |
| 92 | assert.equal(by.has_agents_md.fix?.title, "Add an AGENTS.md to web"); |
| 93 | assert.match(by.tests_in_ci.fix!.body, /`npm test`/); |
| 94 | assert.equal(by.production_green.status, "fail"); |
| 95 | assert.match(by.production_green.detail, /build failed/); |
| 96 | assert.equal(by.no_secret_findings.status, "na"); |
| 97 | const quiet = evaluate({ name: "lib", owners: ["ana"], docs: ["readme.md", "CLAUDE.md"], tests: true, testCommand: null, deploy: null, secretFindings: 0 }); |
| 98 | assert.deepEqual(quiet.map((rule) => rule.status), ["pass", "pass", "pass", "pass", "na", "pass"]); |
| 99 | }); |
| 100 | |
| 101 | test("the run context marks its sources and keeps to its budget", () => { |
| 102 | const input = { |
| 103 | projects: [ |
| 104 | { |
| 105 | slug: "web", |
| 106 | name: "web", |
| 107 | repo: "acme/web", |
| 108 | rootDir: "", |
| 109 | languages: ["TypeScript"], |
| 110 | packages: ["@acme/web"], |
| 111 | testCommands: ["npm test"], |
| 112 | owners: ["ana"], |
| 113 | dependsOn: [{ slug: "api", as: "API_URL", url: "https://api--acme.g1t.page" }], |
| 114 | usedBy: [], |
| 115 | environments: [{ name: "Production", url: "https://web--acme.g1t.page", status: "ready" }], |
| 116 | docs: ["README.md"], |
| 117 | }, |
| 118 | ], |
| 119 | memories: [{ id: "mem_1", kind: "gotcha", text: "Tests need TZ=UTC.", source: "AGENTS.md" }], |
| 120 | decisions: [{ id: "mem_2", kind: "decision", text: "Decided in #12: keep v1 webhooks.", source: "#12" }], |
| 121 | budget: 4000, |
| 122 | }; |
| 123 | const { text, sources } = composeRunContext(input); |
| 124 | assert.ok(text!.startsWith(HEADER)); |
| 125 | assert.match(text!, /Project web \(acme\/web\) \[source: catalog\]:/); |
| 126 | assert.match(text!, /Uses: api at https:\/\/api--acme\.g1t\.page \(its address is in API_URL\)/); |
| 127 | assert.match(text!, /\[gotcha\] Tests need TZ=UTC\. \[source: AGENTS\.md\]/); |
| 128 | assert.match(text!, /Recent decisions:\n- Decided in #12: keep v1 webhooks\. \[source: #12\]/); |
| 129 | assert.deepEqual(sources.sort(), ["catalog:web", "memory:mem_1", "memory:mem_2"]); |
| 130 | const tight = composeRunContext({ ...input, budget: HEADER.length + 120 }); |
| 131 | assert.ok(tight.text!.length <= HEADER.length + 120); |
| 132 | assert.deepEqual(composeRunContext({ projects: [], memories: [], decisions: [], budget: 4000 }), { text: null, sources: [] }); |
| 133 | }); |
| 134 | |
| 135 | test("search reads one workspace, and only what a reader may see", () => { |
| 136 | const member = { workspace: "acme", member: true, visible: new Set<string>() }; |
| 137 | const outsider = { workspace: "acme", member: false, visible: new Set(["site"]) }; |
| 138 | assert.deepEqual(indexFilter(member, {}), { workspace: "acme" }); |
| 139 | assert.deepEqual(indexFilter(outsider, { project: "site", kinds: ["memory", "doc"] }), { workspace: "acme", private: false, project: "site", kind: { $in: ["doc"] } }); |
| 140 | assert.ok(!(allowedKinds(outsider) ?? []).includes("memory")); |
| 141 | const row = { workspace: "acme", kind: "doc", project: "site", private: false }; |
| 142 | assert.ok(readable(row, outsider)); |
| 143 | assert.ok(!readable({ ...row, workspace: "other" }, member), "never another workspace"); |
| 144 | assert.ok(!readable({ ...row, private: true }, outsider)); |
| 145 | assert.ok(!readable({ ...row, kind: "memory" }, outsider), "memory is for members"); |
| 146 | assert.ok(!readable({ ...row, project: "made-private-since" }, outsider)); |
| 147 | assert.ok(readable({ ...row, kind: "memory", private: true }, member)); |
| 148 | }); |
| 149 | |
| 150 | test("semantic hits come first, without repeats", () => { |
| 151 | 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 }); |
| 152 | assert.deepEqual( |
| 153 | merge([hit("a", 0.5), hit("b", 0.9)], [hit("a", 0.2), hit("c", 0.2)], 10).map((h) => h.id), |
| 154 | ["b", "a", "c"], |
| 155 | ); |
| 156 | assert.equal(merge([hit("a", 1)], [hit("b", 1)], 1).length, 1); |
| 157 | }); |