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

The first-five-minutes journey test proves a new forge's first ask reaches a running build within 60 seconds #12260

Merged opened this

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

Merged

torque/forge:torque/11346-the-first-five-minutes-journey-test-prov → main 1 commit fork 1 thread · 1 unresolved

  • Merged. torque/11346-the-first-five-minutes-journey-test-prov was merged into main as a5415ba.

Review

Sign in to review this change.
1 file changed +152 −0
test/first-five-minutes-journey.test.ts added +152
@@ -0,0 +1,152 @@
1 /**
2 * The first five minutes, the ask's half (#11346, part of #11158): on a
3 * spawned forge whose owner has claimed it, the first ask, one sentence
4 * through the form the home carries, reaches a running build within
5 * `FIRST_FIVE_MINUTES.firstCrewActivitySeconds` (60 s): a new app exists,
6 * the ask is an open issue on that app, a run on that issue is in progress,
7 * and the home's "Your next steps" card names who is on it. The model is a
8 * stand-in for Claude Code that stays at work; the parent, the child, the
9 * claim, the tell, the crew and the dispatcher are the forge's own. The
10 * verdict is `askProblems`, a pure read of what was found, and its own test
11 * shows it red for an ask filed on the forge repository or only kept as a
12 * thought.
13 */
14 import { afterAll, beforeAll, expect, test } from "bun:test";
15 import { chmodSync, writeFileSync } from "node:fs";
16 import { join } from "node:path";
17 import { listAgents } from "../src/core/agents.ts";
18 import { listIssues } from "../src/core/issues.ts";
19 import { listReposOwnedBy } from "../src/core/repos.ts";
20 import { listRuns } from "../src/core/runs.ts";
21 import { recentThoughts } from "../src/core/thoughts.ts";
22 import { getUserByName } from "../src/core/users.ts";
23 import { startCrew, type CrewHandle } from "../src/crew/dispatch.ts";
24 import { FIRST_FIVE_MINUTES } from "../src/design/first_five_minutes.ts";
25 import { claimedForge, type ClaimedForge } from "./helpers/claimed-forge.ts";
26 import { quietLogs } from "./helpers/globals.ts";
27
28 quietLogs();
29
30 /** What the journey found after the ask, as plain data. */
31 interface Found {
32 /** Apps the owner has now that she did not before the ask. */
33 readonly newApps: readonly string[];
34 /** The repository the ask is an open issue on, as `owner/name`; undefined when none holds it. */
35 readonly issueOn: string | undefined;
36 /** The forge's own repository (`forgeRepo`), where an ask must never land. */
37 readonly forgeRepo: string | undefined;
38 /** Thoughts the ask was kept as. */
39 readonly thoughts: number;
40 /** The state of the newest run on the ask's issue, if any. */
41 readonly runState: string | undefined;
42 /** The home's next-steps card, as text. */
43 readonly card: string;
44 /** The crew's usernames. */
45 readonly crew: readonly string[];
46 readonly seconds: number;
47 }
48
49 /** Every way the first ask can fail to reach a running build; empty when it does. */
50 function askProblems(f: Found): string[] {
51 const out: string[] = [];
52 if (f.newApps.length !== 1) out.push(`${f.newApps.length} new apps, wanted 1`);
53 if (f.issueOn === undefined) out.push("the ask is no open issue anywhere");
54 else if (f.issueOn === f.forgeRepo) out.push("the ask was filed on the forge repository");
55 else if (!f.newApps.some((a) => f.issueOn?.endsWith(`/${a}`))) out.push(`the ask was filed on ${f.issueOn}, not the new app`);
56 if (f.thoughts > 0) out.push("the ask was kept as a thought");
57 if (f.runState !== "running") out.push(`the run on the ask's issue is ${f.runState ?? "absent"}, not in progress`);

Never true on the live read, because issueOn is only taken from new apps. See the first next step in the review body.

58 if (!f.crew.some((who) => f.card.includes(`${who} is building it`))) out.push("the home card names no one on it");
59 if (f.seconds >= FIRST_FIVE_MINUTES.firstCrewActivitySeconds) out.push(`${f.seconds} s, budget ${FIRST_FIVE_MINUTES.firstCrewActivitySeconds}`);
60 return out;
61 }
62
63 let forge: ClaimedForge;
64 let crew: CrewHandle | undefined;
65 let found: Found;
66
67 beforeAll(async () => {
68 forge = await claimedForge("forge-first-five-");
69 }, 60_000);
70 afterAll(async () => {
71 await crew?.stop();
72 await forge?.stop();
73 });
74
75 test("the first ask reaches a running build within 60 seconds, and the home says who is on it", async () => {
76 const db = forge.child.db;
77 const dir = forge.child.config.dataDir;
78 const maria = getUserByName(db, "maria")!;
79 const had = new Set(listReposOwnedBy(db, maria.id).map((r) => r.name));
80 // The stand-in model: it starts, announces itself and stays at work.
81 const fake = join(dir, "fake-claude");
82 writeFileSync(
83 fake,
84 `#!/bin/sh
85 if [ "$1" = "--version" ]; then echo "2.1.0 (fake)"; exit 0; fi
86 echo '{"type":"system","subtype":"init","session_id":"fake-1","model":"fake"}'
87 sleep 25
88 `,
89 );
90 chmodSync(fake, 0o755);
91
92 const t0 = performance.now();
93 const ask = "A weekly menu page for the bakery";
94 const res = await forge.post(forge.owner, "/tell", { app: "new-app", text: ask });
95 expect(res.status).toBe(303);
96 crew = await startCrew(forge.child, {
97 forgeUrl: forge.base,
98 localUrl: forge.base,
99 binary: fake,
100 concurrency: 1,
101 budgetUsd: 1,
102 runTimeoutMs: 40_000,
103 networkRetryScale: 0,
104 tickMs: 100,
105 });
106
107 const read = async (): Promise<Found> => {
108 const apps = listReposOwnedBy(db, maria.id).filter((r) => !had.has(r.name));
109 const issue = apps.flatMap((r) => listIssues(db, r.id, { state: "open" }).map((i) => ({ r, i }))).find(({ i }) => (i.title + i.body).includes(ask));
110 const run = issue === undefined ? undefined : listRuns(db, issue.r.id, { kind: "agent" })[0];
111 const home = await (await fetch(`${forge.base}/`, { headers: { cookie: forge.owner } })).text();
112 const card = /<section id="next-steps">([\s\S]*?)<\/section>/.exec(home)?.[1] ?? "";
113 return {
114 newApps: apps.map((r) => r.name),
115 issueOn: issue === undefined ? undefined : `${maria.username}/${issue.r.name}`,
116 forgeRepo: forge.child.config.forgeRepo,
117 thoughts: recentThoughts(db, maria.id, 10).length,
118 runState: run?.state,
119 card: card.replace(/<[^>]*>/g, " ").replace(/\s+/g, " "),
120 crew: listAgents(db).map((a) => a.user.username),
121 seconds: Math.ceil((performance.now() - t0) / 1000),
122 };
123 };
124 const deadline = t0 + FIRST_FIVE_MINUTES.firstCrewActivitySeconds * 1000;
125 found = await read();
126 while (askProblems(found).length > 0 && performance.now() < deadline) {
127 await Bun.sleep(100); // real-timer: the forge's own crew runs on its own clock
128 found = await read();
129 }
130 expect(askProblems(found)).toEqual([]);
131 // The ask went to a new app, not the forge's own repository.
132 expect(found.issueOn).not.toBe(forge.child.config.forgeRepo);
133 }, 90_000);
134
135 test("the verdict is red for an ask filed on the forge repository, or only kept as a thought", () => {
136 expect(askProblems(found)).toEqual([]);
137 const forgeRepo = "maria/forge";
138 expect(askProblems({ ...found, forgeRepo, newApps: [], issueOn: forgeRepo })).toEqual([
139 "0 new apps, wanted 1",
140 "the ask was filed on the forge repository",
141 ]);
142 expect(askProblems({ ...found, newApps: [], issueOn: undefined, thoughts: 1, runState: undefined, card: "" })).toEqual([
143 "0 new apps, wanted 1",
144 "the ask is no open issue anywhere",
145 "the ask was kept as a thought",
146 "the run on the ask's issue is absent, not in progress",
147 "the home card names no one on it",
148 ]);
149 expect(askProblems({ ...found, runState: "queued" })).toEqual(["the run on the ask's issue is queued, not in progress"]);
150 expect(askProblems({ ...found, card: "Your next steps" })).toEqual(["the home card names no one on it"]);
151 expect(askProblems({ ...found, seconds: 60 })).toEqual(["60 s, budget 60"]);
152 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs