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