From 1c921cde3187a711c2c9545aa461f068323bdc55 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 13:54:51 +0000 Subject: [PATCH 1/5] test(cli): install-local suites run their independent boot chains at once The control boots of the jobs, handlers and boot-steps suites and the four uninstall orders each own their cwd and home, so they no longer wait for each other. Inside a chain every step still waits for the one before it. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01DcLzU9qErDBr5VydWEmHVM --- ...stall-local-boot-steps.integration.test.ts | 52 ++++-- ...install-local-handlers.integration.test.ts | 60 ++++--- ...age-install-local-jobs.integration.test.ts | 118 +++++++------ ...cal-uninstall-cleanups.integration.test.ts | 156 +++++++++++------- 4 files changed, 237 insertions(+), 149 deletions(-) diff --git a/packages/cli/test/package-install-local-boot-steps.integration.test.ts b/packages/cli/test/package-install-local-boot-steps.integration.test.ts index ea40dbdb8df..fb888174d9f 100644 --- a/packages/cli/test/package-install-local-boot-steps.integration.test.ts +++ b/packages/cli/test/package-install-local-boot-steps.integration.test.ts @@ -300,6 +300,17 @@ async function probe(live: LiveStart, token: string): Promise { return { permissionSet, created, updated, notes }; } +/** + * Run phases that share no state at once, and wait for EVERY one to settle + * before reporting the first failure: a phase that rejects must not leave a + * sibling still booting after `afterAll` has stopped the process groups. + */ +async function allPhases(chains: Array<() => Promise>): Promise { + const settled = await Promise.allSettled(chains.map((chain) => chain())); + const failed = settled.find((s): s is PromiseRejectedResult => s.status === 'rejected'); + if (failed) throw failed.reason; +} + const phases: Record<'install' | 'restart' | 'control', Phase | undefined> = { install: undefined, restart: undefined, control: undefined, }; @@ -319,24 +330,33 @@ beforeAll(async () => { writeFileSync(hostArtifact, JSON.stringify(HOST_ARTIFACT, null, 2), 'utf8'); const home = join(runtimeDir, 'home'); - // ── boot 1: the host, hot install, probe ─────────────────────────────── - const first = await bootStart(runtimeDir, home, ['--artifact', hostArtifact]); - const token = await authenticate(first); - installs.push(await packageInstall(appDir, first)); - phases.install = await probe(first, token); - await stopGroup(first.child); + // Boots 1 and 2 share one home and run in order. Boot 3, the CONTROL, has + // its own cwd and home and only reads the package file, so it runs beside + // them; every boot binds a kernel-assigned port. + await allPhases([ + // ── boot 1: the host, hot install, probe ─────────────────────────────── + async () => { + const first = await bootStart(runtimeDir, home, ['--artifact', hostArtifact]); + const token = await authenticate(first); + installs.push(await packageInstall(appDir, first)); + phases.install = await probe(first, token); + await stopGroup(first.child); - // ── boot 2: same host, home and cwd — the ledger rehydrates on kernel:ready ── - const second = await bootStart(runtimeDir, home, ['--artifact', hostArtifact]); - phases.restart = await probe(second, await authenticate(second)); - await stopGroup(second.child); + // ── boot 2: same host, home and cwd — the ledger rehydrates on kernel:ready ── + const second = await bootStart(runtimeDir, home, ['--artifact', hostArtifact]); + phases.restart = await probe(second, await authenticate(second)); + await stopGroup(second.child); + }, - // ── boot 3: the CONTROL — the same file as the boot artifact ─────────── - const controlDir = join(root, 'control'); - mkdirSync(controlDir, { recursive: true }); - const third = await bootStart(controlDir, join(controlDir, 'home'), ['--artifact', join(appDir, 'dist', 'objectstack.json')]); - phases.control = await probe(third, await authenticate(third)); - await stopGroup(third.child); + // ── boot 3: the CONTROL — the same file as the boot artifact ─────────── + async () => { + const controlDir = join(root, 'control'); + mkdirSync(controlDir, { recursive: true }); + const third = await bootStart(controlDir, join(controlDir, 'home'), ['--artifact', join(appDir, 'dist', 'objectstack.json')]); + phases.control = await probe(third, await authenticate(third)); + await stopGroup(third.child); + }, + ]); }, 4 * BOOT_TIMEOUT_MS); afterAll(async () => { diff --git a/packages/cli/test/package-install-local-handlers.integration.test.ts b/packages/cli/test/package-install-local-handlers.integration.test.ts index f172da69d64..891bd34ce16 100644 --- a/packages/cli/test/package-install-local-handlers.integration.test.ts +++ b/packages/cli/test/package-install-local-handlers.integration.test.ts @@ -310,6 +310,17 @@ async function probe(live: LiveStart, token: string): Promise { const rec = (a: Answer) => a.body?.data ?? a.body?.record ?? a.body; +/** + * Run phases that share no state at once, and wait for EVERY one to settle + * before reporting the first failure: a phase that rejects must not leave a + * sibling still booting after `afterAll` has stopped the process groups. + */ +async function allPhases(chains: Array<() => Promise>): Promise { + const settled = await Promise.allSettled(chains.map((chain) => chain())); + const failed = settled.find((s): s is PromiseRejectedResult => s.status === 'rejected'); + if (failed) throw failed.reason; +} + const phases: Record<'install' | 'reinstall' | 'restart' | 'control', Phase | undefined> = { install: undefined, reinstall: undefined, restart: undefined, control: undefined, }; @@ -326,26 +337,35 @@ beforeAll(async () => { mkdirSync(runtimeDir, { recursive: true }); const home = join(runtimeDir, 'home'); - // ── boot 1: empty `os start`, install, probe, reinstall, probe ───────── - const first = await bootStart(runtimeDir, home); - const token = await authenticate(first); - installs.push(await packageInstall(appDir, first)); - phases.install = await probe(first, token); - installs.push(await packageInstall(appDir, first)); - phases.reinstall = await probe(first, token); - await stopGroup(first.child); - - // ── boot 2: same home and cwd — the ledger rehydrates on kernel:ready ── - const second = await bootStart(runtimeDir, home); - phases.restart = await probe(second, await authenticate(second)); - await stopGroup(second.child); - - // ── boot 3: the CONTROL — the same file as the boot artifact ─────────── - const controlDir = join(root, 'control'); - mkdirSync(controlDir, { recursive: true }); - const third = await bootStart(controlDir, join(controlDir, 'home'), ['--artifact', join(appDir, 'dist', 'objectstack.json')]); - phases.control = await probe(third, await authenticate(third)); - await stopGroup(third.child); + // Boots 1 and 2 share one home and run in order. Boot 3, the CONTROL, has + // its own cwd and home and only reads the package file, so it runs beside + // them; every boot binds a kernel-assigned port. + await allPhases([ + // ── boot 1: empty `os start`, install, probe, reinstall, probe ───────── + async () => { + const first = await bootStart(runtimeDir, home); + const token = await authenticate(first); + installs.push(await packageInstall(appDir, first)); + phases.install = await probe(first, token); + installs.push(await packageInstall(appDir, first)); + phases.reinstall = await probe(first, token); + await stopGroup(first.child); + + // ── boot 2: same home and cwd — the ledger rehydrates on kernel:ready ── + const second = await bootStart(runtimeDir, home); + phases.restart = await probe(second, await authenticate(second)); + await stopGroup(second.child); + }, + + // ── boot 3: the CONTROL — the same file as the boot artifact ─────────── + async () => { + const controlDir = join(root, 'control'); + mkdirSync(controlDir, { recursive: true }); + const third = await bootStart(controlDir, join(controlDir, 'home'), ['--artifact', join(appDir, 'dist', 'objectstack.json')]); + phases.control = await probe(third, await authenticate(third)); + await stopGroup(third.child); + }, + ]); }, 4 * BOOT_TIMEOUT_MS); afterAll(async () => { diff --git a/packages/cli/test/package-install-local-jobs.integration.test.ts b/packages/cli/test/package-install-local-jobs.integration.test.ts index 3e3179f0b1e..aa8345d9b5e 100644 --- a/packages/cli/test/package-install-local-jobs.integration.test.ts +++ b/packages/cli/test/package-install-local-jobs.integration.test.ts @@ -358,6 +358,17 @@ async function quietAfter(live: LiveStart, token: string, object: string, job: s return { floor, after }; } +/** + * Run phases that share no state at once, and wait for EVERY one to settle + * before reporting the first failure: a phase that rejects must not leave a + * sibling still booting after `afterAll` has stopped the process groups. + */ +async function allPhases(chains: Array<() => Promise>): Promise { + const settled = await Promise.allSettled(chains.map((chain) => chain())); + const failed = settled.find((s): s is PromiseRejectedResult => s.status === 'rejected'); + if (failed) throw failed.reason; +} + interface InstallRun { exit: number | null; output: string } const readings: { bodyInstall?: InstallRun; @@ -411,55 +422,64 @@ beforeAll(async () => { writeFileSync(hostArtifact, JSON.stringify(HOST_ARTIFACT, null, 2), 'utf8'); const home = join(runtimeDir, 'home'); - // ── boot 1: the host, hot installs, the body job runs ───────────────── - const first = await bootStart(runtimeDir, home, ['--artifact', hostArtifact]); - const token = await authenticate(first); - readings.bodyInstall = await packageInstall(bodyApp, first); - readings.otherInstall = await packageInstall(otherApp, first); - readings.dropInstall = await packageInstall(dropAppV1, first); - readings.handlerInstall = await packageInstall(handlerApp, first); - readings.installed = await http(first, 'GET', '/api/v1/marketplace/install-local', token); - readings.afterInstall = await awaitRuns(first, token, HOST_TICK, BODY_JOB, 0); - - // Reinstall the drop package with a version that no longer declares DROP_GONE. - readings.dropBefore = await awaitRuns(first, token, HOST_TICK, DROP_GONE, 0); - readings.dropReinstall = await packageInstall(dropAppV2, first); - readings.dropGoneHot = await quietAfter(first, token, HOST_TICK, DROP_GONE); - readings.dropKeptHot = await awaitRuns(first, token, HOST_TICK, DROP_KEPT, - rowsOf(await jobRows(first, token, HOST_TICK, DROP_KEPT)).length); - - // Uninstall the body package; the other package is the control. - readings.uninstall = await http(first, 'DELETE', `/api/v1/marketplace/install-local/${BODY_APP_ID}`, token); - readings.bodyGoneHot = await quietAfter(first, token, HOST_TICK, BODY_JOB); - readings.otherHot = await awaitRuns(first, token, HOST_TICK, OTHER_JOB, - rowsOf(await jobRows(first, token, HOST_TICK, OTHER_JOB)).length); - readings.output.install = first.output(); - await stopGroup(first.child); - - // ── boot 2: same host, home and cwd — the ledger rehydrates on kernel:ready ── - const second = await bootStart(runtimeDir, home, ['--artifact', hostArtifact]); - const token2 = await authenticate(second); - // The rows boot 1 left behind are the floors: only a run in THIS boot lifts one. - const floorOf = async (object: string, job: string) => rowsOf(await jobRows(second, token2, object, job)).length; - const bodyFloor = await floorOf(HOST_TICK, BODY_JOB); - const goneFloor = await floorOf(HOST_TICK, DROP_GONE); - const restartedAt = Date.now(); - readings.afterRestart = await awaitRuns(second, token2, HOST_TICK, OTHER_JOB, await floorOf(HOST_TICK, OTHER_JOB)); - readings.dropKeptRestart = await awaitRuns(second, token2, HOST_TICK, DROP_KEPT, await floorOf(HOST_TICK, DROP_KEPT)); - await sleep(Math.max(0, QUIET_WAIT_MS - (Date.now() - restartedAt))); - readings.bodyGoneRestart = { floor: bodyFloor, after: await floorOf(HOST_TICK, BODY_JOB) }; - readings.dropGoneRestart = { floor: goneFloor, after: await floorOf(HOST_TICK, DROP_GONE) }; - readings.output.restart = second.output(); - await stopGroup(second.child); - - // ── boot 3: the CONTROL — both job forms in the boot artifact ───────── - const controlHome = join(controlApp, 'home'); - const third = await bootStart(controlApp, controlHome, ['--artifact', join(controlApp, 'dist', 'objectstack.json')]); - const token3 = await authenticate(third); - readings.controlHandler = await awaitRuns(third, token3, HANDLER_TICK, HANDLER_JOB, 0); - readings.controlBody = await awaitRuns(third, token3, BODY_TICK, BODY_JOB, 0); - readings.output.control = third.output(); - await stopGroup(third.child); + // Boots 1 and 2 share one home and run in order. Boot 3, the CONTROL, has + // its own cwd and home and reads nothing they write, so it runs beside + // them; every boot binds a kernel-assigned port. + await allPhases([ + // ── boot 1: the host, hot installs, the body job runs ───────────────── + async () => { + const first = await bootStart(runtimeDir, home, ['--artifact', hostArtifact]); + const token = await authenticate(first); + readings.bodyInstall = await packageInstall(bodyApp, first); + readings.otherInstall = await packageInstall(otherApp, first); + readings.dropInstall = await packageInstall(dropAppV1, first); + readings.handlerInstall = await packageInstall(handlerApp, first); + readings.installed = await http(first, 'GET', '/api/v1/marketplace/install-local', token); + readings.afterInstall = await awaitRuns(first, token, HOST_TICK, BODY_JOB, 0); + + // Reinstall the drop package with a version that no longer declares DROP_GONE. + readings.dropBefore = await awaitRuns(first, token, HOST_TICK, DROP_GONE, 0); + readings.dropReinstall = await packageInstall(dropAppV2, first); + readings.dropGoneHot = await quietAfter(first, token, HOST_TICK, DROP_GONE); + readings.dropKeptHot = await awaitRuns(first, token, HOST_TICK, DROP_KEPT, + rowsOf(await jobRows(first, token, HOST_TICK, DROP_KEPT)).length); + + // Uninstall the body package; the other package is the control. + readings.uninstall = await http(first, 'DELETE', `/api/v1/marketplace/install-local/${BODY_APP_ID}`, token); + readings.bodyGoneHot = await quietAfter(first, token, HOST_TICK, BODY_JOB); + readings.otherHot = await awaitRuns(first, token, HOST_TICK, OTHER_JOB, + rowsOf(await jobRows(first, token, HOST_TICK, OTHER_JOB)).length); + readings.output.install = first.output(); + await stopGroup(first.child); + + // ── boot 2: same host, home and cwd — the ledger rehydrates on kernel:ready ── + const second = await bootStart(runtimeDir, home, ['--artifact', hostArtifact]); + const token2 = await authenticate(second); + // The rows boot 1 left behind are the floors: only a run in THIS boot lifts one. + const floorOf = async (object: string, job: string) => rowsOf(await jobRows(second, token2, object, job)).length; + const bodyFloor = await floorOf(HOST_TICK, BODY_JOB); + const goneFloor = await floorOf(HOST_TICK, DROP_GONE); + const restartedAt = Date.now(); + readings.afterRestart = await awaitRuns(second, token2, HOST_TICK, OTHER_JOB, await floorOf(HOST_TICK, OTHER_JOB)); + readings.dropKeptRestart = await awaitRuns(second, token2, HOST_TICK, DROP_KEPT, await floorOf(HOST_TICK, DROP_KEPT)); + await sleep(Math.max(0, QUIET_WAIT_MS - (Date.now() - restartedAt))); + readings.bodyGoneRestart = { floor: bodyFloor, after: await floorOf(HOST_TICK, BODY_JOB) }; + readings.dropGoneRestart = { floor: goneFloor, after: await floorOf(HOST_TICK, DROP_GONE) }; + readings.output.restart = second.output(); + await stopGroup(second.child); + }, + + // ── boot 3: the CONTROL — both job forms in the boot artifact ───────── + async () => { + const controlHome = join(controlApp, 'home'); + const third = await bootStart(controlApp, controlHome, ['--artifact', join(controlApp, 'dist', 'objectstack.json')]); + const token3 = await authenticate(third); + readings.controlHandler = await awaitRuns(third, token3, HANDLER_TICK, HANDLER_JOB, 0); + readings.controlBody = await awaitRuns(third, token3, BODY_TICK, BODY_JOB, 0); + readings.output.control = third.output(); + await stopGroup(third.child); + }, + ]); }, 3 * BOOT_TIMEOUT_MS + 12 * RUN_WAIT_MS); afterAll(async () => { diff --git a/packages/cli/test/package-install-local-uninstall-cleanups.integration.test.ts b/packages/cli/test/package-install-local-uninstall-cleanups.integration.test.ts index 78db7ca644f..b00a5fb539d 100644 --- a/packages/cli/test/package-install-local-uninstall-cleanups.integration.test.ts +++ b/packages/cli/test/package-install-local-uninstall-cleanups.integration.test.ts @@ -58,7 +58,9 @@ * package — `@objectstack/cloud-connection` and `@objectstack/metadata-protocol` * included — resolved through its `exports` to `dist/`, so an ablation of either * package's source reaches this file only after that package is rebuilt. Every - * boot runs in `beforeAll`; the `it`s only read what the phases recorded. + * boot runs in `beforeAll`; the `it`s only read what the phases recorded. The + * four orders run at once, each in its own cwd and home: one order's steps + * run in sequence, and no order reads another's. */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; @@ -324,6 +326,17 @@ async function readAfterRestart(live: LiveStart, run: Run): Promise { run.object = await http(live, 'GET', `/api/v1/data/${TASK}`, token); } +/** + * Run phases that share no state at once, and wait for EVERY one to settle + * before reporting the first failure: a phase that rejects must not leave a + * sibling still booting after `afterAll` has stopped the process groups. + */ +async function allPhases(chains: Array<() => Promise>): Promise { + const settled = await Promise.allSettled(chains.map((chain) => chain())); + const failed = settled.find((s): s is PromiseRejectedResult => s.status === 'rejected'); + if (failed) throw failed.reason; +} + const runs: Record<'hot' | 'restarted' | 'reseed' | 'reinstall', Run> = { hot: {}, restarted: {}, reseed: {}, reinstall: {} }; beforeAll(async () => { @@ -336,69 +349,84 @@ beforeAll(async () => { mkdirSync(join(otherAppDir, 'dist'), { recursive: true }); writeFileSync(join(otherAppDir, 'dist', 'objectstack.json'), JSON.stringify(OTHER_ARTIFACT, null, 2), 'utf8'); - // ── order 1: hot install → DELETE → restart ──────────────────────────── - const hotDir = join(root, 'hot'); - mkdirSync(hotDir, { recursive: true }); - const hotHome = join(hotDir, 'home'); - const a = await bootStart(hotDir, hotHome); - const aSession = await authenticate(a); - runs.hot.install = await packageInstall(appDir, a); - await grantThenUninstall(a, aSession, runs.hot); - await stopGroup(a.child); - const b = await bootStart(hotDir, hotHome); - await readAfterRestart(b, runs.hot); - await stopGroup(b.child); - - // ── order 2: install → restart → DELETE → restart ────────────────────── - // The DELETE meets a package the ledger REHYDRATED, not one this process - // hot-installed — the path that existed before #21322's change. - const coldDir = join(root, 'cold'); - mkdirSync(coldDir, { recursive: true }); - const coldHome = join(coldDir, 'home'); - const c = await bootStart(coldDir, coldHome); - await authenticate(c); - runs.restarted.install = await packageInstall(appDir, c); - await stopGroup(c.child); - const d = await bootStart(coldDir, coldHome); - await grantThenUninstall(d, await authenticate(d), runs.restarted); - await stopGroup(d.child); - const e = await bootStart(coldDir, coldHome); - await readAfterRestart(e, runs.restarted); - await stopGroup(e.child); - - // ── order 3: hot install → DELETE → hot install of ANOTHER package → restart ── - // The second install announces `metadata:reloaded` into the process the - // DELETE ran in, so whatever that process still counts as registered is - // re-seeded — the withdrawal is what keeps the uninstalled package out of it. - const reseedDir = join(root, 'reseed'); - mkdirSync(reseedDir, { recursive: true }); - const reseedHome = join(reseedDir, 'home'); - const f = await bootStart(reseedDir, reseedHome); - const fSession = await authenticate(f); - runs.reseed.install = await packageInstall(appDir, f); - await grantThenUninstall(f, fSession, runs.reseed); - runs.reseed.otherInstall = await packageInstall(otherAppDir, f); - runs.reseed.afterOtherInstall = await readGrants(f, fSession.token); - await stopGroup(f.child); - const g = await bootStart(reseedDir, reseedHome); - await readAfterRestart(g, runs.reseed); - await stopGroup(g.child); - - // ── order 4: hot install → DELETE → hot reinstall of the SAME package ────── - // The withdrawal's control: whatever `uninstallPackage` took from the running - // kernel, the install path registers again, in the same process. - const reinstallDir = join(root, 'reinstall'); - mkdirSync(reinstallDir, { recursive: true }); - const h = await bootStart(reinstallDir, join(reinstallDir, 'home')); - const hSession = await authenticate(h); - runs.reinstall.install = await packageInstall(appDir, h); - await grantThenUninstall(h, hSession, runs.reinstall); - runs.reinstall.reinstall = await packageInstall(appDir, h); - runs.reinstall.afterReinstall = { - object: await http(h, 'GET', `/api/v1/data/${TASK}`, hSession.token), - ...await readGrants(h, hSession.token), - }; - await stopGroup(h.child); + // The four orders share nothing but the two package directories they read: + // each has its own cwd, home and ledger, and every boot binds a + // kernel-assigned port. So they run at once, and the file waits for the + // longest one rather than for their sum. Inside an order every step still + // waits for the one before it. + await allPhases([ + // ── order 1: hot install → DELETE → restart ──────────────────────────── + async () => { + const hotDir = join(root, 'hot'); + mkdirSync(hotDir, { recursive: true }); + const hotHome = join(hotDir, 'home'); + const a = await bootStart(hotDir, hotHome); + const aSession = await authenticate(a); + runs.hot.install = await packageInstall(appDir, a); + await grantThenUninstall(a, aSession, runs.hot); + await stopGroup(a.child); + const b = await bootStart(hotDir, hotHome); + await readAfterRestart(b, runs.hot); + await stopGroup(b.child); + }, + + // ── order 2: install → restart → DELETE → restart ────────────────────── + // The DELETE meets a package the ledger REHYDRATED, not one this process + // hot-installed — the path that existed before #21322's change. + async () => { + const coldDir = join(root, 'cold'); + mkdirSync(coldDir, { recursive: true }); + const coldHome = join(coldDir, 'home'); + const c = await bootStart(coldDir, coldHome); + await authenticate(c); + runs.restarted.install = await packageInstall(appDir, c); + await stopGroup(c.child); + const d = await bootStart(coldDir, coldHome); + await grantThenUninstall(d, await authenticate(d), runs.restarted); + await stopGroup(d.child); + const e = await bootStart(coldDir, coldHome); + await readAfterRestart(e, runs.restarted); + await stopGroup(e.child); + }, + + // ── order 3: hot install → DELETE → hot install of ANOTHER package → restart ── + // The second install announces `metadata:reloaded` into the process the + // DELETE ran in, so whatever that process still counts as registered is + // re-seeded — the withdrawal is what keeps the uninstalled package out of it. + async () => { + const reseedDir = join(root, 'reseed'); + mkdirSync(reseedDir, { recursive: true }); + const reseedHome = join(reseedDir, 'home'); + const f = await bootStart(reseedDir, reseedHome); + const fSession = await authenticate(f); + runs.reseed.install = await packageInstall(appDir, f); + await grantThenUninstall(f, fSession, runs.reseed); + runs.reseed.otherInstall = await packageInstall(otherAppDir, f); + runs.reseed.afterOtherInstall = await readGrants(f, fSession.token); + await stopGroup(f.child); + const g = await bootStart(reseedDir, reseedHome); + await readAfterRestart(g, runs.reseed); + await stopGroup(g.child); + }, + + // ── order 4: hot install → DELETE → hot reinstall of the SAME package ────── + // The withdrawal's control: whatever `uninstallPackage` took from the running + // kernel, the install path registers again, in the same process. + async () => { + const reinstallDir = join(root, 'reinstall'); + mkdirSync(reinstallDir, { recursive: true }); + const h = await bootStart(reinstallDir, join(reinstallDir, 'home')); + const hSession = await authenticate(h); + runs.reinstall.install = await packageInstall(appDir, h); + await grantThenUninstall(h, hSession, runs.reinstall); + runs.reinstall.reinstall = await packageInstall(appDir, h); + runs.reinstall.afterReinstall = { + object: await http(h, 'GET', `/api/v1/data/${TASK}`, hSession.token), + ...await readGrants(h, hSession.token), + }; + await stopGroup(h.child); + }, + ]); }, 9 * BOOT_TIMEOUT_MS); afterAll(async () => { From fd0075359fb6d2024d54f734067378268ed4d582 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 14:51:02 +0000 Subject: [PATCH 2/5] test(cli): the install-local suites declare their measured spawn budgets Each file boots `os start` from the tsx source entry two or three times per order of events (one boot to READY is about 20 s), so none can fit the 10 s line. Each declares its budget with the ruled class, from the CI reading where the shape is unchanged and from the local after-reading scaled by the file's own CI-over-local ratio where it changed. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01DcLzU9qErDBr5VydWEmHVM --- .../test/package-install-local-boot-steps.integration.test.ts | 1 + .../cli/test/package-install-local-handlers.integration.test.ts | 1 + .../cli/test/package-install-local-hooks.integration.test.ts | 1 + .../cli/test/package-install-local-jobs-pull.integration.test.ts | 1 + .../package-install-local-jobs-shared-name.integration.test.ts | 1 + packages/cli/test/package-install-local-jobs.integration.test.ts | 1 + .../package-install-local-uninstall-cleanups.integration.test.ts | 1 + 7 files changed, 7 insertions(+) diff --git a/packages/cli/test/package-install-local-boot-steps.integration.test.ts b/packages/cli/test/package-install-local-boot-steps.integration.test.ts index fb888174d9f..452b1d61f49 100644 --- a/packages/cli/test/package-install-local-boot-steps.integration.test.ts +++ b/packages/cli/test/package-install-local-boot-steps.integration.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 70 — spawns the real producer once per single-cause fixture: three `os start` boots of the tsx source entry (install, restart on the same home, and the `--artifact` control on its own home, run beside the other two) and one `os package install` run; slowest of three local runs after the control ran beside them 49.1 s, x 1.11 (this file's CI-over-local ratio before: 68.2 s in hourly full battery run 40290659753 over a 61.3 s local median) x 1.25, rounded up; no CI reading of this shape yet /** * #21322 — a hot install (`os package install ` into a RUNNING diff --git a/packages/cli/test/package-install-local-handlers.integration.test.ts b/packages/cli/test/package-install-local-handlers.integration.test.ts index 891bd34ce16..40c8258eb62 100644 --- a/packages/cli/test/package-install-local-handlers.integration.test.ts +++ b/packages/cli/test/package-install-local-handlers.integration.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 100 — spawns the real producer once per single-cause fixture: three `os start` boots of the tsx source entry (install and reinstall, restart on the same home, and the `--artifact` control on its own home, run beside the other two) and two `os package install` runs; slowest of three local runs after the control ran beside them 57.5 s, x 1.35 (this file's CI-over-local ratio before: 97.8 s in hourly full battery run 40290659753 over a 72.7 s local median) x 1.25, rounded up; no CI reading of this shape yet /** * #21321 — an app installed with `os package install ` runs its diff --git a/packages/cli/test/package-install-local-hooks.integration.test.ts b/packages/cli/test/package-install-local-hooks.integration.test.ts index b780f86ebb0..3c77fabef79 100644 --- a/packages/cli/test/package-install-local-hooks.integration.test.ts +++ b/packages/cli/test/package-install-local-hooks.integration.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 150 — spawns the real producer once per single-cause fixture: two `os start` boots of the tsx source entry on one home (the hot installs, then the rehydrate of an entry an earlier build wrote) and five `os package install` runs, one per package under test; slowest CI reading 114.4 s (hourly full battery run 40290659753, 2026-10-26) x 1.25, rounded up /** * #21585 — install-local refuses the code it cannot run: a hook with no `body`, diff --git a/packages/cli/test/package-install-local-jobs-pull.integration.test.ts b/packages/cli/test/package-install-local-jobs-pull.integration.test.ts index 2f99e61de3a..fa7920cb95e 100644 --- a/packages/cli/test/package-install-local-jobs-pull.integration.test.ts +++ b/packages/cli/test/package-install-local-jobs-pull.integration.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 140 — spawns the real producer once per single-cause fixture: two `os start` boots of the tsx source entry on one home (the hot installs, then the rehydrate of an entry an earlier build wrote) and four `os package install` runs, one per package under test; slowest CI reading 107.9 s (run 38335967790, the card's window; the job logs of run 40290659753 read here carry no line for this file) x 1.25, rounded up /** * install-local refuses an enabled job whose `pull` cannot bind, as it refuses diff --git a/packages/cli/test/package-install-local-jobs-shared-name.integration.test.ts b/packages/cli/test/package-install-local-jobs-shared-name.integration.test.ts index bc1e7d9b3ab..c46561fe018 100644 --- a/packages/cli/test/package-install-local-jobs-shared-name.integration.test.ts +++ b/packages/cli/test/package-install-local-jobs-shared-name.integration.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 140 — spawns the real producer once per single-cause fixture: two `os start` boots of the tsx source entry on one home (the hot installs and an uninstall, then the rehydrate and a second uninstall) and three `os package install` runs, one per package sharing the job name; slowest CI reading 107.1 s (hourly full battery run 40290659753, 2026-10-26) x 1.25, rounded up /** * #21602 — two install-local packages that declare the SAME job name both keep diff --git a/packages/cli/test/package-install-local-jobs.integration.test.ts b/packages/cli/test/package-install-local-jobs.integration.test.ts index aa8345d9b5e..2d3315b263f 100644 --- a/packages/cli/test/package-install-local-jobs.integration.test.ts +++ b/packages/cli/test/package-install-local-jobs.integration.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 180 — spawns the real producer once per single-cause fixture: three `os start` boots of the tsx source entry (install, restart on the same home, and the `--artifact` control on its own home, run beside the other two) and six `os package install` runs; slowest of three local runs after the control ran beside them 104.4 s, x 1.34 (this file's CI-over-local ratio before: 161.8 s in hourly full battery run 40290659753 over a 120.7 s local median) x 1.25, rounded up; no CI reading of this shape yet /** * #21489 — a package's job BODIES run on every door that brings an artifact diff --git a/packages/cli/test/package-install-local-uninstall-cleanups.integration.test.ts b/packages/cli/test/package-install-local-uninstall-cleanups.integration.test.ts index b00a5fb539d..f7e5dc8caf5 100644 --- a/packages/cli/test/package-install-local-uninstall-cleanups.integration.test.ts +++ b/packages/cli/test/package-install-local-uninstall-cleanups.integration.test.ts @@ -1,4 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// test-budget: 180 — spawns the real producer once per single-cause fixture: eight `os start` boots of the tsx source entry across four orders of events, each order its own cwd and home and the four run at once, and five `os package install` runs; slowest of three local runs after the orders ran at once 105.5 s, x 1.35 (this file's CI-over-local ratio before: 270.1 s in hourly full battery run 40290659753 over a 200.5 s local median) x 1.25, rounded up; no CI reading of this shape yet /** * #21490 — an install-local uninstall (`DELETE /api/v1/marketplace/install-local/:id`) From 4e99c0594b4c3fbcebbf9a81a9814e0c8bfb8683 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 14:58:21 +0000 Subject: [PATCH 3/5] test(cli): the uninstall orders run two at a time, not four Four live os start trees at once, beside the package's other workers, ran a 15 GB box out of memory. Two pairs of four boots and three installs each keep most of the saving. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01DcLzU9qErDBr5VydWEmHVM --- ...cal-uninstall-cleanups.integration.test.ts | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/cli/test/package-install-local-uninstall-cleanups.integration.test.ts b/packages/cli/test/package-install-local-uninstall-cleanups.integration.test.ts index f7e5dc8caf5..171067728c8 100644 --- a/packages/cli/test/package-install-local-uninstall-cleanups.integration.test.ts +++ b/packages/cli/test/package-install-local-uninstall-cleanups.integration.test.ts @@ -59,9 +59,9 @@ * package — `@objectstack/cloud-connection` and `@objectstack/metadata-protocol` * included — resolved through its `exports` to `dist/`, so an ablation of either * package's source reaches this file only after that package is rebuilt. Every - * boot runs in `beforeAll`; the `it`s only read what the phases recorded. The - * four orders run at once, each in its own cwd and home: one order's steps - * run in sequence, and no order reads another's. + * boot runs in `beforeAll`; the `it`s only read what the phases recorded. Each + * order has its own cwd and home and runs its steps in sequence; two orders run + * at a time, and no order reads another's. */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; @@ -352,10 +352,9 @@ beforeAll(async () => { // The four orders share nothing but the two package directories they read: // each has its own cwd, home and ledger, and every boot binds a - // kernel-assigned port. So they run at once, and the file waits for the - // longest one rather than for their sum. Inside an order every step still - // waits for the one before it. - await allPhases([ + // kernel-assigned port. Inside an order every step still waits for the one + // before it. + const [hot, restarted, reseed, reinstall]: Array<() => Promise> = [ // ── order 1: hot install → DELETE → restart ──────────────────────────── async () => { const hotDir = join(root, 'hot'); @@ -427,6 +426,14 @@ beforeAll(async () => { }; await stopGroup(h.child); }, + ]; + // Two orders at a time, paired so each pair is four boots and three installs. + // ⛔ Not all four at once: every live `os start` holds well over a gigabyte, + // and four of them beside the package's other workers ran a 15 GB box out of + // memory (exit 137, SIGKILL). + await allPhases([ + async () => { await hot(); await reseed(); }, + async () => { await restarted(); await reinstall(); }, ]); }, 9 * BOOT_TIMEOUT_MS); From db8bd85c195eaa474eaab49629a8bbb8a08e045e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 15:24:25 +0000 Subject: [PATCH 4/5] test(cli): the uninstall suite's budget follows its two-at-a-time reading Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01DcLzU9qErDBr5VydWEmHVM --- ...package-install-local-uninstall-cleanups.integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/test/package-install-local-uninstall-cleanups.integration.test.ts b/packages/cli/test/package-install-local-uninstall-cleanups.integration.test.ts index 171067728c8..39225388acf 100644 --- a/packages/cli/test/package-install-local-uninstall-cleanups.integration.test.ts +++ b/packages/cli/test/package-install-local-uninstall-cleanups.integration.test.ts @@ -1,5 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. -// test-budget: 180 — spawns the real producer once per single-cause fixture: eight `os start` boots of the tsx source entry across four orders of events, each order its own cwd and home and the four run at once, and five `os package install` runs; slowest of three local runs after the orders ran at once 105.5 s, x 1.35 (this file's CI-over-local ratio before: 270.1 s in hourly full battery run 40290659753 over a 200.5 s local median) x 1.25, rounded up; no CI reading of this shape yet +// test-budget: 200 — spawns the real producer once per single-cause fixture: eight `os start` boots of the tsx source entry across four orders of events, each order its own cwd and home and two orders at a time, and five `os package install` runs; slowest of three local runs after the orders ran two at a time 114.3 s, x 1.35 (this file's CI-over-local ratio before: 270.1 s in hourly full battery run 40290659753 over a 200.5 s local median) x 1.25, rounded up; no CI reading of this shape yet /** * #21490 — an install-local uninstall (`DELETE /api/v1/marketplace/install-local/:id`) From ff0ca4cc8752bec9404c38e7ee1c608851763504 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 16:19:11 +0000 Subject: [PATCH 5/5] test(cli): install-local chains stay sequential; budgets from CI readings Seat review: running chains side by side packs work on CI's shared box and adds about 3.3 GB of peak RSS per file, so every chain runs in sequence again, as on main. Each file's budget is now its slowest CI reading of that sequential shape x 1.25, rounded up. Co-authored-by: Claude Claude-Session: https://claude.ai/code/session_01DcLzU9qErDBr5VydWEmHVM --- ...stall-local-boot-steps.integration.test.ts | 54 ++---- ...install-local-handlers.integration.test.ts | 62 +++---- ...ge-install-local-hooks.integration.test.ts | 2 +- ...nstall-local-jobs-pull.integration.test.ts | 2 +- ...local-jobs-shared-name.integration.test.ts | 2 +- ...age-install-local-jobs.integration.test.ts | 120 ++++++------- ...cal-uninstall-cleanups.integration.test.ts | 165 +++++++----------- 7 files changed, 156 insertions(+), 251 deletions(-) diff --git a/packages/cli/test/package-install-local-boot-steps.integration.test.ts b/packages/cli/test/package-install-local-boot-steps.integration.test.ts index 452b1d61f49..84808bc4ddf 100644 --- a/packages/cli/test/package-install-local-boot-steps.integration.test.ts +++ b/packages/cli/test/package-install-local-boot-steps.integration.test.ts @@ -1,5 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. -// test-budget: 70 — spawns the real producer once per single-cause fixture: three `os start` boots of the tsx source entry (install, restart on the same home, and the `--artifact` control on its own home, run beside the other two) and one `os package install` run; slowest of three local runs after the control ran beside them 49.1 s, x 1.11 (this file's CI-over-local ratio before: 68.2 s in hourly full battery run 40290659753 over a 61.3 s local median) x 1.25, rounded up; no CI reading of this shape yet +// test-budget: 120 — spawns the real producer once per single-cause fixture: three `os start` boots of the tsx source entry in sequence (install, restart on the same home, and the `--artifact` control on its own home) and one `os package install` run; slowest CI reading of this shape 94.3 s (run 38222448090, the card's window; 68.2 s in hourly full battery run 40290659753) x 1.25, rounded up /** * #21322 — a hot install (`os package install ` into a RUNNING @@ -301,17 +301,6 @@ async function probe(live: LiveStart, token: string): Promise { return { permissionSet, created, updated, notes }; } -/** - * Run phases that share no state at once, and wait for EVERY one to settle - * before reporting the first failure: a phase that rejects must not leave a - * sibling still booting after `afterAll` has stopped the process groups. - */ -async function allPhases(chains: Array<() => Promise>): Promise { - const settled = await Promise.allSettled(chains.map((chain) => chain())); - const failed = settled.find((s): s is PromiseRejectedResult => s.status === 'rejected'); - if (failed) throw failed.reason; -} - const phases: Record<'install' | 'restart' | 'control', Phase | undefined> = { install: undefined, restart: undefined, control: undefined, }; @@ -331,33 +320,24 @@ beforeAll(async () => { writeFileSync(hostArtifact, JSON.stringify(HOST_ARTIFACT, null, 2), 'utf8'); const home = join(runtimeDir, 'home'); - // Boots 1 and 2 share one home and run in order. Boot 3, the CONTROL, has - // its own cwd and home and only reads the package file, so it runs beside - // them; every boot binds a kernel-assigned port. - await allPhases([ - // ── boot 1: the host, hot install, probe ─────────────────────────────── - async () => { - const first = await bootStart(runtimeDir, home, ['--artifact', hostArtifact]); - const token = await authenticate(first); - installs.push(await packageInstall(appDir, first)); - phases.install = await probe(first, token); - await stopGroup(first.child); + // ── boot 1: the host, hot install, probe ─────────────────────────────── + const first = await bootStart(runtimeDir, home, ['--artifact', hostArtifact]); + const token = await authenticate(first); + installs.push(await packageInstall(appDir, first)); + phases.install = await probe(first, token); + await stopGroup(first.child); - // ── boot 2: same host, home and cwd — the ledger rehydrates on kernel:ready ── - const second = await bootStart(runtimeDir, home, ['--artifact', hostArtifact]); - phases.restart = await probe(second, await authenticate(second)); - await stopGroup(second.child); - }, + // ── boot 2: same host, home and cwd — the ledger rehydrates on kernel:ready ── + const second = await bootStart(runtimeDir, home, ['--artifact', hostArtifact]); + phases.restart = await probe(second, await authenticate(second)); + await stopGroup(second.child); - // ── boot 3: the CONTROL — the same file as the boot artifact ─────────── - async () => { - const controlDir = join(root, 'control'); - mkdirSync(controlDir, { recursive: true }); - const third = await bootStart(controlDir, join(controlDir, 'home'), ['--artifact', join(appDir, 'dist', 'objectstack.json')]); - phases.control = await probe(third, await authenticate(third)); - await stopGroup(third.child); - }, - ]); + // ── boot 3: the CONTROL — the same file as the boot artifact ─────────── + const controlDir = join(root, 'control'); + mkdirSync(controlDir, { recursive: true }); + const third = await bootStart(controlDir, join(controlDir, 'home'), ['--artifact', join(appDir, 'dist', 'objectstack.json')]); + phases.control = await probe(third, await authenticate(third)); + await stopGroup(third.child); }, 4 * BOOT_TIMEOUT_MS); afterAll(async () => { diff --git a/packages/cli/test/package-install-local-handlers.integration.test.ts b/packages/cli/test/package-install-local-handlers.integration.test.ts index 40c8258eb62..b1785631333 100644 --- a/packages/cli/test/package-install-local-handlers.integration.test.ts +++ b/packages/cli/test/package-install-local-handlers.integration.test.ts @@ -1,5 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. -// test-budget: 100 — spawns the real producer once per single-cause fixture: three `os start` boots of the tsx source entry (install and reinstall, restart on the same home, and the `--artifact` control on its own home, run beside the other two) and two `os package install` runs; slowest of three local runs after the control ran beside them 57.5 s, x 1.35 (this file's CI-over-local ratio before: 97.8 s in hourly full battery run 40290659753 over a 72.7 s local median) x 1.25, rounded up; no CI reading of this shape yet +// test-budget: 130 — spawns the real producer once per single-cause fixture: three `os start` boots of the tsx source entry in sequence (install and reinstall, restart on the same home, and the `--artifact` control on its own home) and two `os package install` runs; slowest CI reading of this shape 99.4 s (run 38332198724, the card's window; 97.8 s in hourly full battery run 40290659753) x 1.25, rounded up /** * #21321 — an app installed with `os package install ` runs its @@ -311,17 +311,6 @@ async function probe(live: LiveStart, token: string): Promise { const rec = (a: Answer) => a.body?.data ?? a.body?.record ?? a.body; -/** - * Run phases that share no state at once, and wait for EVERY one to settle - * before reporting the first failure: a phase that rejects must not leave a - * sibling still booting after `afterAll` has stopped the process groups. - */ -async function allPhases(chains: Array<() => Promise>): Promise { - const settled = await Promise.allSettled(chains.map((chain) => chain())); - const failed = settled.find((s): s is PromiseRejectedResult => s.status === 'rejected'); - if (failed) throw failed.reason; -} - const phases: Record<'install' | 'reinstall' | 'restart' | 'control', Phase | undefined> = { install: undefined, reinstall: undefined, restart: undefined, control: undefined, }; @@ -338,35 +327,26 @@ beforeAll(async () => { mkdirSync(runtimeDir, { recursive: true }); const home = join(runtimeDir, 'home'); - // Boots 1 and 2 share one home and run in order. Boot 3, the CONTROL, has - // its own cwd and home and only reads the package file, so it runs beside - // them; every boot binds a kernel-assigned port. - await allPhases([ - // ── boot 1: empty `os start`, install, probe, reinstall, probe ───────── - async () => { - const first = await bootStart(runtimeDir, home); - const token = await authenticate(first); - installs.push(await packageInstall(appDir, first)); - phases.install = await probe(first, token); - installs.push(await packageInstall(appDir, first)); - phases.reinstall = await probe(first, token); - await stopGroup(first.child); - - // ── boot 2: same home and cwd — the ledger rehydrates on kernel:ready ── - const second = await bootStart(runtimeDir, home); - phases.restart = await probe(second, await authenticate(second)); - await stopGroup(second.child); - }, - - // ── boot 3: the CONTROL — the same file as the boot artifact ─────────── - async () => { - const controlDir = join(root, 'control'); - mkdirSync(controlDir, { recursive: true }); - const third = await bootStart(controlDir, join(controlDir, 'home'), ['--artifact', join(appDir, 'dist', 'objectstack.json')]); - phases.control = await probe(third, await authenticate(third)); - await stopGroup(third.child); - }, - ]); + // ── boot 1: empty `os start`, install, probe, reinstall, probe ───────── + const first = await bootStart(runtimeDir, home); + const token = await authenticate(first); + installs.push(await packageInstall(appDir, first)); + phases.install = await probe(first, token); + installs.push(await packageInstall(appDir, first)); + phases.reinstall = await probe(first, token); + await stopGroup(first.child); + + // ── boot 2: same home and cwd — the ledger rehydrates on kernel:ready ── + const second = await bootStart(runtimeDir, home); + phases.restart = await probe(second, await authenticate(second)); + await stopGroup(second.child); + + // ── boot 3: the CONTROL — the same file as the boot artifact ─────────── + const controlDir = join(root, 'control'); + mkdirSync(controlDir, { recursive: true }); + const third = await bootStart(controlDir, join(controlDir, 'home'), ['--artifact', join(appDir, 'dist', 'objectstack.json')]); + phases.control = await probe(third, await authenticate(third)); + await stopGroup(third.child); }, 4 * BOOT_TIMEOUT_MS); afterAll(async () => { diff --git a/packages/cli/test/package-install-local-hooks.integration.test.ts b/packages/cli/test/package-install-local-hooks.integration.test.ts index 3c77fabef79..3245570ae5d 100644 --- a/packages/cli/test/package-install-local-hooks.integration.test.ts +++ b/packages/cli/test/package-install-local-hooks.integration.test.ts @@ -1,5 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. -// test-budget: 150 — spawns the real producer once per single-cause fixture: two `os start` boots of the tsx source entry on one home (the hot installs, then the rehydrate of an entry an earlier build wrote) and five `os package install` runs, one per package under test; slowest CI reading 114.4 s (hourly full battery run 40290659753, 2026-10-26) x 1.25, rounded up +// test-budget: 150 — spawns the real producer once per single-cause fixture: two `os start` boots of the tsx source entry on one home (the hot installs, then the rehydrate of an entry an earlier build wrote) and five `os package install` runs, one per package under test; slowest CI reading 117.1 s (run 38332198724, the card's window; 114.4 s in hourly full battery run 40290659753) x 1.25, rounded up /** * #21585 — install-local refuses the code it cannot run: a hook with no `body`, diff --git a/packages/cli/test/package-install-local-jobs-pull.integration.test.ts b/packages/cli/test/package-install-local-jobs-pull.integration.test.ts index fa7920cb95e..fcab92ed5d1 100644 --- a/packages/cli/test/package-install-local-jobs-pull.integration.test.ts +++ b/packages/cli/test/package-install-local-jobs-pull.integration.test.ts @@ -1,5 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. -// test-budget: 140 — spawns the real producer once per single-cause fixture: two `os start` boots of the tsx source entry on one home (the hot installs, then the rehydrate of an entry an earlier build wrote) and four `os package install` runs, one per package under test; slowest CI reading 107.9 s (run 38335967790, the card's window; the job logs of run 40290659753 read here carry no line for this file) x 1.25, rounded up +// test-budget: 140 — spawns the real producer once per single-cause fixture: two `os start` boots of the tsx source entry on one home (the hot installs, then the rehydrate of an entry an earlier build wrote) and four `os package install` runs, one per package under test; slowest CI reading 107.9 s (run 38335967790, the card's window; the job logs of hourly full battery run 40290659753 read here carry no line for this file) x 1.25, rounded up /** * install-local refuses an enabled job whose `pull` cannot bind, as it refuses diff --git a/packages/cli/test/package-install-local-jobs-shared-name.integration.test.ts b/packages/cli/test/package-install-local-jobs-shared-name.integration.test.ts index c46561fe018..f25adea7df2 100644 --- a/packages/cli/test/package-install-local-jobs-shared-name.integration.test.ts +++ b/packages/cli/test/package-install-local-jobs-shared-name.integration.test.ts @@ -1,5 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. -// test-budget: 140 — spawns the real producer once per single-cause fixture: two `os start` boots of the tsx source entry on one home (the hot installs and an uninstall, then the rehydrate and a second uninstall) and three `os package install` runs, one per package sharing the job name; slowest CI reading 107.1 s (hourly full battery run 40290659753, 2026-10-26) x 1.25, rounded up +// test-budget: 140 — spawns the real producer once per single-cause fixture: two `os start` boots of the tsx source entry on one home (the hot installs and an uninstall, then the rehydrate and a second uninstall) and three `os package install` runs, one per package sharing the job name; slowest CI reading 110.4 s (run 38332198724, the card's window; 107.1 s in hourly full battery run 40290659753) x 1.25, rounded up /** * #21602 — two install-local packages that declare the SAME job name both keep diff --git a/packages/cli/test/package-install-local-jobs.integration.test.ts b/packages/cli/test/package-install-local-jobs.integration.test.ts index 2d3315b263f..07c2998219c 100644 --- a/packages/cli/test/package-install-local-jobs.integration.test.ts +++ b/packages/cli/test/package-install-local-jobs.integration.test.ts @@ -1,5 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. -// test-budget: 180 — spawns the real producer once per single-cause fixture: three `os start` boots of the tsx source entry (install, restart on the same home, and the `--artifact` control on its own home, run beside the other two) and six `os package install` runs; slowest of three local runs after the control ran beside them 104.4 s, x 1.34 (this file's CI-over-local ratio before: 161.8 s in hourly full battery run 40290659753 over a 120.7 s local median) x 1.25, rounded up; no CI reading of this shape yet +// test-budget: 210 — spawns the real producer once per single-cause fixture: three `os start` boots of the tsx source entry in sequence (install, restart on the same home, and the `--artifact` control on its own home) and six `os package install` runs; slowest CI reading of this shape 161.8 s (hourly full battery run 40290659753; 160.9 s in run 38332198724, the card's window) x 1.25, rounded up /** * #21489 — a package's job BODIES run on every door that brings an artifact @@ -359,17 +359,6 @@ async function quietAfter(live: LiveStart, token: string, object: string, job: s return { floor, after }; } -/** - * Run phases that share no state at once, and wait for EVERY one to settle - * before reporting the first failure: a phase that rejects must not leave a - * sibling still booting after `afterAll` has stopped the process groups. - */ -async function allPhases(chains: Array<() => Promise>): Promise { - const settled = await Promise.allSettled(chains.map((chain) => chain())); - const failed = settled.find((s): s is PromiseRejectedResult => s.status === 'rejected'); - if (failed) throw failed.reason; -} - interface InstallRun { exit: number | null; output: string } const readings: { bodyInstall?: InstallRun; @@ -423,64 +412,55 @@ beforeAll(async () => { writeFileSync(hostArtifact, JSON.stringify(HOST_ARTIFACT, null, 2), 'utf8'); const home = join(runtimeDir, 'home'); - // Boots 1 and 2 share one home and run in order. Boot 3, the CONTROL, has - // its own cwd and home and reads nothing they write, so it runs beside - // them; every boot binds a kernel-assigned port. - await allPhases([ - // ── boot 1: the host, hot installs, the body job runs ───────────────── - async () => { - const first = await bootStart(runtimeDir, home, ['--artifact', hostArtifact]); - const token = await authenticate(first); - readings.bodyInstall = await packageInstall(bodyApp, first); - readings.otherInstall = await packageInstall(otherApp, first); - readings.dropInstall = await packageInstall(dropAppV1, first); - readings.handlerInstall = await packageInstall(handlerApp, first); - readings.installed = await http(first, 'GET', '/api/v1/marketplace/install-local', token); - readings.afterInstall = await awaitRuns(first, token, HOST_TICK, BODY_JOB, 0); - - // Reinstall the drop package with a version that no longer declares DROP_GONE. - readings.dropBefore = await awaitRuns(first, token, HOST_TICK, DROP_GONE, 0); - readings.dropReinstall = await packageInstall(dropAppV2, first); - readings.dropGoneHot = await quietAfter(first, token, HOST_TICK, DROP_GONE); - readings.dropKeptHot = await awaitRuns(first, token, HOST_TICK, DROP_KEPT, - rowsOf(await jobRows(first, token, HOST_TICK, DROP_KEPT)).length); - - // Uninstall the body package; the other package is the control. - readings.uninstall = await http(first, 'DELETE', `/api/v1/marketplace/install-local/${BODY_APP_ID}`, token); - readings.bodyGoneHot = await quietAfter(first, token, HOST_TICK, BODY_JOB); - readings.otherHot = await awaitRuns(first, token, HOST_TICK, OTHER_JOB, - rowsOf(await jobRows(first, token, HOST_TICK, OTHER_JOB)).length); - readings.output.install = first.output(); - await stopGroup(first.child); - - // ── boot 2: same host, home and cwd — the ledger rehydrates on kernel:ready ── - const second = await bootStart(runtimeDir, home, ['--artifact', hostArtifact]); - const token2 = await authenticate(second); - // The rows boot 1 left behind are the floors: only a run in THIS boot lifts one. - const floorOf = async (object: string, job: string) => rowsOf(await jobRows(second, token2, object, job)).length; - const bodyFloor = await floorOf(HOST_TICK, BODY_JOB); - const goneFloor = await floorOf(HOST_TICK, DROP_GONE); - const restartedAt = Date.now(); - readings.afterRestart = await awaitRuns(second, token2, HOST_TICK, OTHER_JOB, await floorOf(HOST_TICK, OTHER_JOB)); - readings.dropKeptRestart = await awaitRuns(second, token2, HOST_TICK, DROP_KEPT, await floorOf(HOST_TICK, DROP_KEPT)); - await sleep(Math.max(0, QUIET_WAIT_MS - (Date.now() - restartedAt))); - readings.bodyGoneRestart = { floor: bodyFloor, after: await floorOf(HOST_TICK, BODY_JOB) }; - readings.dropGoneRestart = { floor: goneFloor, after: await floorOf(HOST_TICK, DROP_GONE) }; - readings.output.restart = second.output(); - await stopGroup(second.child); - }, - - // ── boot 3: the CONTROL — both job forms in the boot artifact ───────── - async () => { - const controlHome = join(controlApp, 'home'); - const third = await bootStart(controlApp, controlHome, ['--artifact', join(controlApp, 'dist', 'objectstack.json')]); - const token3 = await authenticate(third); - readings.controlHandler = await awaitRuns(third, token3, HANDLER_TICK, HANDLER_JOB, 0); - readings.controlBody = await awaitRuns(third, token3, BODY_TICK, BODY_JOB, 0); - readings.output.control = third.output(); - await stopGroup(third.child); - }, - ]); + // ── boot 1: the host, hot installs, the body job runs ───────────────── + const first = await bootStart(runtimeDir, home, ['--artifact', hostArtifact]); + const token = await authenticate(first); + readings.bodyInstall = await packageInstall(bodyApp, first); + readings.otherInstall = await packageInstall(otherApp, first); + readings.dropInstall = await packageInstall(dropAppV1, first); + readings.handlerInstall = await packageInstall(handlerApp, first); + readings.installed = await http(first, 'GET', '/api/v1/marketplace/install-local', token); + readings.afterInstall = await awaitRuns(first, token, HOST_TICK, BODY_JOB, 0); + + // Reinstall the drop package with a version that no longer declares DROP_GONE. + readings.dropBefore = await awaitRuns(first, token, HOST_TICK, DROP_GONE, 0); + readings.dropReinstall = await packageInstall(dropAppV2, first); + readings.dropGoneHot = await quietAfter(first, token, HOST_TICK, DROP_GONE); + readings.dropKeptHot = await awaitRuns(first, token, HOST_TICK, DROP_KEPT, + rowsOf(await jobRows(first, token, HOST_TICK, DROP_KEPT)).length); + + // Uninstall the body package; the other package is the control. + readings.uninstall = await http(first, 'DELETE', `/api/v1/marketplace/install-local/${BODY_APP_ID}`, token); + readings.bodyGoneHot = await quietAfter(first, token, HOST_TICK, BODY_JOB); + readings.otherHot = await awaitRuns(first, token, HOST_TICK, OTHER_JOB, + rowsOf(await jobRows(first, token, HOST_TICK, OTHER_JOB)).length); + readings.output.install = first.output(); + await stopGroup(first.child); + + // ── boot 2: same host, home and cwd — the ledger rehydrates on kernel:ready ── + const second = await bootStart(runtimeDir, home, ['--artifact', hostArtifact]); + const token2 = await authenticate(second); + // The rows boot 1 left behind are the floors: only a run in THIS boot lifts one. + const floorOf = async (object: string, job: string) => rowsOf(await jobRows(second, token2, object, job)).length; + const bodyFloor = await floorOf(HOST_TICK, BODY_JOB); + const goneFloor = await floorOf(HOST_TICK, DROP_GONE); + const restartedAt = Date.now(); + readings.afterRestart = await awaitRuns(second, token2, HOST_TICK, OTHER_JOB, await floorOf(HOST_TICK, OTHER_JOB)); + readings.dropKeptRestart = await awaitRuns(second, token2, HOST_TICK, DROP_KEPT, await floorOf(HOST_TICK, DROP_KEPT)); + await sleep(Math.max(0, QUIET_WAIT_MS - (Date.now() - restartedAt))); + readings.bodyGoneRestart = { floor: bodyFloor, after: await floorOf(HOST_TICK, BODY_JOB) }; + readings.dropGoneRestart = { floor: goneFloor, after: await floorOf(HOST_TICK, DROP_GONE) }; + readings.output.restart = second.output(); + await stopGroup(second.child); + + // ── boot 3: the CONTROL — both job forms in the boot artifact ───────── + const controlHome = join(controlApp, 'home'); + const third = await bootStart(controlApp, controlHome, ['--artifact', join(controlApp, 'dist', 'objectstack.json')]); + const token3 = await authenticate(third); + readings.controlHandler = await awaitRuns(third, token3, HANDLER_TICK, HANDLER_JOB, 0); + readings.controlBody = await awaitRuns(third, token3, BODY_TICK, BODY_JOB, 0); + readings.output.control = third.output(); + await stopGroup(third.child); }, 3 * BOOT_TIMEOUT_MS + 12 * RUN_WAIT_MS); afterAll(async () => { diff --git a/packages/cli/test/package-install-local-uninstall-cleanups.integration.test.ts b/packages/cli/test/package-install-local-uninstall-cleanups.integration.test.ts index 39225388acf..362323c7771 100644 --- a/packages/cli/test/package-install-local-uninstall-cleanups.integration.test.ts +++ b/packages/cli/test/package-install-local-uninstall-cleanups.integration.test.ts @@ -1,5 +1,5 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. -// test-budget: 200 — spawns the real producer once per single-cause fixture: eight `os start` boots of the tsx source entry across four orders of events, each order its own cwd and home and two orders at a time, and five `os package install` runs; slowest of three local runs after the orders ran two at a time 114.3 s, x 1.35 (this file's CI-over-local ratio before: 270.1 s in hourly full battery run 40290659753 over a 200.5 s local median) x 1.25, rounded up; no CI reading of this shape yet +// test-budget: 350 — spawns the real producer once per single-cause fixture: eight `os start` boots of the tsx source entry across four orders of events, run in sequence, each order its own cwd and home, and five `os package install` runs; slowest CI reading of this shape 275.2 s (run 38332198724, the card's window; 270.1 s in hourly full battery run 40290659753) x 1.25, rounded up /** * #21490 — an install-local uninstall (`DELETE /api/v1/marketplace/install-local/:id`) @@ -59,9 +59,7 @@ * package — `@objectstack/cloud-connection` and `@objectstack/metadata-protocol` * included — resolved through its `exports` to `dist/`, so an ablation of either * package's source reaches this file only after that package is rebuilt. Every - * boot runs in `beforeAll`; the `it`s only read what the phases recorded. Each - * order has its own cwd and home and runs its steps in sequence; two orders run - * at a time, and no order reads another's. + * boot runs in `beforeAll`; the `it`s only read what the phases recorded. */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; @@ -327,17 +325,6 @@ async function readAfterRestart(live: LiveStart, run: Run): Promise { run.object = await http(live, 'GET', `/api/v1/data/${TASK}`, token); } -/** - * Run phases that share no state at once, and wait for EVERY one to settle - * before reporting the first failure: a phase that rejects must not leave a - * sibling still booting after `afterAll` has stopped the process groups. - */ -async function allPhases(chains: Array<() => Promise>): Promise { - const settled = await Promise.allSettled(chains.map((chain) => chain())); - const failed = settled.find((s): s is PromiseRejectedResult => s.status === 'rejected'); - if (failed) throw failed.reason; -} - const runs: Record<'hot' | 'restarted' | 'reseed' | 'reinstall', Run> = { hot: {}, restarted: {}, reseed: {}, reinstall: {} }; beforeAll(async () => { @@ -350,91 +337,69 @@ beforeAll(async () => { mkdirSync(join(otherAppDir, 'dist'), { recursive: true }); writeFileSync(join(otherAppDir, 'dist', 'objectstack.json'), JSON.stringify(OTHER_ARTIFACT, null, 2), 'utf8'); - // The four orders share nothing but the two package directories they read: - // each has its own cwd, home and ledger, and every boot binds a - // kernel-assigned port. Inside an order every step still waits for the one - // before it. - const [hot, restarted, reseed, reinstall]: Array<() => Promise> = [ - // ── order 1: hot install → DELETE → restart ──────────────────────────── - async () => { - const hotDir = join(root, 'hot'); - mkdirSync(hotDir, { recursive: true }); - const hotHome = join(hotDir, 'home'); - const a = await bootStart(hotDir, hotHome); - const aSession = await authenticate(a); - runs.hot.install = await packageInstall(appDir, a); - await grantThenUninstall(a, aSession, runs.hot); - await stopGroup(a.child); - const b = await bootStart(hotDir, hotHome); - await readAfterRestart(b, runs.hot); - await stopGroup(b.child); - }, - - // ── order 2: install → restart → DELETE → restart ────────────────────── - // The DELETE meets a package the ledger REHYDRATED, not one this process - // hot-installed — the path that existed before #21322's change. - async () => { - const coldDir = join(root, 'cold'); - mkdirSync(coldDir, { recursive: true }); - const coldHome = join(coldDir, 'home'); - const c = await bootStart(coldDir, coldHome); - await authenticate(c); - runs.restarted.install = await packageInstall(appDir, c); - await stopGroup(c.child); - const d = await bootStart(coldDir, coldHome); - await grantThenUninstall(d, await authenticate(d), runs.restarted); - await stopGroup(d.child); - const e = await bootStart(coldDir, coldHome); - await readAfterRestart(e, runs.restarted); - await stopGroup(e.child); - }, - - // ── order 3: hot install → DELETE → hot install of ANOTHER package → restart ── - // The second install announces `metadata:reloaded` into the process the - // DELETE ran in, so whatever that process still counts as registered is - // re-seeded — the withdrawal is what keeps the uninstalled package out of it. - async () => { - const reseedDir = join(root, 'reseed'); - mkdirSync(reseedDir, { recursive: true }); - const reseedHome = join(reseedDir, 'home'); - const f = await bootStart(reseedDir, reseedHome); - const fSession = await authenticate(f); - runs.reseed.install = await packageInstall(appDir, f); - await grantThenUninstall(f, fSession, runs.reseed); - runs.reseed.otherInstall = await packageInstall(otherAppDir, f); - runs.reseed.afterOtherInstall = await readGrants(f, fSession.token); - await stopGroup(f.child); - const g = await bootStart(reseedDir, reseedHome); - await readAfterRestart(g, runs.reseed); - await stopGroup(g.child); - }, - - // ── order 4: hot install → DELETE → hot reinstall of the SAME package ────── - // The withdrawal's control: whatever `uninstallPackage` took from the running - // kernel, the install path registers again, in the same process. - async () => { - const reinstallDir = join(root, 'reinstall'); - mkdirSync(reinstallDir, { recursive: true }); - const h = await bootStart(reinstallDir, join(reinstallDir, 'home')); - const hSession = await authenticate(h); - runs.reinstall.install = await packageInstall(appDir, h); - await grantThenUninstall(h, hSession, runs.reinstall); - runs.reinstall.reinstall = await packageInstall(appDir, h); - runs.reinstall.afterReinstall = { - object: await http(h, 'GET', `/api/v1/data/${TASK}`, hSession.token), - ...await readGrants(h, hSession.token), - }; - await stopGroup(h.child); - }, - ]; - // Two orders at a time, paired so each pair is four boots and three installs. - // ⛔ Not all four at once: every live `os start` holds well over a gigabyte, - // and four of them beside the package's other workers ran a 15 GB box out of - // memory (exit 137, SIGKILL). - await allPhases([ - async () => { await hot(); await reseed(); }, - async () => { await restarted(); await reinstall(); }, - ]); + // ── order 1: hot install → DELETE → restart ──────────────────────────── + const hotDir = join(root, 'hot'); + mkdirSync(hotDir, { recursive: true }); + const hotHome = join(hotDir, 'home'); + const a = await bootStart(hotDir, hotHome); + const aSession = await authenticate(a); + runs.hot.install = await packageInstall(appDir, a); + await grantThenUninstall(a, aSession, runs.hot); + await stopGroup(a.child); + const b = await bootStart(hotDir, hotHome); + await readAfterRestart(b, runs.hot); + await stopGroup(b.child); + + // ── order 2: install → restart → DELETE → restart ────────────────────── + // The DELETE meets a package the ledger REHYDRATED, not one this process + // hot-installed — the path that existed before #21322's change. + const coldDir = join(root, 'cold'); + mkdirSync(coldDir, { recursive: true }); + const coldHome = join(coldDir, 'home'); + const c = await bootStart(coldDir, coldHome); + await authenticate(c); + runs.restarted.install = await packageInstall(appDir, c); + await stopGroup(c.child); + const d = await bootStart(coldDir, coldHome); + await grantThenUninstall(d, await authenticate(d), runs.restarted); + await stopGroup(d.child); + const e = await bootStart(coldDir, coldHome); + await readAfterRestart(e, runs.restarted); + await stopGroup(e.child); + + // ── order 3: hot install → DELETE → hot install of ANOTHER package → restart ── + // The second install announces `metadata:reloaded` into the process the + // DELETE ran in, so whatever that process still counts as registered is + // re-seeded — the withdrawal is what keeps the uninstalled package out of it. + const reseedDir = join(root, 'reseed'); + mkdirSync(reseedDir, { recursive: true }); + const reseedHome = join(reseedDir, 'home'); + const f = await bootStart(reseedDir, reseedHome); + const fSession = await authenticate(f); + runs.reseed.install = await packageInstall(appDir, f); + await grantThenUninstall(f, fSession, runs.reseed); + runs.reseed.otherInstall = await packageInstall(otherAppDir, f); + runs.reseed.afterOtherInstall = await readGrants(f, fSession.token); + await stopGroup(f.child); + const g = await bootStart(reseedDir, reseedHome); + await readAfterRestart(g, runs.reseed); + await stopGroup(g.child); + + // ── order 4: hot install → DELETE → hot reinstall of the SAME package ────── + // The withdrawal's control: whatever `uninstallPackage` took from the running + // kernel, the install path registers again, in the same process. + const reinstallDir = join(root, 'reinstall'); + mkdirSync(reinstallDir, { recursive: true }); + const h = await bootStart(reinstallDir, join(reinstallDir, 'home')); + const hSession = await authenticate(h); + runs.reinstall.install = await packageInstall(appDir, h); + await grantThenUninstall(h, hSession, runs.reinstall); + runs.reinstall.reinstall = await packageInstall(appDir, h); + runs.reinstall.afterReinstall = { + object: await http(h, 'GET', `/api/v1/data/${TASK}`, hSession.token), + ...await readGrants(h, hSession.token), + }; + await stopGroup(h.child); }, 9 * BOOT_TIMEOUT_MS); afterAll(async () => {