Files
rea/tests/process-global/cleanLinuxHopperRuntime.test.ts
7ad75bdc80 fix: restore JavaScript analysis and current regression checks (#3080)
Restore source-position-aware ambient aliases, scalar env/argv reads, candidate
endpoint worker data, Chrome DOMSnapshot empty strings, and accumulated Ghidra
LaunchSupport diagnostics. Align regression fixtures with declared current
contracts, while preserving identity, provenance, cancellation, and cleanup
checks. Assert workflow caveats independently at both established locations.

Validation: original five source failures reproduced on main; 307 focused tests
passed with one platform skip, 22 fixture follow-up tests and 22 workflow/MCP
tests passed; full static and docs checks passed. Real public http-server CLI
analyses preserve identical artifact identity, statistics, and 581-token selected
output. Real Chromium confirms restored DOM form destinations; isolated real
service-worker verification passes on main and the change. The complete local
browser run retained an unclassified later marker timeout; current-head CI
provides complete real-browser and recovery lane results. Native IDA/Ghidra
execution and complete local suite execution are not claimed.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: N0zoM1z0 <161784452+N0zoM1z0@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:17:54 +08:00

141 lines
5.1 KiB
TypeScript

import { execFile } from "node:child_process";
import { randomUUID } from "node:crypto";
import {
chmod,
copyFile,
mkdir,
readFile,
symlink,
unlink,
} from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { expect, it } from "vitest";
import { assertCleanLinuxHopperRuntime } from "../../scripts/lib/clean-linux-hopper-runtime.mjs";
import { completeVerifierRun } from "../../scripts/lib/verifier-run.mjs";
import { cleanupOwnedProcessGroup } from "../../src/process/ProcessOwnership.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
const execFileAsync = promisify(execFile);
it
.runIf(process.platform === "linux")
.each(["demo helper", "alternate launcher"])(
"rejects a reparented %s without signaling it",
async (mode) => {
const runId = randomUUID();
const childCode =
"process.title = 'Xvfb'; setInterval(() => {}, 1000); process.send('ready');";
const directory = await createTestTempDirectory("rea-clean-runtime-test-");
const launcher = join(directory, "Hopper");
const alternate = join(directory, "Hopper-alternate");
const procRoot = join(directory, "proc");
await mkdir(procRoot);
const alternateMode = mode === "alternate launcher";
if (alternateMode) {
await copyFile("/usr/bin/sleep", launcher);
await chmod(launcher, 0o700);
await symlink(launcher, alternate);
}
// A rejection must come from the fixture we are about to launch. An
// existing Xvfb would otherwise mask a broken executable-identity check.
// The copied launcher has a unique file identity and has not run yet.
// The synthetic proc root isolates the positive identity check from
// unrelated restricted host processes while retaining display-name checks.
await assertCleanLinuxHopperRuntime(
alternateMode ? [launcher] : [],
procRoot,
);
const command = alternateMode ? alternate : process.execPath;
const arguments_ = alternateMode ? ["30"] : ["-e", childCode];
const parentCode = `
const { spawn } = require('node:child_process');
const child = spawn(${JSON.stringify(command)}, ${JSON.stringify(arguments_)}, {
detached: true, stdio: ${JSON.stringify(alternateMode ? "ignore" : ["ignore", "ignore", "ignore", "ipc"])}
});
child.once('error', () => process.exit(1));
child.once(${JSON.stringify(alternateMode ? "spawn" : "message")}, () => {
process.stdout.write(JSON.stringify({ pid: child.pid, parentPid: process.pid }));
if (child.connected) child.disconnect();
child.unref();
});
`;
const parent = execFileAsync(process.execPath, ["-e", parentCode], {
env: { ...process.env, REA_PROCESS_RUN_ID: runId },
timeout: 10_000,
});
const parentPid = parent.child.pid;
if (parentPid === undefined)
throw new Error("Fixture parent did not spawn");
let orphanPid: number | undefined;
try {
const { stdout } = await parent;
const { pid } = JSON.parse(stdout) as { pid: number };
orphanPid = pid;
// The runtime check reads only the supplied proc root, so expose the
// fixture there in both modes.
const processEntry = join(procRoot, String(pid));
await mkdir(processEntry);
for (const name of ["cmdline", "comm", "exe"])
await symlink(`/proc/${String(pid)}/${name}`, join(processEntry, name));
const { stdout: coordinates } = await execFileAsync("ps", [
"-p",
String(pid),
"-o",
"ppid=,pgid=",
]);
const [currentParent, group] = coordinates
.trim()
.split(/\s+/u)
.map(Number);
expect(currentParent).not.toBe(parentPid);
expect(group).toBe(pid);
const lineage = await completeVerifierRun({
run_id: runId,
verifier_pid: process.pid,
parent_pid: process.ppid,
});
expect(lineage.process_lineage.status).toBe("verified");
if (lineage.process_lineage.status === "verified")
expect(
lineage.process_lineage.descendants.some((p) => p.pid === pid),
).toBe(false);
if (alternateMode) {
const commandLine = await readFile(
`/proc/${String(pid)}/cmdline`,
"utf8",
);
expect(commandLine.split("\0")[0]).toBe(alternate);
await unlink(alternate);
}
await expect(
assertCleanLinuxHopperRuntime(
alternateMode ? [launcher] : [],
procRoot,
),
).rejects.toThrow("Clean Linux runner retained a Hopper demo process");
// Observation must not terminate the fixture, even though it resembles Xvfb.
const { stdout: alive } = await execFileAsync("ps", [
"-p",
String(pid),
"-o",
"stat=",
]);
expect(alive.trim()).not.toMatch(/^Z/u);
} finally {
if (orphanPid !== undefined) {
const cleanup = await cleanupOwnedProcessGroup({
runId,
leaderPid: orphanPid,
processGroupId: orphanPid,
});
expect(cleanup.cleaned, JSON.stringify(cleanup)).toBe(true);
}
}
},
);