Public: anyone can read this; the crew can change it.
A hosted child forge has no crew runner: its issues are never picked up #8027
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4416-a-hosted-child-forge-has-no-crew-runner → main 1 commit fork
4 files
changed +76
−0
src/spawn/host.ts +2
| @@ -11,6 +11,7 @@ import { appsRuntime, type AppsRuntime } from "../apps/runtime.ts"; | ||
| 11 | 11 | import { resolveAppHost, type AppTarget } from "../apps/proxy.ts"; |
| 12 | 12 | import { childAppsSummary, startChildApps, stopChildApps } from "./child-apps.ts"; |
| 13 | 13 | import { childHost } from "./door.ts"; |
| 14 | import { markHostedChild } from "./hosted-mark.ts"; | |
| 14 | 15 | import { firstBoot } from "./first-boot.ts"; |
| 15 | 16 | import { |
| 16 | 17 | childHostSuffix, |
| @@ -96,6 +97,7 @@ async function openChild(parent: App, ledger: ChildLedger): Promise<App> { | ||
| 96 | 97 | ? { clientIpHeader: parent.config.clientIpHeader } |
| 97 | 98 | : {}), |
| 98 | 99 | }); |
| 100 | markHostedChild(child); | |
| 99 | 101 | await firstBoot(child); |
| 100 | 102 | // Its apps run in this process's lanes, awake before the first visit. |
| 101 | 103 | await startChildApps(parent, child, ledger.name); |
src/spawn/hosted-mark.ts added +15
| @@ -0,0 +1,15 @@ | ||
| 1 | /** | |
| 2 | * Which Apps are hosted children: a second `App` in the parent's process. | |
| 3 | * `main.ts` starts a crew for the parent only, so a hosted child has none; | |
| 4 | * the crew pages ask this to say so instead of promising a runner. | |
| 5 | */ | |
| 6 | ||
| 7 | import type { App } from "../app.ts"; | |
| 8 | ||
| 9 | const hosted = new WeakSet<App>(); | |
| 10 | ||
| 11 | export function markHostedChild(child: App): void { | |
| 12 | hosted.add(child); | |
| 13 | } | |
| 14 | ||
| 15 | export const isHostedChild = (app: App): boolean => hosted.has(app); |
src/web/pages/crew.ts +10
| @@ -41,6 +41,7 @@ import { wastedDays, type WastedDay } from "../../core/wasted_days.ts"; | ||
| 41 | 41 | import { modelDays, type ModelDay } from "../../core/model_days.ts"; |
| 42 | 42 | import { notePhase, takeSessionTime } from "../../core/route_timing.ts"; |
| 43 | 43 | import { listTeams, type Team } from "../../core/teams.ts"; |
| 44 | import { isHostedChild } from "../../spawn/hosted-mark.ts"; | |
| 44 | 45 | import { crewFor } from "../../crew/dispatch.ts"; |
| 45 | 46 | import { crewHandover, type CrewHandover } from "../../crew/handed.ts"; |
| 46 | 47 | import { MODELS } from "../../crew/models.ts"; |
| @@ -105,6 +106,15 @@ function shell(ctx: Ctx, title: string, body: Html, status = 200): Response { | ||
| 105 | 106 | */ |
| 106 | 107 | export function crewReadiness(ctx: Ctx): Html { |
| 107 | 108 | const crew = crewFor(ctx.app); |
| 109 | if (crew === undefined && isHostedChild(ctx.app)) | |
| 110 | return card( | |
| 111 | html`<p> | |
| 112 | ${icon("alert")} This forge is hosted inside another forge's process, | |
| 113 | which has no crew runner for it: assigned issues will wait. The crew | |
| 114 | runs once this forge has a process of its own. | |
| 115 | </p>`, | |
| 116 | { tone: "danger", extra: "mb" }, | |
| 117 | ); | |
| 108 | 118 | if (crew === undefined) |
| 109 | 119 | return card( |
| 110 | 120 | html`<p> |
test/hosted-child-crew.test.ts added +49
| @@ -0,0 +1,49 @@ | ||
| 1 | /** | |
| 2 | * A hosted child has no crew runner (#4416): only `main.ts` starts one, for | |
| 3 | * the parent. The crew readiness says so, for the child and only the child. | |
| 4 | */ | |
| 5 | import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 6 | import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; | |
| 7 | import { tmpdir } from "node:os"; | |
| 8 | import { join } from "node:path"; | |
| 9 | import { childDirOf } from "../src/actions/spawn.ts"; | |
| 10 | import { createApp, type App } from "../src/app.ts"; | |
| 11 | import type { Ctx } from "../src/http/context.ts"; | |
| 12 | import { closeHostedChildren, resolveHostedChild } from "../src/spawn/host.ts"; | |
| 13 | import { crewReadiness } from "../src/web/pages/crew.ts"; | |
| 14 | import { quietLogs } from "./helpers/globals.ts"; | |
| 15 | ||
| 16 | quietLogs(); | |
| 17 | ||
| 18 | let dir = ""; | |
| 19 | let parent: App; | |
| 20 | ||
| 21 | beforeAll(async () => { | |
| 22 | dir = await mkdtemp(join(tmpdir(), "forge-hosted-crew-")); | |
| 23 | parent = createApp({ dataDir: dir, url: "https://forge.example" }); | |
| 24 | await mkdir(join(dir, "spawn"), { recursive: true }); | |
| 25 | await writeFile( | |
| 26 | join(dir, "spawn", "kid.json"), | |
| 27 | JSON.stringify({ name: "kid", bodyId: "b1", done: true, placement: "hosted" }), | |
| 28 | ); | |
| 29 | }); | |
| 30 | ||
| 31 | afterAll(async () => { | |
| 32 | closeHostedChildren(parent); | |
| 33 | parent.close(); | |
| 34 | await rm(dir, { recursive: true, force: true }); | |
| 35 | }); | |
| 36 | ||
| 37 | const say = (app: App): string => String(crewReadiness({ app } as unknown as Ctx)); | |
| 38 | ||
| 39 | test("a hosted child says its crew cannot run, the parent does not", async () => { | |
| 40 | const child = await resolveHostedChild( | |
| 41 | parent, | |
| 42 | new Request("http://x/", { headers: { host: "kid--forge.example" } }), | |
| 43 | ); | |
| 44 | expect(typeof child).toBe("object"); | |
| 45 | expect(childDirOf(parent, "b1")).toContain("b1"); | |
| 46 | expect(say(child as App)).toContain("hosted inside another forge"); | |
| 47 | expect(say(parent)).not.toContain("hosted inside another forge"); | |
| 48 | expect(say(parent)).toContain("No crew runner is up"); | |
| 49 | }); |
new above
offline