Public: anyone can read this; the crew can change it.

told-by and standing-every-process are green in any order and on any shard, or each is quarantined under its own issue #2074

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/2070-told-by-and-standing-every-process-are-g → main 2 commits fork

  • Merged. scout/2070-told-by-and-standing-every-process-are-g was merged into main as 81fff81.

Review

Sign in to review this change.
3 files changed +69 −50
test/standing-every-process.test.ts +4 −1
@@ -152,7 +152,9 @@ describe("a suspension holds in every process serving the forge", () => {
152152 const { ended } = await streamOn(drainingServers, cookie);
153153 expect(await within(ended, SWEEP_MS * 6)).toBe(false);
154154 });
155 // Last: a transfer cut mid-request leaves its connection to be dropped.
155 // A transfer cut mid-request leaves its connection to be dropped, so it
156 // takes a connection of its own: a pooled one handed to the next test's
157 // fetch comes back 400, and the file would pass only with this test last.
156158 test("a git transfer under way on the draining process is cut", async () => {
157159 const token = createToken(fresh.db, sam.id, "t", "rw").token;
158160 // An upload-pack whose negotiation never finishes: git waits on it,
@@ -162,6 +164,7 @@ describe("a suspension holds in every process serving the forge", () => {
162164 });
163165 const transfer = fetch(`${drainingServers.url}/sam/lab.git/git-upload-pack`, {
164166 method: "POST",
167 keepalive: false,
165168 headers: {
166169 authorization: `Basic ${Buffer.from(`sam:${token}`).toString("base64")}`,
167170 "content-type": "application/x-git-upload-pack-request",
test/told-by.test.ts +60 −49
@@ -27,12 +27,57 @@ describe("who told and who shipped", () => {
2727 let alice: User;
2828 let bob: User;
2929 let repo: Repo;
30 let shipped: Repo;
3031 let cookie: string;
31 let number: number;
3232
3333 const get = (path: string) =>
3434 fetch(`${servers.url}${path}`, { headers: { cookie } });
3535
36 /** A fresh issue from bob, answered by scout's pull: every test makes its own, so none depends on another's merge. */
37 const answered = async (into: Repo, slug: string): Promise<number> => {
38 const told = openIssue(app, bob, into, { title: `Make it ${slug}` });
39 if (!told.ok) throw new Error(String(told.error));
40 const branch = await commitToBranch(app, alice, into, {
41 branch: `feature/${slug}`,
42 base: "main",
43 message: `Do ${slug}`,
44 files: [{ path: `${slug}.md`, content: `${slug}\n` }],
45 });
46 if (!branch.ok) throw new Error(JSON.stringify(branch.error));
47 const opened = await openPull(app, getUserByName(app.db, "scout")!, into, {
48 title: `Notes, ${slug}`,
49 body: `Done.\n\nCloses #${told.value.number}`,
50 baseBranch: "main",
51 headBranch: `feature/${slug}`,
52 });
53 if (!opened.ok) throw new Error(String(opened.error));
54 return opened.value.number;
55 };
56
57 /** A public app of alice's with a first commit: the feed shows one shipped line per app, so a merge gets an app of its own. */
58 const seedRepo = async (name: string): Promise<Repo> => {
59 const made = await createRepo(app, alice, { name, visibility: "public" });
60 if (!made.ok) throw new Error(String(made.error));
61 const seeded = await commitToBranch(app, alice, made.value, {
62 message: "Start",
63 files: [
64 {
65 path: "package.json",
66 content: `{"name":"${name}","scripts":{"start":"bun run server.ts"}}\n`,
67 },
68 {
69 path: "server.ts",
70 content:
71 "Bun.serve({ port: Number(process.env.PORT), fetch: () => new Response('ok') });\n",
72 },
73 ],
74 });
75 if (!seeded.ok) throw new Error(JSON.stringify(seeded.error));
76 const enabled = await enableApp(app, alice, made.value, { slug: name });
77 if (!enabled.ok) throw new Error(String(enabled.error));
78 return made.value;
79 };
80
3681 beforeAll(async () => {
3782 dataDir = await mkdtemp(join(tmpdir(), "forge-toldby-"));
3883 app = createApp({ dataDir, port: 0 });
@@ -63,50 +108,12 @@ describe("who told and who shipped", () => {
63108 })
64109 ).headers.get("set-cookie") ?? ""
65110 ).split(";")[0]!;
66 const made = await createRepo(app, alice, {
67 name: "notes",
68 visibility: "public",
69 });
70 if (!made.ok) throw new Error(String(made.error));
71 repo = made.value;
72 const seeded = await commitToBranch(app, alice, repo, {
73 message: "Start",
74 files: [
75 {
76 path: "package.json",
77 content: `{"name":"notes","scripts":{"start":"bun run server.ts"}}\n`,
78 },
79 {
80 path: "server.ts",
81 content:
82 "Bun.serve({ port: Number(process.env.PORT), fetch: () => new Response('ok') });\n",
83 },
84 ],
85 });
86 if (!seeded.ok) throw new Error(JSON.stringify(seeded.error));
87 const enabled = await enableApp(app, alice, repo, { slug: "notes" });
88 if (!enabled.ok) throw new Error(String(enabled.error));
111 repo = await seedRepo("notes");
112 shipped = await seedRepo("shipped");
89113 const scout = await createAgent(app, alice, { username: "scout" });
90114 if (!scout.ok) throw new Error(String(scout.error));
91115 addToCrew(app, alice, repo, "scout");
92
93 const told = openIssue(app, bob, repo, { title: "Make it fast" });
94 if (!told.ok) throw new Error(String(told.error));
95 const branch = await commitToBranch(app, alice, repo, {
96 branch: "feature/fast",
97 base: "main",
98 message: "Faster",
99 files: [{ path: "FAST.md", content: "fast\n" }],
100 });
101 if (!branch.ok) throw new Error(JSON.stringify(branch.error));
102 const opened = await openPull(app, getUserByName(app.db, "scout")!, repo, {
103 title: "Faster notes",
104 body: `Done.\n\nCloses #${told.value.number}`,
105 baseBranch: "main",
106 headBranch: "feature/fast",
107 });
108 if (!opened.ok) throw new Error(String(opened.error));
109 number = opened.value.number;
116 addToCrew(app, alice, shipped, "scout");
110117 }, 30_000);
111118
112119 afterAll(async () => {
@@ -115,7 +122,8 @@ describe("who told and who shipped", () => {
115122 await rm(dataDir, { recursive: true, force: true });
116123 });
117124
118 test("toldBy is the person who wrote the issue it closes, nobody else", () => {
125 test("toldBy is the person who wrote the issue it closes, nobody else", async () => {
126 const number = await answered(repo, "told");
119127 const pull = getPull(app.db, repo.id, number)!;
120128 expect(toldBy(app.db, pull)).toBe("bob");
121129 expect(toldBy(app.db, { ...pull, body: "no issue here" })).toBeNull();
@@ -123,27 +131,30 @@ describe("who told and who shipped", () => {
123131 });
124132
125133 test("the pull's page names both, escaped", async () => {
134 const number = await answered(repo, "page");
126135 const html = await (await get(`/alice/notes/pulls/${number}`)).text();
127136 expect(html).toContain("in answer to");
128137 expect(html).toMatch(/in answer to[\s\S]{0,200}bob/);
129138 expect(html).toContain("scout");
130139 });
131140
132 test("the feed item says who told", () => {
133 const items = readFeed(app.db, alice).items;
141 test("the feed item says who told", async () => {
142 await answered(repo, "feed");
143 const items = readFeed(app.db, alice, { limit: 100 }).items;
134144 expect(
135 items.some((i) => i.reason.includes("Faster notes") &&
145 items.some((i) => i.reason.includes("Notes, feed") &&
136146 i.reason.includes("(told by bob, by scout)")),
137147 ).toBe(true);
138148 });
139149
140150 test("the merge commit is the agent's, with both trailers; the board card says who told", async () => {
141 const pull = getPull(app.db, repo.id, number)!;
142 const merged = await mergePull(app, alice, repo, pull, "squash");
151 const number = await answered(shipped, "merge");
152 const pull = getPull(app.db, shipped.id, number)!;
153 const merged = await mergePull(app, alice, shipped, pull, "squash");
143154 if (!merged.ok) throw new Error(JSON.stringify(merged.error));
144155 const log = Bun.spawnSync(
145156 ["git", "log", "-1", "--format=%an|%cn%n%B", "main"],
146 { cwd: repoDir(app.reposRoot, "alice", "notes") },
157 { cwd: repoDir(app.reposRoot, "alice", "shipped") },
147158 ).stdout.toString();
148159 expect(log).toContain("scout|alice");
149160 expect(log).toContain("Requested-by: bob <[email protected]>");
test/vitals.test.ts +5
@@ -104,6 +104,7 @@ test("a full queue shows in queue, and an old one in needs", async () => {
104104 // Errors and refused calls are process-wide: another file in the shard may have left some.
105105 app.signals.errors.clear();
106106 app.signals.refused.clear();
107 app.db.query("DELETE FROM run").run();
107108 const old = Date.now() - 3_600_000;
108109 for (let i = 0; i < 3; i++) createRun(app.db, { repoId, kind: "agent" });
109110 const v = vitals({ db: app.db, signals: app.signals, dataDir });
@@ -252,6 +253,10 @@ test("two forges in one process keep their own error and refusal signals", async
252253 const { ERROR_BURST } = await import("../src/kernel/vitals.ts");
253254 const { createLog } = await import("../src/kernel/log.ts");
254255 const { noteRefusedInternal } = await import("../src/kernel/internal_refused.ts");
256 // The forge's ledger takes every unowned logger's errors while it lives, so
257 // anything another file in this process logged is in `app`'s: start empty.
258 (await import("../src/kernel/log.ts")).clearErrors();
259 app.signals.refused.clear();
255260 const dirB = await mkdtemp(join(tmpdir(), "forge-vitals-b-"));
256261 const other = createApp({ dataDir: dirB, port: 0 });
257262 try {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs