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
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/2070-told-by-and-standing-every-process-are-g → main 2 commits fork
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", () => { | ||
| 152 | 152 | const { ended } = await streamOn(drainingServers, cookie); |
| 153 | 153 | expect(await within(ended, SWEEP_MS * 6)).toBe(false); |
| 154 | 154 | }); |
| 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. | |
| 156 | 158 | test("a git transfer under way on the draining process is cut", async () => { |
| 157 | 159 | const token = createToken(fresh.db, sam.id, "t", "rw").token; |
| 158 | 160 | // 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", () => { | ||
| 162 | 164 | }); |
| 163 | 165 | const transfer = fetch(`${drainingServers.url}/sam/lab.git/git-upload-pack`, { |
| 164 | 166 | method: "POST", |
| 167 | keepalive: false, | |
| 165 | 168 | headers: { |
| 166 | 169 | authorization: `Basic ${Buffer.from(`sam:${token}`).toString("base64")}`, |
| 167 | 170 | "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", () => { | ||
| 27 | 27 | let alice: User; |
| 28 | 28 | let bob: User; |
| 29 | 29 | let repo: Repo; |
| 30 | let shipped: Repo; | |
| 30 | 31 | let cookie: string; |
| 31 | let number: number; | |
| 32 | 32 | |
| 33 | 33 | const get = (path: string) => |
| 34 | 34 | fetch(`${servers.url}${path}`, { headers: { cookie } }); |
| 35 | 35 | |
| 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 | ||
| 36 | 81 | beforeAll(async () => { |
| 37 | 82 | dataDir = await mkdtemp(join(tmpdir(), "forge-toldby-")); |
| 38 | 83 | app = createApp({ dataDir, port: 0 }); |
| @@ -63,50 +108,12 @@ describe("who told and who shipped", () => { | ||
| 63 | 108 | }) |
| 64 | 109 | ).headers.get("set-cookie") ?? "" |
| 65 | 110 | ).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"); | |
| 89 | 113 | const scout = await createAgent(app, alice, { username: "scout" }); |
| 90 | 114 | if (!scout.ok) throw new Error(String(scout.error)); |
| 91 | 115 | 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"); | |
| 110 | 117 | }, 30_000); |
| 111 | 118 | |
| 112 | 119 | afterAll(async () => { |
| @@ -115,7 +122,8 @@ describe("who told and who shipped", () => { | ||
| 115 | 122 | await rm(dataDir, { recursive: true, force: true }); |
| 116 | 123 | }); |
| 117 | 124 | |
| 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"); | |
| 119 | 127 | const pull = getPull(app.db, repo.id, number)!; |
| 120 | 128 | expect(toldBy(app.db, pull)).toBe("bob"); |
| 121 | 129 | expect(toldBy(app.db, { ...pull, body: "no issue here" })).toBeNull(); |
| @@ -123,27 +131,30 @@ describe("who told and who shipped", () => { | ||
| 123 | 131 | }); |
| 124 | 132 | |
| 125 | 133 | test("the pull's page names both, escaped", async () => { |
| 134 | const number = await answered(repo, "page"); | |
| 126 | 135 | const html = await (await get(`/alice/notes/pulls/${number}`)).text(); |
| 127 | 136 | expect(html).toContain("in answer to"); |
| 128 | 137 | expect(html).toMatch(/in answer to[\s\S]{0,200}bob/); |
| 129 | 138 | expect(html).toContain("scout"); |
| 130 | 139 | }); |
| 131 | 140 | |
| 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; | |
| 134 | 144 | expect( |
| 135 | items.some((i) => i.reason.includes("Faster notes") && | |
| 145 | items.some((i) => i.reason.includes("Notes, feed") && | |
| 136 | 146 | i.reason.includes("(told by bob, by scout)")), |
| 137 | 147 | ).toBe(true); |
| 138 | 148 | }); |
| 139 | 149 | |
| 140 | 150 | 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"); | |
| 143 | 154 | if (!merged.ok) throw new Error(JSON.stringify(merged.error)); |
| 144 | 155 | const log = Bun.spawnSync( |
| 145 | 156 | ["git", "log", "-1", "--format=%an|%cn%n%B", "main"], |
| 146 | { cwd: repoDir(app.reposRoot, "alice", "notes") }, | |
| 157 | { cwd: repoDir(app.reposRoot, "alice", "shipped") }, | |
| 147 | 158 | ).stdout.toString(); |
| 148 | 159 | expect(log).toContain("scout|alice"); |
| 149 | 160 | 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 () => { | ||
| 104 | 104 | // Errors and refused calls are process-wide: another file in the shard may have left some. |
| 105 | 105 | app.signals.errors.clear(); |
| 106 | 106 | app.signals.refused.clear(); |
| 107 | app.db.query("DELETE FROM run").run(); | |
| 107 | 108 | const old = Date.now() - 3_600_000; |
| 108 | 109 | for (let i = 0; i < 3; i++) createRun(app.db, { repoId, kind: "agent" }); |
| 109 | 110 | 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 | ||
| 252 | 253 | const { ERROR_BURST } = await import("../src/kernel/vitals.ts"); |
| 253 | 254 | const { createLog } = await import("../src/kernel/log.ts"); |
| 254 | 255 | 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(); | |
| 255 | 260 | const dirB = await mkdtemp(join(tmpdir(), "forge-vitals-b-")); |
| 256 | 261 | const other = createApp({ dataDir: dirB, port: 0 }); |
| 257 | 262 | try { |
new above
offline