test(process): stress real forkpty cleanup

This commit is contained in:
N0zoM1z0 2026-07-14 21:32:17 +00:00
parent f762790e30
commit dc9bdb0175
3 changed files with 111 additions and 0 deletions

View File

@ -0,0 +1,33 @@
name: Process cleanup stress
on:
pull_request:
paths:
- ".github/workflows/process-cleanup-stress.yml"
- "scripts/verify-process-cleanup-stress.mjs"
- "src/application/ProcessCaptureLifecycle.ts"
- "src/application/ProcessHarness.ts"
- "src/application/ProcessOwnership.ts"
- "tests/fixtures/processFidelity.mjs"
- "package.json"
- "package-lock.json"
schedule:
- cron: "17 4 * * 1"
workflow_dispatch:
permissions:
contents: read
jobs:
real-forkpty-cleanup:
runs-on: ubuntu-latest
timeout-minutes: 10
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run verify:process:stress

View File

@ -71,6 +71,7 @@
"typecheck": "tsc --noEmit",
"verify:hopper": "npm run build && node scripts/verify-real-hopper.mjs",
"verify:browser": "npm run build && node scripts/verify-real-browser.mjs",
"verify:process:stress": "npm run build && node scripts/verify-process-cleanup-stress.mjs",
"verify:hopper:linux": "npm run build && node scripts/verify-real-hopper-linux.mjs",
"verify:setup:linux": "npm run build && node scripts/verify-linux-setup-e2e.mjs",
"build:fixtures": "node scripts/build-conformance-fixtures.mjs",

View File

@ -0,0 +1,77 @@
#!/usr/bin/env node
import { execFile } from "node:child_process";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import {
captureProcessScenario,
probeProcessCaptureCapability,
} from "../dist/application/ProcessHarness.js";
import { parseProcessScenario } from "../dist/domain/processScenario.js";
const execFileAsync = promisify(execFile);
const fixture = fileURLToPath(
new URL("../tests/fixtures/processFidelity.mjs", import.meta.url),
);
const iterations = Number.parseInt(
process.env.REA_PROCESS_STRESS_RUNS ?? "25",
10,
);
if (process.platform !== "linux")
throw new Error("Real forkpty cleanup stress verification requires Linux");
if (!Number.isSafeInteger(iterations) || iterations < 1 || iterations > 250)
throw new Error("REA_PROCESS_STRESS_RUNS must be an integer from 1 to 250");
const capability = await probeProcessCaptureCapability();
if (!capability.available)
throw new Error(`Process capture unavailable: ${capability.reason}`);
for (let iteration = 1; iteration <= iterations; iteration += 1) {
const result = await captureProcessScenario(
parseProcessScenario({
approved: true,
executable: process.execPath,
arguments: [fixture, "tree"],
working_directory: dirname(fixture),
timeout_ms: 1_000,
idle_timeout_ms: 2_000,
settle_ms: 50,
}),
{
enabled: true,
executableRoots: [dirname(process.execPath)],
workingRoots: [dirname(fixture)],
allowedEnvironment: [],
allowExternalNetwork: true,
},
);
if (!result.ok)
throw new Error(`forkpty cleanup run ${String(iteration)} failed`, {
cause: result.error,
});
if (
result.value.cleanup.owned_process_group !== "verified" ||
result.value.cleanup.temporary_root !== "removed"
)
throw new Error(`forkpty cleanup run ${String(iteration)} was incomplete`);
const commands = result.value.process_samples.map(({ command }) => command);
if (!commands.some((command) => command.includes("tree-child")))
throw new Error(
`forkpty cleanup run ${String(iteration)} missed its child`,
);
const { stdout } = await execFileAsync("ps", ["-axo", "command="]);
if (
stdout.includes(`${fixture} tree-child`) ||
stdout.includes(`${fixture} tree-grandchild`)
)
throw new Error(
`forkpty cleanup run ${String(iteration)} leaked a process`,
);
}
process.stdout.write(
`${JSON.stringify({ platform: process.platform, iterations, verified: true })}\n`,
);