Public: anyone can read this; the crew can change it.
The client list says in plain words where each sent forge link stands: sent, opened, signed in, first tell #10978
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/10923-the-client-list-says-in-plain-words-wher → main 7 commits fork 1 thread · 1 unresolved
13 files
changed +207
−13
docs/MCP.md +1 −1
| @@ -112,7 +112,7 @@ bytes in `tools/list`. After changing a tool run `bun scripts/mcp-table.ts`; | ||
| 112 | 112 | | `following` | — | Where a fork stands against what it follows: in step, behind, ahead, diverged | 243 | |
| 113 | 113 | | `forge_proposals` | — | What this forge proposed to its parent; `reachable` false if it is silent | 245 | |
| 114 | 114 | | `forge_view` | — | The forge as one picture: counts, apps, runs with stations, health | 189 | |
| 115 | | `forges` | — | Forges sent from here (admin): name, address, apps, claimed, spendTodayUsd, ceilingUsd, size, use | 216 | | |
| 115 | | `forges` | — | Forges sent from here (admin): name, address, apps, claimed, stage, resend, spendTodayUsd, ceilingUsd, size, use | 231 | | |
| 116 | 116 | | `fork` | — | Fork a repository into your account; for a guest, a copy to send changes back from | 232 | |
| 117 | 117 | | `fork_tree` | — | The fork tree around a repo: ancestors, siblings and descendants you may read | 347 | |
| 118 | 118 | | `forks` | — | A repo's forks you may see, paged by limit and cursor (next_cursor), each with commits ahead and behind | 345 | |
docs/features/forge-link-states.md added +30
| @@ -0,0 +1,30 @@ | ||
| 1 | # Where a sent forge link stands | |
| 2 | ||
| 3 | This page is for the person who sends forges to clients and wants to know, | |
| 4 | without asking, whether a client took the link. It extends | |
| 5 | [](client-forge.md). | |
| 6 | ||
| 7 | ## The four states | |
| 8 | ||
| 9 | `/forges`, the terminal's forges screen and MCP tool `forges` say the same | |
| 10 | words for each forge sent, read from what is already kept: | |
| 11 | ||
| 12 | - **Sent**: the link was made and not opened. The time is when it was sent. | |
| 13 | - **Opened**: the link was spent, but nobody has signed in yet. | |
| 14 | - **Signed in**: the client made an account. The time is when the link was | |
| 15 | spent; when the child did not say, no time is shown. | |
| 16 | - **Last tell**: the client told the crew something. The time is the latest | |
| 17 | tell the child's heartbeat carries (it carries the latest, not the first). | |
| 18 | ||
| 19 | ## Resend | |
| 20 | ||
| 21 | A link unopened for three days reads "Not opened in 3 days", and the page's | |
| 22 | button says **Resend** (it posts to `/forges/<name>/claim-link`, the same as | |
| 23 | "Send a fresh link": a new link, the old one stops working). In MCP, the row's | |
| 24 | `stage.resend` is true; the action is MCP tool `claim_link` with the forge's | |
| 25 | name. A claimed forge is never marked. | |
| 26 | ||
| 27 | ## Left out | |
| 28 | ||
| 29 | - The first tell's time: the heartbeat carries only the latest tell. | |
| 30 | - A one-call resend in MCP: `claim_link` is that call. |
src/contract/openapi/routes/getApiV1Forge.ts +1 −1
| @@ -1,3 +1,3 @@ | ||
| 1 | 1 | import type { Route } from "../shared.ts"; |
| 2 | 2 | |
| 3 | export const route: Route = { method: "get", path: "/api/v1/forge", summary: "A spawned forge's word about itself: { claimed, lastActivityAt, link? }; lastActivityAt and link ({ state, revokedAt }) are null or absent unless the parent's secret is sent", auth: false, role: "anonymous", refusals: { not_found: "404: this forge was not spawned" } }; | |
| 3 | export const route: Route = { method: "get", path: "/api/v1/forge", summary: "A spawned forge's word about itself: { claimed, lastActivityAt, link? }; lastActivityAt and link ({ state, revokedAt, usedAt }) are null or absent unless the parent's secret is sent", auth: false, role: "anonymous", refusals: { not_found: "404: this forge was not spawned" } }; |
src/http/mcp/tools/forges.ts +2 −2
| @@ -3,9 +3,9 @@ import { good, bad, type Tool } from "../shared.ts"; | ||
| 3 | 3 | |
| 4 | 4 | export const tool: Tool = { |
| 5 | 5 | name: "forges", |
| 6 | doc: "an admin's list of the forges sent from here: name, address, whom for, apps, claimed, last activity, reachable, size, use", | |
| 6 | doc: "an admin's list of the forges sent from here: name, address, whom for, apps, claimed, stage (sent/opened/signed in/last tell; resend true when unopened 3 days: claim_link), last activity, reachable, size, use", | |
| 7 | 7 | description: |
| 8 | "Forges sent from here (admin): name, address, apps, claimed, spendTodayUsd, ceilingUsd, size, use.", | |
| 8 | "Forges sent from here (admin): name, address, apps, claimed, stage, resend, spendTodayUsd, ceilingUsd, size, use.", | |
| 9 | 9 | inputSchema: { type: "object", properties: {} }, |
| 10 | 10 | writes: false, |
| 11 | 11 | async run(ctx, user) { |
src/spawn/claim.ts +3 −1
| @@ -249,6 +249,8 @@ export type LinkState = "open" | "used" | "ended"; | ||
| 249 | 249 | export interface LinkInfo { |
| 250 | 250 | readonly state: LinkState; |
| 251 | 251 | readonly revokedAt: string | null; |
| 252 | /** When the link was spent (someone opened it and made an account), ISO; absent while unspent. */ | |
| 253 | readonly usedAt?: string | null; | |
| 252 | 254 | } |
| 253 | 255 | |
| 254 | 256 | /** True while an ended link can still be brought back: revoked in the last minute, in date, unspent. */ |
| @@ -276,7 +278,7 @@ const stateOf = (s: Stored, now: number): LinkState => | ||
| 276 | 278 | /** The claim link in this forge's own database; null when it has none. */ |
| 277 | 279 | export function linkOf(db: Database, now = Date.now()): LinkInfo | null { |
| 278 | 280 | const found = readStored(db); |
| 279 | return found === undefined ? null : { state: stateOf(found.stored, now), revokedAt: found.stored.r ?? null }; | |
| 281 | return found === undefined ? null : { state: stateOf(found.stored, now), revokedAt: found.stored.r ?? null, usedAt: found.stored.u }; | |
| 280 | 282 | } |
| 281 | 283 | |
| 282 | 284 | /** Replace the stored link only if nobody changed it since it was read. */ |
src/spawn/clients.ts +52 −3
| @@ -52,6 +52,8 @@ export interface ClientRow { | ||
| 52 | 52 | readonly link: LinkInfo | null; |
| 53 | 53 | /** Why this forge needs its owner now; null when it does not. */ |
| 54 | 54 | readonly attention: Attention | null; |
| 55 | /** Where the sent link stands, in four plain states; null while it is being made or the child did not answer. */ | |
| 56 | readonly stage: Stage | null; | |
| 55 | 57 | /** Dollars the child's crew spent today, from its last heartbeat; null when it has not said. */ |
| 56 | 58 | readonly spendTodayUsd: number | null; |
| 57 | 59 | /** The child's forge cap in dollars; null when none or unsaid. */ |
| @@ -144,7 +146,8 @@ function linkOfBody(v: unknown): LinkInfo | null { | ||
| 144 | 146 | if (typeof v !== "object" || v === null) return null; |
| 145 | 147 | const { state, revokedAt } = v as Record<string, unknown>; |
| 146 | 148 | if (!LINK_STATES.includes(state as LinkState)) return null; |
| 147 | return { state: state as LinkState, revokedAt: typeof revokedAt === "string" && !Number.isNaN(Date.parse(revokedAt)) ? revokedAt : null }; | |
| 149 | const iso = (x: unknown): string | null => (typeof x === "string" && !Number.isNaN(Date.parse(x)) ? x : null); | |
| 150 | return { state: state as LinkState, revokedAt: iso(revokedAt), usedAt: iso((v as Record<string, unknown>)["usedAt"]) }; | |
| 148 | 151 | } |
| 149 | 152 | |
| 150 | 153 | async function probe(app: App, child: Child): Promise<ForgeState | undefined> { |
| @@ -281,11 +284,57 @@ export function attentionOf(r: ClientRow, now: number): Attention | null { | ||
| 281 | 284 | return null; |
| 282 | 285 | } |
| 283 | 286 | |
| 287 | export type StageKey = "sent" | "opened" | "signedin" | "tell"; | |
| 288 | ||
| 289 | /** Where a sent link stands, the time that changed, and whether it is time to send it again. */ | |
| 290 | export interface Stage { | |
| 291 | readonly key: StageKey; | |
| 292 | /** The plain words, the same on the page, the terminal and the MCP answer. */ | |
| 293 | readonly words: string; | |
| 294 | /** When it changed, ISO; null when the child did not say. */ | |
| 295 | readonly at: string | null; | |
| 296 | /** True when the link has gone unopened for three days: the one action is to resend it. */ | |
| 297 | readonly resend: boolean; | |
| 298 | } | |
| 299 | ||
| 300 | export const STAGE_WORDS: Record<StageKey, string> = { | |
| 301 | sent: "Sent", | |
| 302 | opened: "Opened", | |
| 303 | signedin: "Signed in", | |
| 304 | tell: "Last tell", | |
| 305 | }; | |
| 306 | ||
| 307 | export const STALE_WORDS = "Not opened in 3 days"; | |
| 308 | ||
| 309 | /** | |
| 310 | * Sent, opened, signed in, told: read from what is already kept (the | |
| 311 | * ledger's send time, the link's spent time, the claim, the last tell in the | |
| 312 | * child's heartbeat). The heartbeat carries only the latest tell, so the last stage says "Last tell", never "First". | |
| 313 | */ | |
| 314 | export function stageOf(r: Omit<ClientRow, "attention" | "stage">, now: number): Stage | null { | |
| 315 | if (!r.made || r.claimed === null) return null; | |
| 316 | const at = (...xs: (string | null | undefined)[]): string | null => xs.find((x) => typeof x === "string") ?? null; | |
| 317 | const make = (key: StageKey, when: string | null, resend = false): Stage => ({ | |
| 318 | key, | |
| 319 | words: resend ? STALE_WORDS : STAGE_WORDS[key], | |
| 320 | at: when, | |
| 321 | resend, | |
| 322 | }); | |
| 323 | if (r.claimed) { | |
| 324 | return r.nextStep.lastTellAt !== null | |
| 325 | ? make("tell", r.nextStep.lastTellAt) | |
| 326 | : make("signedin", at(r.link?.usedAt)); | |
| 327 | } | |
| 328 | if (r.link?.state === "used") return make("opened", at(r.link.usedAt)); | |
| 329 | const sent = r.sentAt === null ? NaN : Date.parse(r.sentAt); | |
| 330 | return make("sent", r.sentAt, !Number.isNaN(sent) && now - sent > UNCLAIMED_MS); | |
| 331 | } | |
| 332 | ||
| 284 | 333 | /** Rows that need the owner first, by reason; then by last activity, newest first. */ |
| 285 | export function orderRows(rows: readonly Omit<ClientRow, "attention">[], now: number): ClientRow[] { | |
| 334 | export function orderRows(rows: readonly Omit<ClientRow, "attention" | "stage">[], now: number): ClientRow[] { | |
| 286 | 335 | const tier = (a: Attention | null) => (a === null ? ATTENTION_ORDER.length : ATTENTION_ORDER.indexOf(a)); |
| 287 | 336 | return rows |
| 288 | .map((r) => ({ ...r, attention: attentionOf(r as ClientRow, now) })) | |
| 337 | .map((r) => ({ ...r, attention: attentionOf(r as ClientRow, now), stage: stageOf(r as ClientRow, now) })) | |
| 289 | 338 | .sort( |
| 290 | 339 | (a, b) => |
| 291 | 340 | tier(a.attention) - tier(b.attention) || |
src/tui/screens/forges.ts +1 −1
| @@ -24,7 +24,7 @@ export function forgeLines(rows: readonly ClientRow[]): [string, string][] { | ||
| 24 | 24 | if (rows.length === 0) return [["", "You have not sent a forge yet"]]; |
| 25 | 25 | return rows.flatMap((r): [string, string][] => [[ |
| 26 | 26 | r.name, |
| 27 | [r.made ? (r.awake === true ? "awake" : "asleep") : null, r.attention === null ? null : ATTENTION_WORDS[r.attention], r.for === null ? null : `for ${r.for}`, CLAIM_WORDS[claimState(r)], linkWords(r.link), runningWords(r), r.nextStep.words, spendWords(r)?.words ?? null, creditWords(r), useLine(r.size, r.use), r.made ? diskWords(r.disk) : null, r.lastHeartbeatAt === null ? "no heartbeat yet" : `heartbeat ${timeAgo(r.lastHeartbeatAt)}`] | |
| 27 | [r.made ? (r.awake === true ? "awake" : "asleep") : null, r.attention === null ? null : ATTENTION_WORDS[r.attention], r.for === null ? null : `for ${r.for}`, CLAIM_WORDS[claimState(r)], linkWords(r.link), runningWords(r), r.stage?.words ?? null, r.nextStep.words, spendWords(r)?.words ?? null, creditWords(r), useLine(r.size, r.use), r.made ? diskWords(r.disk) : null, r.lastHeartbeatAt === null ? "no heartbeat yet" : `heartbeat ${timeAgo(r.lastHeartbeatAt)}`] | |
| 28 | 28 | .filter((m): m is string => m !== null) |
| 29 | 29 | .join(" · "), |
| 30 | 30 | ], ...addressLines(r)]); |
src/web/pages/forges.ts +4 −2
| @@ -55,7 +55,9 @@ export function forgeRow(r: ClientRow, now: number): Html { | ||
| 55 | 55 | runningNote(r), |
| 56 | 56 | spendNote(r), |
| 57 | 57 | creditWords(r) === null ? null : html`<span data-credit>${creditWords(r)}</span>`, |
| 58 | r.sentAt === null ? null : html`sent ${time(r.sentAt)}`, | |
| 58 | r.stage === null | |
| 59 | ? null | |
| 60 | : html`<span data-stage="${r.stage.key}"${r.stage.resend ? html` data-resend` : null}>${r.stage.words}${r.stage.at === null ? null : html` ${time(r.stage.at)}`}</span>`, | |
| 59 | 61 | r.lastActivityAt === null |
| 60 | 62 | ? null |
| 61 | 63 | : html`last activity ${time(r.lastActivityAt)}`, |
| @@ -88,7 +90,7 @@ export function forgeRow(r: ClientRow, now: number): Html { | ||
| 88 | 90 | ...(state === "yes" ? { tone: "success" as const } : { variant: "outline" as const }), |
| 89 | 91 | })}${state === "no" |
| 90 | 92 | ? html`<form method="post" action="/forges/${r.name}/claim-link"> |
| 91 | ${btn("Send a fresh link", { size: "sm", title: "A new claim link; the old one stops working" })} | |
| 93 | ${btn(r.stage?.resend === true ? "Resend" : "Send a fresh link", { size: "sm", title: "A new claim link; the old one stops working" })} | |
| 92 | 94 | </form>` |
| 93 | 95 | : null}${r.link === null |
| 94 | 96 | ? null |
test/client-states.test.ts added +108
| @@ -0,0 +1,108 @@ | ||
| 1 | /** | |
| 2 | * Where each sent forge link stands (#10923): sent, opened, signed in, | |
| 3 | * first tell, in the same words on the page and in the MCP answer, read | |
| 4 | * from what is already kept. A link unopened for three days says so and | |
| 5 | * offers one action, resend. | |
| 6 | */ | |
| 7 | ||
| 8 | import { describe, expect, test } from "bun:test"; | |
| 9 | import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; | |
| 10 | import { tmpdir } from "node:os"; | |
| 11 | import { join } from "node:path"; | |
| 12 | import { createApp } from "../src/app.ts"; | |
| 13 | import { forgeLines } from "../src/tui/screens/forges.ts"; | |
| 14 | import { clientRows, orderRows, STAGE_WORDS, useChildFetch, STALE_WORDS, stageOf, type ClientRow } from "../src/spawn/clients.ts"; | |
| 15 | import { forgeRow } from "../src/web/pages/forges.ts"; | |
| 16 | import { tool } from "../src/http/mcp/tools/forges.ts"; | |
| 17 | ||
| 18 | const NOW = Date.parse("2026-11-10T12:00:00Z"); | |
| 19 | const DAY = 86_400_000; | |
| 20 | const ago = (ms: number) => new Date(NOW - ms).toISOString(); | |
| 21 | ||
| 22 | const base = (over: Partial<Omit<ClientRow, "attention" | "stage">> = {}) => | |
| 23 | ({ | |
| 24 | name: "acme", url: "https://acme.example", for: "Maria", apps: [], sentAt: ago(DAY), made: true, claimed: false, | |
| 25 | lastActivityAt: null, running: null, reach: null, link: { state: "open", revokedAt: null, usedAt: null }, spendTodayUsd: null, | |
| 26 | ceilingUsd: null, size: "small", use: { kind: "shared" }, disk: { own: false }, lastHeartbeatAt: null, | |
| 27 | nextStep: { key: "claim", words: "not claimed yet", lastTellAt: null }, ...over, | |
| 28 | }) as unknown as Omit<ClientRow, "attention" | "stage">; | |
| 29 | ||
| 30 | const stage = (over = {}) => orderRows([base(over)], NOW)[0]!; | |
| 31 | ||
| 32 | describe("client states", () => { | |
| 33 | test("sent, with the time it was sent", () => { | |
| 34 | const s = stage().stage!; | |
| 35 | expect([s.key, s.words, s.at, s.resend]).toEqual(["sent", "Sent", ago(DAY), false]); | |
| 36 | }); | |
| 37 | ||
| 38 | test("opened once the link is spent but no one is signed in", () => { | |
| 39 | const s = stage({ link: { state: "used", revokedAt: null, usedAt: ago(2 * DAY) } }).stage!; | |
| 40 | expect([s.key, s.at]).toEqual(["opened", ago(2 * DAY)]); | |
| 41 | }); | |
| 42 | ||
| 43 | test("signed in when claimed, at the time the link was spent; no time invented without it", () => { | |
| 44 | const s = stage({ claimed: true, link: { state: "used", revokedAt: null, usedAt: ago(DAY / 2) } }).stage!; | |
| 45 | expect([s.key, s.words, s.at]).toEqual(["signedin", "Signed in", ago(DAY / 2)]); | |
| 46 | const bare = stage({ claimed: true, lastActivityAt: ago(60_000), link: { state: "used", revokedAt: null, usedAt: null } }).stage!; | |
| 47 | expect([bare.key, bare.at]).toEqual(["signedin", null]); | |
| 48 | }); | |
| 49 | ||
| 50 | test("last tell once the child's heartbeat carries a tell", () => { | |
| 51 | const s = stage({ claimed: true, nextStep: { key: "going", words: "", lastTellAt: ago(3600_000) } }).stage!; | |
| 52 | expect([s.key, s.words, s.at]).toEqual(["tell", "Last tell", ago(3600_000)]); | |
| 53 | }); | |
| 54 | ||
| 55 | test("no state while it is made or when the child did not answer", () => { | |
| 56 | expect(stage({ made: false }).stage).toBeNull(); | |
| 57 | expect(stage({ claimed: null }).stage).toBeNull(); | |
| 58 | }); | |
| 59 | ||
| 60 | test("unopened for three days is marked, and only then", () => { | |
| 61 | expect(stageOf(base({ sentAt: ago(2 * DAY) }), NOW)!.resend).toBe(false); | |
| 62 | const s = stageOf(base({ sentAt: ago(4 * DAY) }), NOW)!; | |
| 63 | expect([s.key, s.words, s.resend]).toEqual(["sent", STALE_WORDS, true]); | |
| 64 | expect(stageOf(base({ sentAt: ago(4 * DAY), claimed: true }), NOW)!.resend).toBe(false); | |
| 65 | }); | |
| 66 | ||
| 67 | test("the page says the same words and offers resend only when stale", () => { | |
| 68 | const fresh = String(forgeRow(stage(), NOW)); | |
| 69 | expect(fresh).toContain(`>${STAGE_WORDS.sent} `); | |
| 70 | expect(fresh).not.toContain("Resend"); | |
| 71 | const stale = String(forgeRow(stage({ sentAt: ago(4 * DAY) }), NOW)); | |
| 72 | expect(stale).toContain(STALE_WORDS); | |
| 73 | expect(stale).toContain("Resend"); | |
| 74 | expect(stale).toContain("/forges/acme/claim-link"); | |
| 75 | }); | |
| 76 | ||
| 77 | test("the terminal line carries the same words, and resend", () => { | |
| 78 | const fresh = stage(); | |
| 79 | expect(forgeLines([fresh])[0]![1]).toContain(` ${fresh.stage!.words} `); | |
| 80 | const stale = stage({ sentAt: ago(4 * DAY) }); | |
| 81 | expect(stale.stage!.resend).toBe(true); | |
| 82 | expect(forgeLines([stale])[0]![1]).toContain(STALE_WORDS); | |
| 83 | }); | |
| 84 | ||
| 85 | test("the forges tool answers with the page's words and resend", async () => { | |
| 86 | const dir = mkdtempSync(join(tmpdir(), "forge-stage-")); | |
| 87 | const app = createApp({ dataDir: dir, url: "https://forge.test", port: 0 }); | |
| 88 | try { | |
| 89 | mkdirSync(join(dir, "spawn"), { recursive: true }); | |
| 90 | writeFileSync( | |
| 91 | join(dir, "spawn", "kid.json"), | |
| 92 | JSON.stringify({ name: "kid", bodyId: "cell-k", done: true, placement: "hosted", size: "small", apps: [], createdAt: new Date(Date.now() - 4 * DAY).toISOString() }), | |
| 93 | ); | |
| 94 | useChildFetch(app, async () => Response.json({ claimed: false, link: { state: "open", revokedAt: null, usedAt: null } })); | |
| 95 | const [row] = await clientRows(app); | |
| 96 | const out = (await tool.run({ app } as never, { isAdmin: true } as never, {})) as { ok: true; value: any }; | |
| 97 | const got = (out.value.forges ?? out.value.value?.forges ?? JSON.parse(JSON.stringify(out)).forges ?? [])[0]; | |
| 98 | expect(got.stage).toEqual({ ...row!.stage!, at: got.stage.at }); | |
| 99 | expect(got.stage.words).toBe(STALE_WORDS); | |
| 100 | expect(got.stage.resend).toBe(true); | |
| 101 | expect(String(forgeRow(row!, Date.now()))).toContain(got.stage.words); | |
| 102 | expect(tool.doc).toContain("reachable"); | |
| 103 | expect(tool.description).toContain("resend"); | |
| 104 | } finally { | |
| 105 | app.close(); | |
| 106 | } | |
| 107 | }); | |
| 108 | }); |
test/disk-mount.test.ts +1
| @@ -166,6 +166,7 @@ describe("the child's line", () => { | ||
| 166 | 166 | url: "https://kid--forge.test", |
| 167 | 167 | for: null, |
| 168 | 168 | apps: [], |
| 169 | stage: null, | |
| 169 | 170 | sentAt: null, |
| 170 | 171 | made: true, |
| 171 | 172 | claimed: true, |
test/forges-attention.test.ts +2 −1
| @@ -10,11 +10,12 @@ import { ATTENTION_WORDS, orderRows, type ClientRow } from "../src/spawn/clients | ||
| 10 | 10 | const NOW = Date.parse("2026-10-25T12:00:00Z"); |
| 11 | 11 | const ago = (h: number) => new Date(NOW - h * 3_600_000).toISOString(); |
| 12 | 12 | |
| 13 | const row = (name: string, o: Partial<ClientRow> = {}): Omit<ClientRow, "attention"> => ({ | |
| 13 | const row = (name: string, o: Partial<ClientRow> = {}): Omit<ClientRow, "attention" | "stage"> => ({ | |
| 14 | 14 | name, |
| 15 | 15 | url: `https://${name}.test`, |
| 16 | 16 | for: null, |
| 17 | 17 | apps: [], |
| 18 | stage: null, | |
| 18 | 19 | sentAt: ago(1), |
| 19 | 20 | made: true, |
| 20 | 21 | claimed: true, |
test/security-floor-doors.test.ts +1 −1
| @@ -92,7 +92,7 @@ test("each return of the public door and of `answered` is floored", () => { | ||
| 92 | 92 | // `response` is only ever what `answered` gave back. |
| 93 | 93 | for (const m of region.matchAll(/\bresponse\s*=\s*([^\n;]+)/g)) expect(m[1]!.trim()).toMatch(/^await answered\(/); |
| 94 | 94 | // `answered` floors what the work returns, and its failure path floors too. |
| 95 | expect(region).toContain("const done = await compress(request, response).then(securityFloor)"); | |
| 95 | expect(region).toMatch(/const done = await compress\(request, response\)\.then\(\(r\) => securityFloor\(/); | |
| 96 | 96 | expect(server).toMatch(/function failure\([^]*?\n}\n/); |
| 97 | 97 | const failure = server.match(/function failure\([^]*?\n}\n/)![0]; |
| 98 | 98 | for (const r of failure.matchAll(/\breturn\s+([^\n]+)/g)) expect(r[1]!.trim()).toMatch(/^securityFloor\(/); |
test/tui-forges.test.ts +1
| @@ -20,6 +20,7 @@ const row = (over: Partial<ClientRow>): ClientRow => ({ | ||
| 20 | 20 | url: "https://maria.test", |
| 21 | 21 | for: "Maria's bakery", |
| 22 | 22 | apps: [], |
| 23 | stage: null, | |
| 23 | 24 | sentAt: null, |
| 24 | 25 | made: true, |
| 25 | 26 | claimed: false, |
Principle 1/dead code: the
r.stage == nullfallback keeps the old sent-time branch alive; make stage required and render only the stage.