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

The three real-cell spawn test files run under the test budget #12453

Merged opened this

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

Merged

scout/forge:scout/12396-the-three-real-cell-spawn-test-files-run → main 4 commits fork

  • Merged. scout/12396-the-three-real-cell-spawn-test-files-run was merged into main as 28f939d.

Review

Sign in to review this change.
11 files changed +176 −108
scripts/gate.ts −3
@@ -160,10 +160,7 @@ export const SLOW: readonly Exclusion[] = [
160160 { file: "test/signup-fresh.test.ts", reason: "boots main.ts on an empty data directory as a real process; 3.1 s a test on the gate box", issue: 1242 },
161161 { file: "test/signup-gate-parent.test.ts", reason: "walks every caller of createUser in the source tree; 2.3 s a test on the gate box", issue: 1242 },
162162 { file: "test/snapshot-uplink.test.ts", reason: "uploads a 400 MB snapshot in parts through a throttled uplink; 4.5 s a test on the gate box", issue: 1242 },
163 { file: "test/spawn-cell-e2e.test.ts", reason: "spawns a child forge into a real cell and serves its app through the forward; 25.4 s a test on the gate box", issue: 1242 },
164 { file: "test/spawn-cell.test.ts", reason: "boots a real child forge in its own cell and waits for its first request; 21.7 s a test on the gate box", issue: 1242 },
165163 { file: "test/spawn-move-cell-e2e.test.ts", reason: "moves a running child forge into a real cell with its data, repository and app; 13.1 s a test on the gate box", issue: 1242 },
166 { file: "test/spawn-small-cell.test.ts", reason: "boots a child forge in a real cell and restarts it to check its limits; 33.3 s a test on the gate box", issue: 1242 },
167164 { file: "test/ssh-port-taken.test.ts", reason: "boots main.ts as a real process with its SSH port already held; 14.2 s a test on the gate box", issue: 1242 },
168165 { file: "test/standby-restore.test.ts", reason: "boots a restored snapshot read-only as a real process; 3.1 s a test on the gate box", issue: 1242 },
169166 { file: "test/subscriptions.test.ts", reason: "runs stand-in agents on granted accounts, waiting for a refusal and a handover; 10.4 s a test on the gate box", issue: 1242 },
src/actions/fork_limit.ts added +14
@@ -0,0 +1,14 @@
1 /**
2 * How many forks one person may make in any 24 hours (#853, #888): each may
3 * become an app that deploys at once. Counted from the ledger of forks made,
4 * so deleting one makes no room. The crew's agents fork per issue to work —
5 * bounded instead by the crew's own concurrency and spend ceilings, since an
6 * agent forks only for a run — and the forge's own admin answers for its
7 * forge: neither is held to it.
8 *
9 * A leaf of its own, importing nothing: `make_it_yours.ts` reads it while
10 * its module is evaluated, and `repos.ts` reaches `make_it_yours.ts` back
11 * through its imports, so the constant cannot live in `repos.ts` without a
12 * "before initialization" error for whoever loads that cycle first.
13 */
14 export const FORKS_PER_DAY = 5;
src/actions/make_it_yours.ts +2 −1
@@ -46,7 +46,8 @@ import { requireAbility } from "./roles.ts";
4646 import { requireAppRoom, appLimitWords } from "./app_limit.ts";
4747 import { disableApp, enableApp } from "./apps.ts";
4848 import { forkDrift } from "./forks.ts";
49 import { deleteRepo, FORKS_PER_DAY, forkRepo, repositoryLimitMessage } from "./repos.ts";
49 import { deleteRepo, forkRepo, repositoryLimitMessage } from "./repos.ts";
50 import { FORKS_PER_DAY } from "./fork_limit.ts";
5051
5152 export interface MakeItYoursPlan {
5253 readonly dryRun: boolean;
src/actions/repos.ts +2 −9
@@ -79,6 +79,7 @@ import { validPolishShare } from "../crew/polish_share.ts";
7979 import { removeInstallCache } from "../crew/workdir.ts";
8080 import type { SettingsChanged } from "../core/settings_changed.ts";
8181 import { seatStarterCrew } from "../spawn/starter_crew.ts";
82 import { FORKS_PER_DAY } from "./fork_limit.ts";
8283
8384 export interface CreateRepoInput {
8485 /** Owner name: the actor themselves (default) or an org they can write to. */
@@ -197,15 +198,7 @@ const doorPayload = (actorId: string): Record<string, string> => {
197198 return door === undefined ? {} : { door };
198199 };
199200
200 /**
201 * How many forks one person may make in any 24 hours (#853, #888): each may
202 * become an app that deploys at once. Counted from the ledger of forks made,
203 * so deleting one makes no room. The crew's agents fork per issue to work —
204 * bounded instead by the crew's own concurrency and spend ceilings, since an
205 * agent forks only for a run — and the forge's own admin answers for its
206 * forge: neither is held to it.
207 */
208 export const FORKS_PER_DAY = 5;
201 export { FORKS_PER_DAY };
209202
210203 export async function forkRepo(
211204 app: App,
test/fork-limit-cycle.test.ts added +27
@@ -0,0 +1,27 @@
1 /**
2 * `repos.ts` reaches `make_it_yours.ts` back through its imports, and
3 * `make_it_yours.ts` reads the fork limit while it is evaluated. A process
4 * that loads `repos.ts` first (the real-cell helpers do) died with "Cannot
5 * access 'FORKS_PER_DAY' before initialization"; the limit lives in a leaf
6 * module now. Tried in a fresh process, since this one may have loaded both
7 * already; the process is the file's time, in its hook.
8 */
9 import { beforeAll, expect, test } from "bun:test";
10 import { join } from "node:path";
11
12 const root = join(import.meta.dir, "..");
13 let seen: { code: number; out: string; err: string } = { code: -1, out: "", err: "not run" };
14
15 beforeAll(async () => {
16 const script = `const a = await import(${JSON.stringify(join(root, "src/actions/repos.ts"))});
17 const b = await import(${JSON.stringify(join(root, "src/actions/make_it_yours.ts"))});
18 console.log(JSON.stringify([a.FORKS_PER_DAY, b.COPIES_PER_DAY]));`;
19 const proc = Bun.spawn([process.execPath, "-e", script], { cwd: root, stdout: "pipe", stderr: "pipe" });
20 const [out, err, code] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]);
21 seen = { code, out, err: err.slice(-800) };
22 }, 20_000);
23
24 test("a process that loads repos.ts first evaluates the fork limit on both sides", () => {
25 expect({ code: seen.code, err: seen.code === 0 ? "" : seen.err }).toEqual({ code: 0, err: "" });
26 expect(JSON.parse(seen.out.trim().split("\n").at(-1) ?? "null")).toEqual([5, 5]);
27 });
test/helpers/machine-note.ts +28
@@ -6,6 +6,7 @@
66 */
77 import { statfsSync } from "node:fs";
88 import { availableParallelism, loadavg, tmpdir } from "node:os";
9 import { resultLine } from "./result-line.ts";
910
1011 export function machineNote(dir: string = tmpdir()): string {
1112 let disk = "disk unknown";
@@ -23,3 +24,30 @@ export function machineNote(dir: string = tmpdir()): string {
2324 export function failedWith(what: string, seen: unknown, errText: string): string {
2425 return `${what}: ${JSON.stringify(seen)}\n${machineNote()}\nstderr tail: ${errText.slice(-1500)}`;
2526 }
27
28 /**
29 * What a real-cell helper process gave: its JSON result, the host's reason
30 * to skip, or the whole evidence of a failure.
31 */
32 export type HelperRun = { seen: Record<string, unknown>; out: string; errText: string } | { skipped: string } | { error: string };
33
34 /**
35 * Run a real-cell helper once, without FORGE_CELLS (a check turns cells off
36 * for the forges its tests boot), and read its one result line. A file calls
37 * this from its `beforeAll`, so the boot is the file's time and each test
38 * reads what the one child showed (#12396).
39 */
40 export async function runHelper(argv: string[], what: string, timeoutMs: number): Promise<HelperRun> {
41 const env: Record<string, string> = {};
42 for (const [k, v] of Object.entries(process.env)) if (v !== undefined && k !== "FORGE_CELLS") env[k] = v;
43 const proc = Bun.spawn([process.execPath, ...argv], { env, stdout: "pipe", stderr: "pipe", timeout: timeoutMs });
44 const [out, errText] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]);
45 let seen: Record<string, unknown>;
46 try {
47 seen = JSON.parse(resultLine(out)) as Record<string, unknown>;
48 } catch {
49 return { error: failedWith(`${what} printed no result`, out.slice(-800), errText) };
50 }
51 if (typeof seen["skipped"] === "string") return { skipped: seen["skipped"] };
52 return { seen, out, errText };
53 }
test/helpers/real-child-forge.ts +23 −5
@@ -6,7 +6,11 @@
66 *
77 * The parent holds a finished `cell` ledger for `kid`. The first request
88 * is answered before the cell is up; then the cell is awaited and `/` and
9 * `/healthz` are carried to it. Printed: one JSON line of what was seen.
9 * `/healthz` are carried to it. The child's parent record names an
10 * upstream nobody can reach, as a preview's cell has, so its first
11 * heartbeat arriving proves the cell relays the parent's beat door (the
12 * one child serves both proofs, #12396). Printed: one JSON line of what
13 * was seen.
1014 */
1115 import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
1216 import { tmpdir } from "node:os";
@@ -15,6 +19,7 @@ import { createApp } from "../../src/app.ts";
1519 import { cellCounters, cellsAvailable } from "../../src/apps/contain.ts";
1620 import { cellChildren, stopCellChildren } from "../../src/spawn/cell.ts";
1721 import { answerFromCell } from "../../src/spawn/host.ts";
22 import { lastHeard } from "../../src/spawn/heartbeat.ts";
1823 import "./roomy-disk.ts";
1924
2025 const cells = await cellsAvailable();
@@ -27,7 +32,10 @@ const dataDir = mkdtempSync(join(tmpdir(), "real-child-"));
2732 const parent = createApp({ dataDir, url: "https://f.test" });
2833 const ledger = { name: "kid", bodyId: "cell-real", size: "small" } as const;
2934 mkdirSync(join(dataDir, "spawn"), { recursive: true });
30 writeFileSync(join(dataDir, "spawn", "kid.json"), JSON.stringify({ ...ledger, done: true, placement: "cell" }));
35 writeFileSync(join(dataDir, "spawn", "kid.json"), JSON.stringify({ ...ledger, done: true, placement: "cell", beatKey: "beat-key" }));
36 const childDir = join(dataDir, "children", ledger.bodyId);
37 mkdirSync(childDir, { recursive: true });
38 writeFileSync(join(childDir, "parent.json"), JSON.stringify({ name: "kid", upstream: "https://unreachable.invalid", beatKey: "beat-key" }));
3139 const ask = (path: string) => answerFromCell(parent, new Request(`https://kid--f.test${path}`, { headers: { host: "kid--f.test" } }));
3240
3341 try {
@@ -38,8 +46,17 @@ try {
3846 const child = await children.start(ledger);
3947 const root = child === undefined ? null : (await ask("/")).status;
4048 const health = child === undefined ? null : (await ask("/healthz")).status;
41 // The watch reads the cell's tree once a second, by its label.
42 await Bun.sleep(1_500);
49 // The watch reads the cell's tree once a second, by its label; the child
50 // beats once as it boots. Both are asked until seen, not waited out.
51 const until = Date.now() + 10_000;
52 let labelled = false;
53 let heard = child === undefined ? undefined : await lastHeard(parent, "kid");
54 while (child !== undefined && Date.now() < until) {
55 labelled ||= cellCounters().some((c) => c.label === "forge:kid");
56 heard ??= await lastHeard(parent, "kid");
57 if (labelled && heard !== undefined) break;
58 await Bun.sleep(100);
59 }
4360 console.log(
4461 JSON.stringify({
4562 first: first.status,
@@ -48,7 +65,8 @@ try {
4865 limits: child?.limits ?? null,
4966 root,
5067 health,
51 labelled: cellCounters().some((c) => c.label === "forge:kid"),
68 labelled,
69 beat: heard?.beat.name ?? null,
5270 }),
5371 );
5472 } finally {
test/helpers/real-spawn-e2e.ts +2 −2
@@ -131,14 +131,14 @@ try {
131131 }
132132 return last;
133133 }
134 const first = await pollApp(POLL_MS, recover ? 100 : 1000);
134 const first = await pollApp(POLL_MS, 100);
135135 let limits = null;
136136 let after = null;
137137 if (recover) {
138138 const held = cellChildren(parent).list()[0];
139139 limits = held?.limits ?? null;
140140 if (held !== undefined) {
141 noteStop(parent, held.bodyId, "kid", "used 2374 MiB of memory, over its 2048 MiB limit", Date.now(), 1_000);
141 noteStop(parent, held.bodyId, "kid", "used 2374 MiB of memory, over its 2048 MiB limit", Date.now(), 100);
142142 killTree(held.pid);
143143 after = (await pollApp(RECOVER_MS, 100)).status;
144144 }
test/spawn-cell-e2e.test.ts +15 −19
@@ -3,35 +3,31 @@
33 * app, then `<app>--<child>--<parent>` through the real server answers with
44 * the app's own body. `spawn-cell-app-hosts.test.ts` fakes the cell; this
55 * does not. It runs where bwrap and socat are (as `spawn-cell.test.ts`'s
6 * real child does) and says it skipped where they are not.
6 * real child does) and says it skipped where they are not. The one child is
7 * spawned in the file's hook (#12396); the test reads what it showed.
78 */
8 import { test } from "bun:test";
9 import { beforeAll, test } from "bun:test";
910 import { join } from "node:path";
1011 import { portInsteadOfSocket } from "../src/git/hook-address.ts";
1112 import { missing } from "./helpers/tools.ts";
12 import { failedWith } from "./helpers/machine-note.ts";
13 import { resultLine } from "./helpers/result-line.ts";
13 import { failedWith, runHelper, type HelperRun } from "./helpers/machine-note.ts";
1414
1515 const cellTools = process.platform === "linux" && !portInsteadOfSocket() && !missing("bwrap", "socat");
16 let run: HelperRun = { skipped: "no bwrap and socat here" };
1617
17 test.skipIf(!cellTools)("a child forge spawned into a cell serves its app at <app>--<child>--<parent>", async () => {
18 const env: Record<string, string> = {};
19 for (const [k, v] of Object.entries(process.env)) if (v !== undefined && k !== "FORGE_CELLS") env[k] = v;
20 const proc = Bun.spawn([process.execPath, join(import.meta.dir, "helpers", "real-spawn-e2e.ts")], { env, stdout: "pipe", stderr: "pipe", timeout: 290_000 });
21 const [out, errText] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]);
22 const line = resultLine(out);
23 let seen: Record<string, unknown>;
24 try {
25 seen = JSON.parse(line) as Record<string, unknown>;
26 } catch {
27 throw new Error(failedWith("the spawn run printed no result", out.slice(-800), errText));
28 }
29 if (typeof seen["skipped"] === "string") {
30 console.log(`SKIPPING: this host refuses a cell inside the test (${seen["skipped"]}), so no child was spawned into one`);
18 beforeAll(async () => {
19 if (cellTools) run = await runHelper([join(import.meta.dir, "helpers", "real-spawn-e2e.ts")], "the spawn run", 290_000);
20 }, 300_000);
21
22 test.skipIf(!cellTools)("a child forge spawned into a cell serves its app at <app>--<child>--<parent>", () => {
23 if ("error" in run) throw new Error(run.error);
24 if ("skipped" in run) {
25 console.log(`SKIPPING: this host refuses a cell inside the test (${run.skipped}), so no child was spawned into one`);
3126 return;
3227 }
28 const { seen, out, errText } = run;
3329 // The child's own reason (its parent's "did not answer" line) and the
3430 // machine's disk and load are in the message, not a bare mismatch.
3531 const ok = seen["placement"] === "cell" && seen["status"] === 200 && seen["app"] === "bakery" && seen["body"] === "bakery says hi";
3632 if (!ok) throw new Error(failedWith("the child forge's app did not answer through the cell", { seen, out: out.slice(-1500) }, errText));
37 }, 310_000);
33 });
test/spawn-cell.test.ts +32 −46
@@ -1,4 +1,4 @@
1 import { afterAll, describe, expect, test } from "bun:test";
1 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
22 import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
33 import { tmpdir } from "node:os";
44 import { join } from "node:path";
@@ -10,7 +10,7 @@ import { createRun, queueDepth } from "../src/core/runs.ts";
1010 import { createApp } from "../src/app.ts";
1111 import { childEnv } from "../src/apps/process.ts";
1212 import { watchedLimits } from "../src/apps/contain.ts";
13 import { childLog, plainLine, cellChildren, crewAtWork, nextRunDue, type CellChild, stopCellChildren, stopGently, cellEnv, childEnvFor, cellLimits, cellPlacement, cellSpecFor, forward, launchSpecFor, ownsBody } from "../src/spawn/cell.ts";
13 import { childLog, plainLine, cellChildren, crewAtWork, nextRunDue, type CellChild, stopGently, cellEnv, childEnvFor, cellLimits, cellPlacement, cellSpecFor, forward, launchSpecFor, ownsBody } from "../src/spawn/cell.ts";
1414 import { fallbackPlacement, hostedPlacement, memoryBodies } from "../src/spawn/placement.ts";
1515 import type { SpawnPlan } from "../src/spawn/plan.ts";
1616 import { SIZES } from "../src/spawn/sizes.ts";
@@ -18,13 +18,12 @@ import { notePeer } from "../src/http/peer.ts";
1818 import type { App } from "../src/app.ts";
1919 import { portInsteadOfSocket } from "../src/git/hook-address.ts";
2020 import { missing } from "./helpers/tools.ts";
21 import { resultLine } from "./helpers/result-line.ts";
2221 import { hostAllowed } from "../src/apps/egress.ts";
2322 import { CAPACITY, childModelHosts, childPaymentHosts, childRegistryHosts,
2423 childOriginHosts, setCapacity } from "../src/core/capacity.ts";
2524 import { openBeatDoor, parentLocal } from "../src/spawn/beat-door.ts";
26 import { lastHeard, sendBeat } from "../src/spawn/heartbeat.ts";
27 import { failedWith } from "./helpers/machine-note.ts";
25 import { sendBeat } from "../src/spawn/heartbeat.ts";
26 import { failedWith, runHelper, type HelperRun } from "./helpers/machine-note.ts";
2827
2928 const plan = (name: string): SpawnPlan => ({ name, org: "o", apps: [], briefs: [], subscriptions: [], direction: "", keepAccess: false, budgetUsd: null });
3029 const tmp = mkdtempSync(join(tmpdir(), "spawn-cell-"));
@@ -568,55 +567,42 @@ describe("a child that will not start", () => {
568567 // must boot a child in a cell anyway (#6023). In its own process, without it.
569568 const cellTools = process.platform === "linux" && !portInsteadOfSocket() && !missing("bwrap", "socat");
570569
570 // One child for the file (#12396): booted once in the hook, so each test
571 // reads what it saw; the boot is the file's time, not a test's.
572 let childRun: HelperRun = { skipped: "no bwrap and socat here" };
573 function seenChild(): Record<string, unknown> | undefined {
574 if ("error" in childRun) throw new Error(childRun.error);
575 if ("skipped" in childRun) {
576 console.log(`SKIPPING: this host refuses a cell inside the test (${childRun.skipped}), so no child forge was booted in one`);
577 return undefined;
578 }
579 // The child's own reason and the machine's disk and load, not a bare mismatch.
580 const { seen, out, errText } = childRun;
581 if (seen["failure"] !== null || seen["health"] !== 200) throw new Error(failedWith("the child forge did not come up in its cell", { seen, out: out.slice(-1500) }, errText));
582 return seen;
583 }
584
571585 describe("a real child forge", () => {
586 beforeAll(async () => {
587 if (cellTools) childRun = await runHelper([join(import.meta.dir, "helpers", "real-child-forge.ts")], "the child forge run", 85_000);
588 }, 90_000);
589
572590 test.skipIf(!cellTools)("boots in its own forge:<name> cell and answers / through the forward; its first request is held until it is up", async () => {
573 const env: Record<string, string> = {};
574 for (const [k, v] of Object.entries(process.env)) if (v !== undefined && k !== "FORGE_CELLS") env[k] = v;
575 const proc = Bun.spawn([process.execPath, join(import.meta.dir, "helpers", "real-child-forge.ts")], { env, stdout: "pipe", stderr: "pipe" });
576 const [out, errText] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]);
577 const line = resultLine(out);
578 let seen: Record<string, unknown>;
579 try {
580 seen = JSON.parse(line) as Record<string, unknown>;
581 } catch {
582 throw new Error(failedWith("the child forge run printed no result", out.slice(-800), errText));
583 }
584 if (typeof seen["skipped"] === "string") {
585 console.log(`SKIPPING: this host refuses a cell inside the test (${seen["skipped"]}), so no child forge was booted in one`);
586 return;
587 }
588 // The child's own reason and the machine's disk and load, not a bare mismatch.
589 if (seen["failure"] !== null || seen["health"] !== 200) throw new Error(failedWith("the child forge did not come up in its cell", { seen, out: out.slice(-1500) }, errText));
591 const seen = seenChild();
592 if (seen === undefined) return;
590593 expect(seen["first"]).toBe(200);
591594 expect(seen["limits"]).toEqual({ memoryMiB: 2048, cpus: 1, diskMiB: 2048 });
592595 expect(seen["root"] as number).toBeLessThan(400);
593596 expect(seen["health"]).toBe(200);
594597 expect(seen["labelled"]).toBe(true);
595 }, 90_000);
598 });
596599
597 test.skipIf(!cells.ok)("its heartbeat reaches the parent through the cell, with no public address", async () => {
598 // The parent's public url is not reachable from here, as in a preview's cell.
599 const parent = createApp({ dataDir: join(tmp, "beat-parent"), url: "https://unreachable.invalid" });
600 try {
601 mkdirSync(join(parent.config.dataDir, "spawn"), { recursive: true });
602 writeFileSync(join(parent.config.dataDir, "spawn", "kid.json"), JSON.stringify({ name: "kid", bodyId: "cell-beat", done: true, beatKey: "beat-key" }));
603 const dir = join(parent.config.dataDir, "children", "cell-beat");
604 mkdirSync(dir, { recursive: true });
605 writeFileSync(join(dir, "parent.json"), JSON.stringify({ name: "kid", upstream: "https://unreachable.invalid", beatKey: "beat-key" }));
606 const children = cellChildren(parent);
607 const child = await children.start({ bodyId: "cell-beat", name: "kid", size: "small" });
608 if (child === undefined) throw new Error(`the child did not start: ${children.failure("cell-beat")}`);
609 let heard = await lastHeard(parent, "kid");
610 for (let i = 0; i < 100 && heard === undefined; i++) {
611 await Bun.sleep(100);
612 heard = await lastHeard(parent, "kid");
613 }
614 expect(heard?.beat.name).toBe("kid");
615 } finally {
616 await stopCellChildren(parent);
617 parent.close();
618 }
619 }, 60_000);
600 test.skipIf(!cellTools)("its heartbeat reaches the parent through the cell, with no public address", async () => {
601 // The child's parent record names an unreachable upstream, as in a preview's cell.
602 const seen = seenChild();
603 if (seen === undefined) return;
604 expect(seen["beat"]).toBe("kid");
605 });
620606 });
621607
622608 describe("a child whose cell will not come up", () => {
test/spawn-small-cell.test.ts +31 −23
@@ -8,37 +8,45 @@
88 * breach, and the app is a committed one-file app: building a first app,
99 * passing its tests and landing a second change are not covered here.
1010 *
11 * Runs where bwrap and socat are, else skips. Expected runtime: about a
12 * minute (spawn and boot, a restart after a 1 s backoff); the 3 minute
13 * ceiling is the worst case.
11 * Runs where bwrap and socat are, else skips. The one child is spawned, stopped
12 * and recovered in the file's hook (#12396), with the restart's backoff set
13 * to 100 ms through noteStop's seam; each test reads what it showed. The
14 * 4 minute ceiling is the worst case.
1415 */
15 import { test } from "bun:test";
16 import { beforeAll, expect, test } from "bun:test";
1617 import { join } from "node:path";
1718 import { portInsteadOfSocket } from "../src/git/hook-address.ts";
1819 import { missing } from "./helpers/tools.ts";
19 import { failedWith } from "./helpers/machine-note.ts";
20 import { resultLine } from "./helpers/result-line.ts";
20 import { failedWith, runHelper, type HelperRun } from "./helpers/machine-note.ts";
2121
2222 const cellTools = process.platform === "linux" && !portInsteadOfSocket() && !missing("bwrap", "socat");
23 let run: HelperRun = { skipped: "no bwrap and socat here" };
2324
24 test.skipIf(!cellTools)("a Small child in a real cell holds its limits and, once stopped, comes back with no 429", async () => {
25 const env: Record<string, string> = {};
26 for (const [k, v] of Object.entries(process.env)) if (v !== undefined && k !== "FORGE_CELLS") env[k] = v;
27 const proc = Bun.spawn([process.execPath, join(import.meta.dir, "helpers", "real-spawn-e2e.ts"), "recover"], { env, stdout: "pipe", stderr: "pipe", timeout: 230_000 });
28 const [out, errText] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]);
29 let seen: Record<string, unknown>;
30 try {
31 seen = JSON.parse(resultLine(out)) as Record<string, unknown>;
32 } catch {
33 throw new Error(failedWith("the recovery run printed no result", out.slice(-800), errText));
25 beforeAll(async () => {
26 if (cellTools) run = await runHelper([join(import.meta.dir, "helpers", "real-spawn-e2e.ts"), "recover"], "the recovery run", 230_000);
27 }, 240_000);
28
29 function seenRun(): { seen: Record<string, unknown>; errText: string } | undefined {
30 if ("error" in run) throw new Error(run.error);
31 if ("skipped" in run) {
32 console.log(`SKIPPING: this host refuses a cell inside the test (${run.skipped})`);
33 return undefined;
3434 }
35 if (typeof seen["skipped"] === "string") {
36 console.log(`SKIPPING: this host refuses a cell inside the test (${seen["skipped"]})`);
37 return;
38 }
39 const limits = seen["limits"] as { memoryMiB?: number; cpus?: number } | null;
35 return run;
36 }
37
38 test.skipIf(!cellTools)("a Small child's app answers through its cell, and its cell holds the Small limits", () => {
39 const r = seenRun();
40 if (r === undefined) return;
41 const { seen, errText } = r;
4042 if (seen["placement"] !== "cell" || seen["status"] !== 200 || seen["body"] !== "bakery says hi") throw new Error(failedWith("the Small child's app did not answer through the cell", { seen }, errText));
41 if (limits?.memoryMiB !== 2048 || limits.cpus !== 1) throw new Error(`the child did not hold the Small limits: ${JSON.stringify(limits)}`);
43 expect(seen["limits"]).toMatchObject({ memoryMiB: 2048, cpus: 1 });
44 });
45
46 test.skipIf(!cellTools)("a Small child stopped over its memory comes back by itself, and is never answered 429", () => {
47 const r = seenRun();
48 if (r === undefined) return;
49 const { seen, errText } = r;
4250 if ((seen["statuses"] as number[]).includes(429)) throw new Error(failedWith("a child was answered 429, the sentence for a full host", { seen }, errText));
4351 if (seen["after"] !== 200) throw new Error(failedWith("the stopped child did not come back", { seen }, errText));
44 }, 240_000);
52 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs