From efde83b9aa13b4d0b882d238d498a89843483138 Mon Sep 17 00:00:00 2001 From: JasonBuildAI Date: Tue, 13 Oct 2026 12:24:15 +0800 Subject: [PATCH 1/2] test(control-plane): guard remaining host-dependent Windows assertions Native Windows contributors running `npm run test:control-plane` get 32 host-dependent false failures and cannot use the suite as a pass/fail signal. Every class is the out_of_scope remainder declared by #6207: - 20 self-SIGKILL process-death checks assert `child.signal === "SIGKILL"`; Windows terminates via TerminateProcess and reports an exit status, so assert a non-zero status there and keep the signal assertion on POSIX (same guard as sqlite_authority_store.test.ts). - 9 file-symlink fixtures fail with EPERM because Windows file symlink creation needs Developer Mode or admin. Skip the file-symlink variant where it is the only subject, guard the block where the test also covers other rejection rules, and use the suite's existing junction fallback for directory symlinks. - The mode-000 preference check is POSIX-only, matching the Python twin made POSIX-only in #6007. - The simulated Linux directory-fsync boundary needs a real POSIX kernel. - The documented goal-acceptance configuration is read with a CRLF-aware fence on a core.autocrlf checkout. Production code is unchanged; POSIX behavior is unchanged. Refs #6351 Signed-off-by: JasonBuildAI --- .../agent_preferences.test.ts | 4 +++- .../authority_archive.test.ts | 7 ++++++- .../canonical_task_lease_renew.test.ts | 20 +++++++++++++++---- .../cold_source_import.test.ts | 4 ++++ .../coordination_runtime_shadow.test.ts | 11 +++++++--- .../effect_runtime_snapshot.test.ts | 19 ++++++++++++------ .../goal_acceptance_authority.test.ts | 2 +- .../quota_void_commit.test.ts | 18 ++++++++++------- .../shadow_entry_delivery.test.ts | 4 ++++ tests/control_plane_ts/sqlite_backup.test.ts | 7 +++++-- 10 files changed, 71 insertions(+), 25 deletions(-) diff --git a/tests/control_plane_ts/agent_preferences.test.ts b/tests/control_plane_ts/agent_preferences.test.ts index d10ffa0912..0a0c04361d 100644 --- a/tests/control_plane_ts/agent_preferences.test.ts +++ b/tests/control_plane_ts/agent_preferences.test.ts @@ -66,7 +66,9 @@ for (const [name, Store] of [["file", FileAuthorityStore], ["sqlite", SqliteAuth } test("known filesystem denial is not missing or corrupt preferences", async t => { - if (process.getuid?.() === 0) { t.skip("root bypasses POSIX read permission"); return; } + if (process.platform === "win32" || process.getuid?.() === 0) { + t.skip("mode 000 read denial needs a non-root POSIX host"); return; + } const root = await mkdtemp(join(tmpdir(), "preferences-permission-")); t.after(() => rm(root, {recursive: true, force: true})); const call = (fields: JsonObject = {}) => agentPreferences({...request, runtime_root: root, ...fields}); diff --git a/tests/control_plane_ts/authority_archive.test.ts b/tests/control_plane_ts/authority_archive.test.ts index 6a0a06ca2d..154b06bcf9 100644 --- a/tests/control_plane_ts/authority_archive.test.ts +++ b/tests/control_plane_ts/authority_archive.test.ts @@ -22,7 +22,12 @@ function sqliteSkipReason(): string | undefined { const sqliteSkip = sqliteSkipReason(); for (const platform of ["win32", "linux"] as const) { - test(`archive publication preserves the ${platform} directory-sync boundary`, async () => { + test(`archive publication preserves the ${platform} directory-sync boundary`, { + // A Windows kernel cannot fsync a directory handle, so the simulated Linux + // boundary is only reproducible on a POSIX host. + skip: platform === "linux" && process.platform === "win32" + ? "the linux directory-sync boundary needs a POSIX host" : false, + }, async () => { const root = await mkdtemp(join(tmpdir(), "authority-archive-")); const source = new FileAuthorityStore(join(root, "source"), "goal"); const descriptor = Object.getOwnPropertyDescriptor(process, "platform")!; diff --git a/tests/control_plane_ts/canonical_task_lease_renew.test.ts b/tests/control_plane_ts/canonical_task_lease_renew.test.ts index 37f81694ac..73c2745a68 100644 --- a/tests/control_plane_ts/canonical_task_lease_renew.test.ts +++ b/tests/control_plane_ts/canonical_task_lease_renew.test.ts @@ -126,7 +126,10 @@ for (const provider of ["file", "sqlite"] as const) { await writeFile(config, JSON.stringify({...command, operation: "acquire", now: acquireNow.toISOString()})); const before = await store.loadAuthority(); const child = spawnSync(process.execPath, ["--no-warnings", "--experimental-sqlite", "--experimental-strip-types", CHILD, config, boundary, "600"], {encoding: "utf8", timeout: 30000}); - assert.equal(child.signal, "SIGKILL", child.stderr); + assert.equal(child.error, undefined); + assert.notEqual(child.status, 0); + // Windows has no POSIX signals; TerminateProcess reports an exit status instead. + if (process.platform !== "win32") assert.equal(child.signal, "SIGKILL", child.stderr); if (boundary === "before") assert.deepEqual(await store.loadAuthority(), before); const result = await executeTaskLeaseAcquire(command, {now: () => acquireNow}); assert.equal(result.ok, true, JSON.stringify(result)); assert.equal(result.status, boundary === "before" ? "applied" : "replayed"); @@ -266,7 +269,10 @@ for (const provider of ["file", "sqlite"] as const) { const config = join(root, "child.json"); await writeFile(config, JSON.stringify({...request, now: NOW.toISOString()})); const before = await store.loadAuthority(); const child = spawnSync(process.execPath, ["--no-warnings", "--experimental-sqlite", "--experimental-strip-types", CHILD, config, boundary, "600"], {encoding: "utf8", timeout: 30000}); - assert.equal(child.signal, "SIGKILL", child.stderr); + assert.equal(child.error, undefined); + assert.notEqual(child.status, 0); + // Windows has no POSIX signals; TerminateProcess reports an exit status instead. + if (process.platform !== "win32") assert.equal(child.signal, "SIGKILL", child.stderr); if (boundary === "before") assert.deepEqual(await store.loadAuthority(), before); const result = await executeTaskLeaseLifecycle(request, {now: () => NOW}); assert.equal(result.ok, true); assert.equal(result.status, boundary === "before" ? "applied" : "replayed"); @@ -315,7 +321,10 @@ for (const provider of ["file", "sqlite"] as const) { const before = await store.loadAuthority(); const child = spawnSync(process.execPath, ["--no-warnings", "--experimental-sqlite", "--experimental-strip-types", CHILD, config, boundary, "600"], {encoding: "utf8", timeout: 30000}); - assert.equal(child.signal, "SIGKILL", child.stderr); + assert.equal(child.error, undefined); + assert.notEqual(child.status, 0); + // Windows has no POSIX signals; TerminateProcess reports an exit status instead. + if (process.platform !== "win32") assert.equal(child.signal, "SIGKILL", child.stderr); if (boundary === "before") assert.deepEqual(await store.loadAuthority(), before); const recovered = await executeTaskLeaseLifecycle(command, {now: () => NOW}); assert.equal(recovered.ok, true, JSON.stringify(recovered)); @@ -355,7 +364,10 @@ for (const provider of ["file", "sqlite"] as const) { const before = await store.loadAuthority(); const child = spawnSync(process.execPath, ["--no-warnings", "--experimental-sqlite", "--experimental-strip-types", CHILD, config, boundary, "600"], {encoding: "utf8", timeout: 30000}); - assert.equal(child.signal, "SIGKILL", child.stderr); + assert.equal(child.error, undefined); + assert.notEqual(child.status, 0); + // Windows has no POSIX signals; TerminateProcess reports an exit status instead. + if (process.platform !== "win32") assert.equal(child.signal, "SIGKILL", child.stderr); if (boundary === "before") assert.deepEqual(await store.loadAuthority(), before); const recovered = await executeTaskLeaseLifecycle(command, {now: () => NOW}); assert.equal(recovered.ok, true, JSON.stringify(recovered)); diff --git a/tests/control_plane_ts/cold_source_import.test.ts b/tests/control_plane_ts/cold_source_import.test.ts index 120b63a317..b76cca9a14 100644 --- a/tests/control_plane_ts/cold_source_import.test.ts +++ b/tests/control_plane_ts/cold_source_import.test.ts @@ -192,6 +192,10 @@ test("a committed original receipt remains readable after backup loss", async t for (const variant of ["expired_active", "orphan_active", "symlink", "unsupported_filename"] as const) { test(`cold import rejects ${variant} lease facts rather than deleting them`, async t => { + if (variant === "symlink" && process.platform === "win32") { + t.skip("file symlink creation needs Windows host privileges"); + return; + } const f = await fixture(t); const directory = join(f.f.root, "goals", "goal-a", "task-leases"); await mkdir(directory, {recursive: true}); diff --git a/tests/control_plane_ts/coordination_runtime_shadow.test.ts b/tests/control_plane_ts/coordination_runtime_shadow.test.ts index dde01b6a34..fc6b3941be 100644 --- a/tests/control_plane_ts/coordination_runtime_shadow.test.ts +++ b/tests/control_plane_ts/coordination_runtime_shadow.test.ts @@ -66,6 +66,10 @@ test("bootstrap is exactly replayable, has no mutation receipt and cannot overwr for (const kind of ["unsupported_name", "file_symlink", "directory_symlink", "parent_symlink", "non_file"] as const) { test(`source admission rejects ${kind} without changing the candidate`, async (t) => { + if (kind === "file_symlink" && process.platform === "win32") { + t.skip("file symlink creation needs Windows host privileges"); + return; + } const f = await fixture(t); const before = await f.store.loadAuthority(); let source = await sourceRequest(f, f.baseline); @@ -80,12 +84,12 @@ for (const kind of ["unsupported_name", "file_symlink", "directory_symlink", "pa if (kind === "non_file") await mkdir(join(directory, "todo_retained.json")); if (kind === "directory_symlink") { await rm(directory, {recursive: true}); - await symlink(outside, directory); + await symlink(outside, directory, process.platform === "win32" ? "junction" : "dir"); } if (kind === "parent_symlink") { const goalDirectory = join(f.root, "goals", "goal-a"); await rm(goalDirectory, {recursive: true}); - await symlink(outside, goalDirectory); + await symlink(outside, goalDirectory, process.platform === "win32" ? "junction" : "dir"); } if (kind !== "non_file") source = await sourceRequest(f, f.baseline); const result = await inspectCoordinationRuntimeShadow({...source, @@ -186,7 +190,8 @@ test("qualification preserves unrecognized residue, symlink partitions and inval await writeFile(cursorPath, Buffer.concat([invalid.subarray(0, offset), Buffer.from([0xff]), invalid.subarray(offset + 3)])); assert.equal((await qualify(f, head)).reason_code, "outbox_file_invalid"); await writeFile(cursorPath, original); - await symlink(directory, join(f.root, "authority-shadow", "outbox", "goal-a", "leases")); + await symlink(directory, join(f.root, "authority-shadow", "outbox", "goal-a", "leases"), + process.platform === "win32" ? "junction" : "dir"); assert.equal((await qualify(f, head)).qualified, false); }); diff --git a/tests/control_plane_ts/effect_runtime_snapshot.test.ts b/tests/control_plane_ts/effect_runtime_snapshot.test.ts index 69d15ebeed..079fe4c99f 100644 --- a/tests/control_plane_ts/effect_runtime_snapshot.test.ts +++ b/tests/control_plane_ts/effect_runtime_snapshot.test.ts @@ -21,12 +21,19 @@ test("local snapshot accepts exact private bytes and rejects altered or unsafe r /snapshot unavailable or invalid/); await assert.rejects(readPrivateJsonSnapshot({...ref, byte_count: bytes.length - 1}), /snapshot unavailable or invalid/); - await chmod(path, 0o644); - await assert.rejects(readPrivateJsonSnapshot(ref), /snapshot unavailable or invalid/); - const link = join(directory, "link.json"); - await symlink(path, link); - await assert.rejects(readPrivateJsonSnapshot({...ref, path: link}), - /snapshot unavailable or invalid/); + // Windows has no POSIX mode denial: the private-mode rejection is only + // observable where process.getuid exists. + if (process.getuid) { + await chmod(path, 0o644); + await assert.rejects(readPrivateJsonSnapshot(ref), /snapshot unavailable or invalid/); + } + // File symlink creation needs Developer Mode or admin on Windows. + if (process.platform !== "win32") { + const link = join(directory, "link.json"); + await symlink(path, link); + await assert.rejects(readPrivateJsonSnapshot({...ref, path: link}), + /snapshot unavailable or invalid/); + } } finally { await rm(directory, {recursive: true, force: true}); } diff --git a/tests/control_plane_ts/goal_acceptance_authority.test.ts b/tests/control_plane_ts/goal_acceptance_authority.test.ts index 53842453ed..31e4fac292 100644 --- a/tests/control_plane_ts/goal_acceptance_authority.test.ts +++ b/tests/control_plane_ts/goal_acceptance_authority.test.ts @@ -31,7 +31,7 @@ const goalA = {goal_id: goal, goal_instance_id: "ginst_aaaaaaaaaaaaaaaaaaaaaaaaa const goalB = {goal_id: goal, goal_instance_id: "ginst_bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}; test("documented owner configuration satisfies the canonical acceptance contract", async () => { const reference = await readFile(new URL("../../docs/reference/goal-acceptance-observations.md", import.meta.url), "utf8"); - const example = reference.match(/```json\n([\s\S]*?)\n```/); + const example = reference.match(/```json\r?\n([\s\S]*?)\r?\n```/); assert.ok(example, "the operation guide must include a runnable configuration"); assert.doesNotThrow(() => normalizeGoalAcceptanceDocument(JSON.parse(example[1]))); }); diff --git a/tests/control_plane_ts/quota_void_commit.test.ts b/tests/control_plane_ts/quota_void_commit.test.ts index 5b13f97fec..9f1e9592ba 100644 --- a/tests/control_plane_ts/quota_void_commit.test.ts +++ b/tests/control_plane_ts/quota_void_commit.test.ts @@ -724,13 +724,17 @@ test("receipt repair rejects paths outside the run directory and symbolic links" ...receipt.value, status: "prepared", }, null, 2)}\n`, "utf8"); - await unlink(String(written.payload.json_path)); - await symlink(outsidePath, String(written.payload.json_path)); - await assert.rejects( - () => evaluateQuotaVoidCommit(params), - /JSON artifact must not be a symbolic link/, - ); - await assert.rejects(() => readFile(outsidePath), /ENOENT/); + // File symlink creation needs Developer Mode or admin on Windows; the + // escaped-path rejection above still covers the outside-run-directory rule. + if (process.platform !== "win32") { + await unlink(String(written.payload.json_path)); + await symlink(outsidePath, String(written.payload.json_path)); + await assert.rejects( + () => evaluateQuotaVoidCommit(params), + /JSON artifact must not be a symbolic link/, + ); + await assert.rejects(() => readFile(outsidePath), /ENOENT/); + } }); test("legacy slot coercion truncates numeric strings and clamps both slot floors", async (t) => { diff --git a/tests/control_plane_ts/shadow_entry_delivery.test.ts b/tests/control_plane_ts/shadow_entry_delivery.test.ts index 64768c74c8..c55eea0917 100644 --- a/tests/control_plane_ts/shadow_entry_delivery.test.ts +++ b/tests/control_plane_ts/shadow_entry_delivery.test.ts @@ -107,6 +107,10 @@ for (const field of ["resolution", "partition_projection", "partition_digest", " for (const changed of ["prepared", "marker", "symlink", "stale_lineage"] as const) { test(`changed ${changed} is rejected without a candidate commit`, async t => { + if (changed === "symlink" && process.platform === "win32") { + t.skip("file symlink creation needs Windows host privileges"); + return; + } const f = await fixture(t); const request = await pendingEntry(f, 1, {handoff_mode: "hard_lease", todos: [todo()]}); const r = selection(request), before = await f.store.loadAuthority(); diff --git a/tests/control_plane_ts/sqlite_backup.test.ts b/tests/control_plane_ts/sqlite_backup.test.ts index bf8e1c26e0..c643e5cabf 100644 --- a/tests/control_plane_ts/sqlite_backup.test.ts +++ b/tests/control_plane_ts/sqlite_backup.test.ts @@ -48,8 +48,11 @@ test("snapshot rejects existing destinations and source aliases without modifyin await assert.rejects(snapshotSqliteDatabase(source, target), /EEXIST/); assert.equal(await readFile(target, "utf8"), "keep existing"); await assert.rejects(snapshotSqliteDatabase(source, source), /distinct absolute/); - const alias = join(directory, "alias"); await symlink(source, alias); - await assert.rejects(snapshotSqliteDatabase(source, alias), /EEXIST/); + // File symlink creation needs Developer Mode or admin on Windows. + if (process.platform !== "win32") { + const alias = join(directory, "alias"); await symlink(source, alias); + await assert.rejects(snapshotSqliteDatabase(source, alias), /EEXIST/); + } assert.deepEqual(await readFile(source), original); }); From 0d26c53913b48944c7ac1ac4432e618e291622a1 Mon Sep 17 00:00:00 2001 From: JasonBuildAI Date: Wed, 14 Oct 2026 08:06:55 +0800 Subject: [PATCH 2/2] test(control-plane): prove the crash child reached the commit stage The Windows branch of the process-death oracle accepted any non-zero exit, so a child that failed before the operation also satisfied "crash before commit" while the parent performed the first operation itself. The child now writes a `.stage` marker at the exact before/after commit fault point before it self-terminates, and the four guards share `assertChildCrashedAtCommitBoundary`, which reads the marker back before it accepts the exit status and signal. A new negative case rejects a start-up failure, a normal exit, a spawn timeout and the other boundary stage. Refs #6351 Signed-off-by: JasonBuildAI --- .../canonical_task_lease_renew.test.ts | 63 ++++++++++++++----- .../canonical_task_lease_renew_process.ts | 13 +++- 2 files changed, 56 insertions(+), 20 deletions(-) diff --git a/tests/control_plane_ts/canonical_task_lease_renew.test.ts b/tests/control_plane_ts/canonical_task_lease_renew.test.ts index 73c2745a68..5ddc21ee0a 100644 --- a/tests/control_plane_ts/canonical_task_lease_renew.test.ts +++ b/tests/control_plane_ts/canonical_task_lease_renew.test.ts @@ -2,9 +2,10 @@ import {executeTaskLeaseAcquire} from "../../loopx/control_plane/work_items/task import assert from "node:assert/strict"; import {createHash} from "node:crypto"; import {mkdtemp, writeFile, rm, access} from "node:fs/promises"; +import {readFileSync} from "node:fs"; import {tmpdir} from "node:os"; import {join} from "node:path"; -import {spawn, spawnSync} from "node:child_process"; +import {spawn, spawnSync, type SpawnSyncReturns} from "node:child_process"; import {fileURLToPath} from "node:url"; import test, {type TestContext} from "node:test"; import {executeTaskLeaseLifecycle, TASK_LEASE_LIFECYCLE_REQUEST_SCHEMA_VERSION} from "../../loopx/control_plane/work_items/task_lease_lifecycle.ts"; @@ -26,6 +27,21 @@ import {atomicWriteJson} from "../../loopx/control_plane/effect_runtime_io.ts"; const NOW = new Date("2026-09-13T10:05:00Z"); const CHILD = fileURLToPath(new URL("./canonical_task_lease_renew_process.ts", import.meta.url)); +// The crash oracle must prove the child reached the requested commit boundary: +// module-load, config-read and other start-up failures also exit non-zero. +// POSIX keeps the self-delivered SIGKILL proof; Windows reports a +// TerminateProcess status instead, so the child writes a stage marker at each +// fault point and the parent reads it back before accepting the exit result. +function assertChildCrashedAtCommitBoundary(child: SpawnSyncReturns, boundary: "before" | "after", configPath: string): void { + assert.equal(child.error, undefined, child.error?.message); + let stage: string | undefined; + try { stage = readFileSync(`${configPath}.stage`, "utf8"); } catch { stage = undefined; } + assert.equal(stage, boundary, stage === undefined + ? `child never reached the ${boundary}-commit fault point (stderr: ${child.stderr})` + : `child died at stage "${stage}", not the ${boundary}-commit fault point (stderr: ${child.stderr})`); + assert.notEqual(child.status, 0); + if (process.platform !== "win32") assert.equal(child.signal, "SIGKILL", child.stderr); +} test("lifecycle identity diagnostics follow the invalid field, not error wording", async () => { for (const [fields, code] of [ [{goal_id: 42, todo_id: "todo_valid"}, "invalid_goal_id"], @@ -126,10 +142,7 @@ for (const provider of ["file", "sqlite"] as const) { await writeFile(config, JSON.stringify({...command, operation: "acquire", now: acquireNow.toISOString()})); const before = await store.loadAuthority(); const child = spawnSync(process.execPath, ["--no-warnings", "--experimental-sqlite", "--experimental-strip-types", CHILD, config, boundary, "600"], {encoding: "utf8", timeout: 30000}); - assert.equal(child.error, undefined); - assert.notEqual(child.status, 0); - // Windows has no POSIX signals; TerminateProcess reports an exit status instead. - if (process.platform !== "win32") assert.equal(child.signal, "SIGKILL", child.stderr); + assertChildCrashedAtCommitBoundary(child, boundary, config); if (boundary === "before") assert.deepEqual(await store.loadAuthority(), before); const result = await executeTaskLeaseAcquire(command, {now: () => acquireNow}); assert.equal(result.ok, true, JSON.stringify(result)); assert.equal(result.status, boundary === "before" ? "applied" : "replayed"); @@ -269,10 +282,7 @@ for (const provider of ["file", "sqlite"] as const) { const config = join(root, "child.json"); await writeFile(config, JSON.stringify({...request, now: NOW.toISOString()})); const before = await store.loadAuthority(); const child = spawnSync(process.execPath, ["--no-warnings", "--experimental-sqlite", "--experimental-strip-types", CHILD, config, boundary, "600"], {encoding: "utf8", timeout: 30000}); - assert.equal(child.error, undefined); - assert.notEqual(child.status, 0); - // Windows has no POSIX signals; TerminateProcess reports an exit status instead. - if (process.platform !== "win32") assert.equal(child.signal, "SIGKILL", child.stderr); + assertChildCrashedAtCommitBoundary(child, boundary, config); if (boundary === "before") assert.deepEqual(await store.loadAuthority(), before); const result = await executeTaskLeaseLifecycle(request, {now: () => NOW}); assert.equal(result.ok, true); assert.equal(result.status, boundary === "before" ? "applied" : "replayed"); @@ -321,10 +331,7 @@ for (const provider of ["file", "sqlite"] as const) { const before = await store.loadAuthority(); const child = spawnSync(process.execPath, ["--no-warnings", "--experimental-sqlite", "--experimental-strip-types", CHILD, config, boundary, "600"], {encoding: "utf8", timeout: 30000}); - assert.equal(child.error, undefined); - assert.notEqual(child.status, 0); - // Windows has no POSIX signals; TerminateProcess reports an exit status instead. - if (process.platform !== "win32") assert.equal(child.signal, "SIGKILL", child.stderr); + assertChildCrashedAtCommitBoundary(child, boundary, config); if (boundary === "before") assert.deepEqual(await store.loadAuthority(), before); const recovered = await executeTaskLeaseLifecycle(command, {now: () => NOW}); assert.equal(recovered.ok, true, JSON.stringify(recovered)); @@ -364,10 +371,7 @@ for (const provider of ["file", "sqlite"] as const) { const before = await store.loadAuthority(); const child = spawnSync(process.execPath, ["--no-warnings", "--experimental-sqlite", "--experimental-strip-types", CHILD, config, boundary, "600"], {encoding: "utf8", timeout: 30000}); - assert.equal(child.error, undefined); - assert.notEqual(child.status, 0); - // Windows has no POSIX signals; TerminateProcess reports an exit status instead. - if (process.platform !== "win32") assert.equal(child.signal, "SIGKILL", child.stderr); + assertChildCrashedAtCommitBoundary(child, boundary, config); if (boundary === "before") assert.deepEqual(await store.loadAuthority(), before); const recovered = await executeTaskLeaseLifecycle(command, {now: () => NOW}); assert.equal(recovered.ok, true, JSON.stringify(recovered)); @@ -402,3 +406,28 @@ for (const provider of ["file", "sqlite"] as const) { }); } } + +test("the commit-boundary crash guard rejects early exit, normal exit, timeout and wrong stage", async t => { + const root = await mkdtemp(join(tmpdir(), "loopx-crash-guard-")); + t.after(() => rm(root, {recursive: true, force: true})); + const config = join(root, "child.json"); + const flags = ["--no-warnings", "--experimental-sqlite", "--experimental-strip-types"]; + // A start-up failure exits non-zero without ever reaching the commit boundary. + const missingConfig = join(root, "missing.json"); + const startup = spawnSync(process.execPath, [...flags, CHILD, missingConfig, "before", "600"], {encoding: "utf8", timeout: 30000}); + assert.notEqual(startup.status, 0); + assert.throws(() => assertChildCrashedAtCommitBoundary(startup, "before", missingConfig), + /never reached the before-commit fault point/); + // A normal exit, a spawn timeout and the other boundary must not satisfy the crash guard. + const normal = spawnSync(process.execPath, ["-e", "process.exit(0)"], {encoding: "utf8", timeout: 30000}); + assert.equal(normal.status, 0); + assert.throws(() => assertChildCrashedAtCommitBoundary(normal, "before", config), + /never reached the before-commit fault point/); + const hung = spawnSync(process.execPath, ["-e", "setTimeout(() => {}, 60000)"], {encoding: "utf8", timeout: 1500}); + assert.equal((hung.error as NodeJS.ErrnoException | undefined)?.code, "ETIMEDOUT"); + assert.throws(() => assertChildCrashedAtCommitBoundary(hung, "before", config), /ETIMEDOUT/); + await writeFile(`${config}.stage`, "after"); + const synthetic: SpawnSyncReturns = {pid: 1, output: [], stdout: "", stderr: "", + status: process.platform === "win32" ? 1 : null, signal: process.platform === "win32" ? null : "SIGKILL", error: undefined}; + assert.throws(() => assertChildCrashedAtCommitBoundary(synthetic, "before", config), /died at stage "after"/); +}); diff --git a/tests/control_plane_ts/canonical_task_lease_renew_process.ts b/tests/control_plane_ts/canonical_task_lease_renew_process.ts index 4afd0c6b82..7bf735a8ad 100644 --- a/tests/control_plane_ts/canonical_task_lease_renew_process.ts +++ b/tests/control_plane_ts/canonical_task_lease_renew_process.ts @@ -1,11 +1,12 @@ import {executeCanonicalTaskLeaseAcquire} from "../../loopx/control_plane/coordination/task_lease_acquire.ts"; /** Disposable child for acquisition/lifecycle crash and race qualification. */ -import {readFileSync} from "node:fs"; +import {readFileSync, writeFileSync} from "node:fs"; import {openLocalAuthorityStore} from "../../loopx/control_plane/coordination/local_authority_provider.ts"; import {executeCanonicalTaskLeaseLifecycle} from "../../loopx/control_plane/coordination/task_lease_lifecycle.ts"; import type {AuthorityStore} from "../../loopx/control_plane/coordination/authority_store.ts"; const [path, mode, ttl] = process.argv.slice(2); const request = JSON.parse(readFileSync(path!, "utf8")); +const stageMarkerPath = `${path}.stage`; const store = await openLocalAuthorityStore(request.runtime_root, request.goal_id); let release: () => void = () => {}; const barrier = new Promise(resolve => {release = resolve;}); @@ -19,9 +20,15 @@ const measured: AuthorityStore = { return head; }, commitAuthority: async input => { - if (mode === "before") process.kill(process.pid, "SIGKILL"); + if (mode === "before") { + writeFileSync(stageMarkerPath, mode); + process.kill(process.pid, "SIGKILL"); + } const result = await store.commitAuthority(input); - if (mode === "after" && result.status === "applied") process.kill(process.pid, "SIGKILL"); + if (mode === "after" && result.status === "applied") { + writeFileSync(stageMarkerPath, mode); + process.kill(process.pid, "SIGKILL"); + } return result; }, };