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

One test walks a stranger from sign-up to a planned forge through the API alone, naming the step that broke #3098

Merged opened this

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

Merged

scout/forge:scout/3096-one-test-walks-a-stranger-from-sign-up-t → main 2 commits fork

  • Merged. scout/3096-one-test-walks-a-stranger-from-sign-up-t was merged into main as 0473ebe.

Review

Sign in to review this change.
3 files changed +87 −1
docs/SPAWN.md +1 −1
@@ -70,7 +70,7 @@ Children report in; the parent never reaches in. Every ten minutes a spawned chi
7070
7171 ## Callers
7272
73 `POST /api/v1/forges {name, apps[], crew[], direction?, keepAccess?, modelKey?, budgetUsd?, dryRun}` (rw token, admin only) and the MCP `spawn` tool share `src/spawn/door.ts`. `dryRun` is true unless it is `false`: the answer is the plan and what is left behind, and nothing is made. `GET /api/v1/forges` lists the ledger's children. The child's org is its name; `crew` names the agents whose briefs travel. Errors: 401 unauthorized, 403 forbidden / cross_origin, 422 invalid, 409 refused / exists, 500 failed — each with one sentence. The MCP lets an `ro` token plan, never make.
73 `POST /api/v1/forges {name, apps[], crew[], direction?, keepAccess?, modelKey?, budgetUsd?, dryRun}` (rw token, admin only) and the MCP `spawn` tool share `src/spawn/door.ts`. `dryRun` is true unless it is `false`: the answer is the plan, what is left behind, and `firstAdmin` (the caller's username, or `null` for an anonymous plan, where whoever claims the child becomes its admin), and nothing is made. `GET /api/v1/forges` lists the ledger's children. The child's org is its name; `crew` names the agents whose briefs travel. Errors: 401 unauthorized, 403 forbidden / cross_origin, 422 invalid, 409 refused / exists, 500 failed — each with one sentence. The MCP lets an `ro` token plan, never make.
7474
7575 ## The forge's own code, and the chain (#1085)
7676
src/spawn/door.ts +2
@@ -268,6 +268,8 @@ export async function spawnDoor(
268268 body: {
269269 dryRun: true,
270270 plan: planned.value,
271 // Who the child's first admin will be: the caller, or whoever claims it.
272 firstAdmin: user?.username ?? null,
271273 ...dryRun(planned.value, source),
272274 ...(planned.value.budgetUsd === null ? {} : { keyNote: KEY_BUDGET_NOTE }),
273275 },
test/stranger-to-forge.test.ts added +84
@@ -0,0 +1,84 @@
1 /**
2 * A stranger to a planned forge through the API alone (#3096, part of #2914):
3 * sign up with a username and password, sign in, take a token, and plan a
4 * forge. No socket, each request handed to forgeHttp(app).handle. Each step
5 * is its own test, so a failure names the step that broke.
6 */
7 import { afterAll, beforeAll, expect, test } from "bun:test";
8 import { mkdtemp, rm } from "node:fs/promises";
9 import { tmpdir } from "node:os";
10 import { join } from "node:path";
11 import { createApp, type App } from "../src/app.ts";
12 import { forgeHttp, type ForgeHttp } from "../src/http/server.ts";
13 import { quietLogs } from "./helpers/globals.ts";
14
15 quietLogs();
16
17 const ORIGIN = "http://forge.test";
18 const USERNAME = "stranger";
19 const PASSWORD = "a-long-passphrase";
20
21 let dir = "";
22 let app: App;
23 let http: ForgeHttp;
24 let cookie = "";
25 let token = "";
26
27 const call = (path: string, body: unknown, headers: Record<string, string> = {}) =>
28 http.handle(
29 new Request(`${ORIGIN}${path}`, {
30 method: "POST",
31 headers: { "content-type": "application/json", origin: ORIGIN, ...headers },
32 body: JSON.stringify(body),
33 }),
34 );
35
36 beforeAll(async () => {
37 dir = await mkdtemp(join(tmpdir(), "forge-stranger-"));
38 delete process.env.FORGE_EDGE_TOKEN;
39 app = createApp({ dataDir: dir, port: 0 });
40 http = forgeHttp(app);
41 });
42
43 afterAll(async () => {
44 http.close();
45 app.close();
46 await rm(dir, { recursive: true, force: true });
47 });
48
49 test("step 1, sign-up: a username and a password, no email, makes an account", async () => {
50 const res = await call("/api/v1/users", { username: USERNAME, password: PASSWORD });
51 expect(res.status).toBe(201);
52 });
53
54 test("step 2, sign-in: the same credentials open a session", async () => {
55 const res = await call("/api/v1/sessions", { username: USERNAME, password: PASSWORD });
56 expect(res.status).toBe(201);
57 cookie = (res.headers.get("set-cookie") ?? "").split(";")[0] ?? "";
58 expect(cookie).not.toBe("");
59 });
60
61 test("step 3, token: the session mints an rw token", async () => {
62 const res = await call("/api/v1/tokens", { scope: "rw", name: "stranger-to-forge" }, { cookie });
63 expect(res.status).toBe(201);
64 token = ((await res.json()) as { token?: string }).token ?? "";
65 expect(token).not.toBe("");
66 });
67
68 test("step 4, plan: a dry-run POST /api/v1/forges names the owner as first admin", async () => {
69 const res = await call(
70 "/api/v1/forges",
71 { name: "strangers-forge", dryRun: true },
72 { authorization: `Bearer ${token}` },
73 );
74 expect(res.status).toBe(200);
75 const plan = (await res.json()) as { dryRun?: boolean; firstAdmin?: string | null };
76 expect(plan.dryRun).toBe(true);
77 expect(plan.firstAdmin).toBe(USERNAME);
78 });
79
80 test("an anonymous plan names no first admin: whoever claims the child becomes it", async () => {
81 const res = await call("/api/v1/forges", { name: "claimed-later", dryRun: true });
82 expect(res.status).toBe(200);
83 expect(((await res.json()) as { firstAdmin?: string | null }).firstAdmin).toBeNull();
84 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs