From baa2ada6286f66b49e8d3180477d17d2a6e6e7a5 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Mon, 7 Sep 2026 08:34:40 -0400 Subject: [PATCH 1/5] test: isolate runtime component checks --- tests/agent-runtime-components.test.ts | 3 +++ tests/host-command-executor.test.ts | 18 +++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/tests/agent-runtime-components.test.ts b/tests/agent-runtime-components.test.ts index b9dd28b84..249530546 100644 --- a/tests/agent-runtime-components.test.ts +++ b/tests/agent-runtime-components.test.ts @@ -14,6 +14,9 @@ const originalContainedRuntimeComponentPaths = process.env.CONTAINED_RUNTIME_COM try { chdir(root) + // This case verifies discovery from the current workspace, not a configured + // deployment path inherited by the test runner. + delete process.env.WP_CODEBOX_AGENTS_API_PATH const runtimeHost = join(root, "runtime-host") const agentsApi = join(runtimeHost, "vendor", "wordpress", "agents-api") diff --git a/tests/host-command-executor.test.ts b/tests/host-command-executor.test.ts index c18c7648c..09bcd9909 100644 --- a/tests/host-command-executor.test.ts +++ b/tests/host-command-executor.test.ts @@ -59,7 +59,7 @@ const processTreeTimedOut = await executeHostCommand( assert.equal(processTreeTimedOut.failureClassification, "timeout") const grandchildPid = Number.parseInt(await readFile(grandchildPidFile, "utf8"), 10) await sleep(150) -assert.equal(isProcessRunning(grandchildPid), false) +assert.equal(await isProcessRunning(grandchildPid), false) const nonZero = await executeHostCommand( { @@ -76,7 +76,7 @@ const artifactsDirectory = join(root, "artifacts") const withArtifacts = await executeHostCommand( { command: process.execPath, - args: ["-e", "process.stdout.write('out'); process.stderr.write('err'); setTimeout(() => {}, 75)"], + args: ["-e", "process.stdout.write('out'); process.stderr.write('err'); setTimeout(() => {}, 250)"], cwd: allowed, artifactsDirectory, memorySampleIntervalMs: 20, @@ -132,10 +132,22 @@ async function sleep(ms: number): Promise { await new Promise((resolve) => setTimeout(resolve, ms)) } -function isProcessRunning(pid: number): boolean { +async function isProcessRunning(pid: number): Promise { try { process.kill(pid, 0) + } catch { + return false + } + + if (process.platform !== "linux") { return true + } + + // A timeout-killed descendant can remain as a zombie briefly while its new + // parent reaps it. It is already terminated and cannot execute further. + try { + const stat = await readFile(`/proc/${pid}/stat`, "utf8") + return stat.slice(stat.lastIndexOf(")") + 2, stat.lastIndexOf(")") + 3) !== "Z" } catch { return false } From 89d8ce00fb90da31dee9f2a15d300bb104ddb168 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Mon, 7 Sep 2026 09:17:52 -0400 Subject: [PATCH 2/5] fix: sample host process after spawn --- packages/runtime-core/src/host-command-executor.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/runtime-core/src/host-command-executor.ts b/packages/runtime-core/src/host-command-executor.ts index 0de5e15ef..7e1fab00e 100644 --- a/packages/runtime-core/src/host-command-executor.ts +++ b/packages/runtime-core/src/host-command-executor.ts @@ -116,6 +116,9 @@ export async function executeHostCommand(config: HostCommandExecutorConfig, inpu void task.finally(() => memorySampleTasks.delete(task)) } sampleMemory() + // The immediate sample can race process-group creation; sample once more + // after Node confirms the child has spawned. + child.once("spawn", sampleMemory) const memoryTimer = setInterval(sampleMemory, memorySampleIntervalMs) child.stdout?.on("data", (chunk: Buffer) => { From 0c8395c91e512cf943e283c8e838c1cb5090ae3c Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Mon, 7 Sep 2026 09:38:29 -0400 Subject: [PATCH 3/5] test: synchronize host memory sampling --- .../runtime-core/src/host-command-executor.ts | 9 +++- tests/host-command-executor.test.ts | 44 +++++++++++++++++-- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/packages/runtime-core/src/host-command-executor.ts b/packages/runtime-core/src/host-command-executor.ts index 7e1fab00e..de0111b12 100644 --- a/packages/runtime-core/src/host-command-executor.ts +++ b/packages/runtime-core/src/host-command-executor.ts @@ -14,6 +14,7 @@ export interface HostCommandExecutorConfig { maxOutputBytes?: number artifactsDirectory?: string memorySampleIntervalMs?: number + onMemorySample?: (sample: HostCommandMemorySample) => void terminationGraceMs?: number inheritedEnv?: string[] allowedInputEnv?: string[] @@ -109,7 +110,13 @@ export async function executeHostCommand(config: HostCommandExecutorConfig, inpu } const task = sampleHostCommandProcessTreeRssBytes(child.pid).then((rssBytes) => { if (rssBytes !== undefined) { - memorySamples.push({ elapsedMs: Date.now() - started, rssBytes }) + const sample = { elapsedMs: Date.now() - started, rssBytes } + memorySamples.push(sample) + try { + config.onMemorySample?.(sample) + } catch { + // Observation callbacks must not affect command execution. + } } }).catch(() => undefined) memorySampleTasks.add(task) diff --git a/tests/host-command-executor.test.ts b/tests/host-command-executor.test.ts index 09bcd9909..a045bbe55 100644 --- a/tests/host-command-executor.test.ts +++ b/tests/host-command-executor.test.ts @@ -1,6 +1,7 @@ import assert from "node:assert/strict" -import { mkdir, readFile, realpath } from "node:fs/promises" -import { join } from "node:path" +import { watch } from "node:fs" +import { access, mkdir, readFile, realpath, writeFile } from "node:fs/promises" +import { basename, dirname, join } from "node:path" import { classifyHostCommandFailure, executeHostCommand, executeManagedHostCommand, hostCommandEnv, ManagedHostCommandError, resolveAllowedHostCommandCwd } from "../packages/runtime-core/src/index.js" import { assertJsonFile, assertTextFile, withTempDir } from "../scripts/test-kit.js" @@ -73,16 +74,34 @@ assert.equal(nonZero.exitCode, 9) assert.equal(nonZero.failureClassification, "non_zero_exit") const artifactsDirectory = join(root, "artifacts") -const withArtifacts = await executeHostCommand( +const samplerReadyPath = join(root, "memory-sampler-ready") +const samplerReleasePath = join(root, "memory-sampler-release") +let recordMemorySample: (() => void) | undefined +const memorySampleRecorded = new Promise((resolve) => { + recordMemorySample = resolve +}) +const withArtifactsPromise = executeHostCommand( { command: process.execPath, - args: ["-e", "process.stdout.write('out'); process.stderr.write('err'); setTimeout(() => {}, 250)"], + args: ["-e", `const { existsSync, watch, writeFileSync } = require("node:fs"); const readyPath = ${JSON.stringify(samplerReadyPath)}; const releasePath = ${JSON.stringify(samplerReleasePath)}; const finish = () => { if (existsSync(releasePath)) process.exit(0) }; process.stdout.write("out"); process.stderr.write("err"); writeFileSync(readyPath, "ready"); const watcher = watch(${JSON.stringify(root)}, finish); finish();`], cwd: allowed, artifactsDirectory, memorySampleIntervalMs: 20, + timeoutMs: 2_000, + onMemorySample: () => recordMemorySample?.(), }, {} ) +await waitForFile(samplerReadyPath) +await Promise.race([ + memorySampleRecorded, + withArtifactsPromise.then( + () => Promise.reject(new Error("host command exited before recording a memory sample")), + (error: unknown) => Promise.reject(error), + ), +]) +await writeFile(samplerReleasePath, "release") +const withArtifacts = await withArtifactsPromise assert.equal(withArtifacts.stdout, "out") assert.equal(withArtifacts.stderr, "err") assert.ok(withArtifacts.artifacts?.stdout?.path.endsWith("stdout.log")) @@ -132,6 +151,23 @@ async function sleep(ms: number): Promise { await new Promise((resolve) => setTimeout(resolve, ms)) } +async function waitForFile(path: string): Promise { + await new Promise((resolve, reject) => { + const watcher = watch(dirname(path), (_event, filename) => { + if (filename !== basename(path)) { + return + } + watcher.close() + resolve() + }) + watcher.once("error", reject) + void access(path).then(() => { + watcher.close() + resolve() + }).catch(() => undefined) + }) +} + async function isProcessRunning(pid: number): Promise { try { process.kill(pid, 0) From c52fa6426e55a7e9e7431c01c2cd8ee3a11a8929 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Mon, 7 Sep 2026 09:48:04 -0400 Subject: [PATCH 4/5] test: bound host memory sampling fixture --- .../runtime-core/src/host-command-executor.ts | 6 --- tests/host-command-executor.test.ts | 45 +++---------------- 2 files changed, 6 insertions(+), 45 deletions(-) diff --git a/packages/runtime-core/src/host-command-executor.ts b/packages/runtime-core/src/host-command-executor.ts index de0111b12..bb81d3438 100644 --- a/packages/runtime-core/src/host-command-executor.ts +++ b/packages/runtime-core/src/host-command-executor.ts @@ -14,7 +14,6 @@ export interface HostCommandExecutorConfig { maxOutputBytes?: number artifactsDirectory?: string memorySampleIntervalMs?: number - onMemorySample?: (sample: HostCommandMemorySample) => void terminationGraceMs?: number inheritedEnv?: string[] allowedInputEnv?: string[] @@ -112,11 +111,6 @@ export async function executeHostCommand(config: HostCommandExecutorConfig, inpu if (rssBytes !== undefined) { const sample = { elapsedMs: Date.now() - started, rssBytes } memorySamples.push(sample) - try { - config.onMemorySample?.(sample) - } catch { - // Observation callbacks must not affect command execution. - } } }).catch(() => undefined) memorySampleTasks.add(task) diff --git a/tests/host-command-executor.test.ts b/tests/host-command-executor.test.ts index a045bbe55..e3a1dfb32 100644 --- a/tests/host-command-executor.test.ts +++ b/tests/host-command-executor.test.ts @@ -1,7 +1,6 @@ import assert from "node:assert/strict" -import { watch } from "node:fs" -import { access, mkdir, readFile, realpath, writeFile } from "node:fs/promises" -import { basename, dirname, join } from "node:path" +import { mkdir, readFile, realpath } from "node:fs/promises" +import { join } from "node:path" import { classifyHostCommandFailure, executeHostCommand, executeManagedHostCommand, hostCommandEnv, ManagedHostCommandError, resolveAllowedHostCommandCwd } from "../packages/runtime-core/src/index.js" import { assertJsonFile, assertTextFile, withTempDir } from "../scripts/test-kit.js" @@ -74,36 +73,21 @@ assert.equal(nonZero.exitCode, 9) assert.equal(nonZero.failureClassification, "non_zero_exit") const artifactsDirectory = join(root, "artifacts") -const samplerReadyPath = join(root, "memory-sampler-ready") -const samplerReleasePath = join(root, "memory-sampler-release") -let recordMemorySample: (() => void) | undefined -const memorySampleRecorded = new Promise((resolve) => { - recordMemorySample = resolve -}) -const withArtifactsPromise = executeHostCommand( +const withArtifacts = await executeHostCommand( { command: process.execPath, - args: ["-e", `const { existsSync, watch, writeFileSync } = require("node:fs"); const readyPath = ${JSON.stringify(samplerReadyPath)}; const releasePath = ${JSON.stringify(samplerReleasePath)}; const finish = () => { if (existsSync(releasePath)) process.exit(0) }; process.stdout.write("out"); process.stderr.write("err"); writeFileSync(readyPath, "ready"); const watcher = watch(${JSON.stringify(root)}, finish); finish();`], + args: ["-e", "process.stdout.write('out'); process.stderr.write('err'); setInterval(() => {}, 1000)"], cwd: allowed, artifactsDirectory, memorySampleIntervalMs: 20, timeoutMs: 2_000, - onMemorySample: () => recordMemorySample?.(), }, {} ) -await waitForFile(samplerReadyPath) -await Promise.race([ - memorySampleRecorded, - withArtifactsPromise.then( - () => Promise.reject(new Error("host command exited before recording a memory sample")), - (error: unknown) => Promise.reject(error), - ), -]) -await writeFile(samplerReleasePath, "release") -const withArtifacts = await withArtifactsPromise assert.equal(withArtifacts.stdout, "out") assert.equal(withArtifacts.stderr, "err") +assert.equal(withArtifacts.timedOut, true) +assert.equal(withArtifacts.failureClassification, "timeout") assert.ok(withArtifacts.artifacts?.stdout?.path.endsWith("stdout.log")) assert.ok(withArtifacts.artifacts?.stderr?.path.endsWith("stderr.log")) assert.ok(withArtifacts.artifacts?.summary?.path.endsWith("command-summary.json")) @@ -151,23 +135,6 @@ async function sleep(ms: number): Promise { await new Promise((resolve) => setTimeout(resolve, ms)) } -async function waitForFile(path: string): Promise { - await new Promise((resolve, reject) => { - const watcher = watch(dirname(path), (_event, filename) => { - if (filename !== basename(path)) { - return - } - watcher.close() - resolve() - }) - watcher.once("error", reject) - void access(path).then(() => { - watcher.close() - resolve() - }).catch(() => undefined) - }) -} - async function isProcessRunning(pid: number): Promise { try { process.kill(pid, 0) From 962ca314c6abab45fed1655c32b3d843aa3e5938 Mon Sep 17 00:00:00 2001 From: Chris Huber Date: Mon, 7 Sep 2026 09:50:02 -0400 Subject: [PATCH 5/5] test: expect timed host artifact summary --- tests/host-command-executor.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/host-command-executor.test.ts b/tests/host-command-executor.test.ts index e3a1dfb32..fe562e603 100644 --- a/tests/host-command-executor.test.ts +++ b/tests/host-command-executor.test.ts @@ -95,7 +95,7 @@ await assertTextFile(withArtifacts.artifacts!.stdout!.path, "out") await assertTextFile(withArtifacts.artifacts!.stderr!.path, "err") const artifactSummary = await assertJsonFile<{ schema: string, failureClassification: string, memorySamples: unknown[] }>(withArtifacts.artifacts!.summary!.path) assert.equal(artifactSummary.schema, "wp-codebox/host-command-summary/v1") -assert.equal(artifactSummary.failureClassification, "none") +assert.equal(artifactSummary.failureClassification, "timeout") assert.ok(Array.isArray(artifactSummary.memorySamples)) assert.ok(withArtifacts.memorySamples.length > 0) assert.ok(withArtifacts.peakRssBytes > 0)