mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
d9660eb032
Join canonical owned command cleanup before releasing package verifier workspaces. Replace raw package command wrappers, the updater supervisor, WASM cleanup composite, and boolean-only retention with explicit capability ownership. Preserve selected environments, npm arguments, no-deadline calls, clean exit status, and distinct aggregate/per-stream output budgets. Exercise actual timeout descendants, cancellation, retained cleanup retry, and workspace admission.
231 lines
8.9 KiB
TypeScript
231 lines
8.9 KiB
TypeScript
import { readFile, writeFile } from "node:fs/promises";
|
|
import { join, resolve } from "node:path";
|
|
import { setTimeout as delay } from "node:timers/promises";
|
|
import { pathToFileURL } from "node:url";
|
|
import { expect } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
import { liveProcesses } from "../../../src/process/ProcessOwnershipProcessTree.js";
|
|
import { systemProcessOwnershipHost } from "../../../src/process/ProcessOwnershipObservation.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { processTest } from "../../support/process/processFixture.js";
|
|
|
|
const descendantSource = `
|
|
import { appendFileSync, existsSync, writeFileSync } from "node:fs";
|
|
const record = (entry) => appendFileSync(process.env.OWNER_EVENTS, JSON.stringify(entry) + "\\n");
|
|
const finish = (reason) => {
|
|
record({ kind: "descendant-exit", reason, rootExists: existsSync(process.env.OWNER_ROOT) });
|
|
process.exit(0);
|
|
};
|
|
process.on("SIGTERM", () => finish("SIGTERM"));
|
|
writeFileSync(process.env.OWNER_DESCENDANT, String(process.pid));
|
|
process.send({ pid: process.pid });
|
|
setInterval(() => {
|
|
if (existsSync(process.env.OWNER_RELEASE)) finish("fixture-release");
|
|
}, 25);
|
|
setTimeout(() => finish("fixture-deadline"), 20000);
|
|
`;
|
|
|
|
const npmSource = `
|
|
import { spawn } from "node:child_process";
|
|
import { appendFileSync, existsSync, writeFileSync } from "node:fs";
|
|
process.once("SIGTERM", () => {
|
|
appendFileSync(process.env.OWNER_EVENTS, JSON.stringify({ kind: "leader-exit", rootExists: existsSync(process.env.OWNER_ROOT) }) + "\\n");
|
|
process.kill(process.pid, "SIGTERM");
|
|
});
|
|
const child = spawn(process.execPath, [process.env.OWNER_DESCENDANT_ENTRY], {
|
|
stdio: ["ignore", "ignore", "ignore", "ipc"],
|
|
});
|
|
const ready = await new Promise((resolve, reject) => {
|
|
child.once("message", resolve);
|
|
child.once("error", reject);
|
|
});
|
|
writeFileSync(process.env.OWNER_READY, JSON.stringify({ pids: [process.pid, ready.pid] }));
|
|
process.stdout.write("npm descendant ready\\n");
|
|
process.stderr.write("selected npm diagnostics\\n");
|
|
setInterval(() => {}, 1000);
|
|
`;
|
|
|
|
const reportSchema = z.object({
|
|
pids: z.array(z.number().int().positive()).length(2),
|
|
liveAtRemoval: z.array(z.number().int().positive()),
|
|
rootRemoved: z.boolean(),
|
|
failure: z.object({
|
|
reason: z.literal("timeout"),
|
|
stdout: z.string(),
|
|
stderr: z.string(),
|
|
}),
|
|
});
|
|
|
|
const stopReceiptSchema = z.object({
|
|
kind: z.enum(["leader-exit", "descendant-exit"]),
|
|
rootExists: z.boolean(),
|
|
reason: z.string().optional(),
|
|
});
|
|
|
|
const releaseDescendant = async (release: string, receipt: string) => {
|
|
await writeFile(release, "release this authored descendant");
|
|
const pid = await readFile(receipt, "utf8").then(Number, (cause: unknown) => {
|
|
if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
|
|
return undefined;
|
|
throw cause;
|
|
});
|
|
if (pid === undefined) return;
|
|
const deadline = Date.now() + 7_500;
|
|
while (
|
|
liveProcesses(await systemProcessOwnershipHost.listProcesses()).some(
|
|
(entry) => entry.pid === pid,
|
|
)
|
|
) {
|
|
if (Date.now() >= deadline)
|
|
throw new Error(
|
|
"Authored npm descendant did not consume its release marker",
|
|
);
|
|
await delay(25);
|
|
}
|
|
};
|
|
|
|
// Actual npm-entry/descendant processes and actual scratch removal; no package
|
|
// install, native engine, or WABT behavior is simulated or claimed.
|
|
processTest.skipIf(process.platform === "win32")(
|
|
"joins a timed-out npm entry and its acknowledged descendant before removing the verifier root",
|
|
{ timeout: 30_000 },
|
|
async ({ processes }) => {
|
|
const root = await createTestTempDirectory("rea-package-command-owner-");
|
|
const scratch = join(root, "scratch");
|
|
const entry = join(root, "selected npm entry.mjs");
|
|
const descendant = join(root, "descendant.mjs");
|
|
const release = join(root, "release-descendant");
|
|
const receipt = join(root, "descendant.pid");
|
|
const ready = join(root, "ready.json");
|
|
const events = join(root, "events.jsonl");
|
|
const driver = join(root, "driver.mjs");
|
|
await writeFile(entry, npmSource);
|
|
await writeFile(descendant, descendantSource);
|
|
const commandsModule = pathToFileURL(
|
|
resolve("scripts/lib/verify-package-commands.mjs"),
|
|
).href;
|
|
const cleanup = pathToFileURL(
|
|
resolve("scripts/lib/verifier-cleanup.mjs"),
|
|
).href;
|
|
const observations = pathToFileURL(
|
|
resolve("dist/process/ProcessOwnershipObservation.js"),
|
|
).href;
|
|
const tree = pathToFileURL(
|
|
resolve("dist/process/ProcessOwnershipProcessTree.js"),
|
|
).href;
|
|
await writeFile(
|
|
driver,
|
|
`
|
|
import { existsSync } from "node:fs";
|
|
import { mkdir, readFile, rm } from "node:fs/promises";
|
|
import { createPackageVerifierCommands } from ${JSON.stringify(commandsModule)};
|
|
import { withVerifierCleanup } from ${JSON.stringify(cleanup)};
|
|
import { systemProcessOwnershipHost } from ${JSON.stringify(observations)};
|
|
import { liveProcesses } from ${JSON.stringify(tree)};
|
|
const scratch = process.env.OWNER_ROOT;
|
|
await mkdir(scratch);
|
|
let commands, recoveryOwner, pids, liveAtRemoval, failure, diagnostics;
|
|
const workspace = {
|
|
path: scratch,
|
|
async close() {
|
|
const ready = await readFile(process.env.OWNER_READY, "utf8").then(JSON.parse, cause => {
|
|
if (cause.code === "ENOENT") return { pids: [] };
|
|
throw cause;
|
|
});
|
|
pids = ready.pids;
|
|
liveAtRemoval = liveProcesses(await systemProcessOwnershipHost.listProcesses()).filter(({ pid }) => pids.includes(pid)).map(({ pid }) => pid);
|
|
await rm(scratch, { recursive: true, force: true });
|
|
},
|
|
};
|
|
try {
|
|
await withVerifierCleanup(
|
|
async () => {
|
|
commands = await createPackageVerifierCommands({ workspace });
|
|
await commands.execNpm(["pack", "caller value with spaces"], { cwd: scratch, env: process.env, timeout: 1500 });
|
|
},
|
|
[async () => {
|
|
if (commands === undefined) await rm(scratch, { recursive: true, force: true });
|
|
else await commands.close();
|
|
}],
|
|
);
|
|
} catch (cause) {
|
|
const failures = cause instanceof AggregateError ? cause.errors : [cause];
|
|
// Capture exact cleanup authority before projecting facts or making assertions.
|
|
recoveryOwner = failures.find(error => typeof error?.cleanupOwner?.close === "function")?.cleanupOwner ?? commands;
|
|
const primary = failures.find(error => error?.snapshot !== undefined && typeof error?.reason === "string");
|
|
failure = { reason: primary?.reason, stdout: primary?.snapshot?.stdout.text, stderr: primary?.snapshot?.stderr.text };
|
|
diagnostics = failures.map(error => ({
|
|
name: error?.name ?? typeof error,
|
|
message: error?.message ?? String(error),
|
|
reason: error?.reason,
|
|
snapshotAvailable: error?.snapshot !== undefined && error?.snapshot !== null,
|
|
stack: error?.stack,
|
|
cleanupFailure: error?.cleanupFailure,
|
|
processError: error?.processError?.message,
|
|
preparationFailure: error?.preparationFailure?.message,
|
|
cause: {
|
|
name: error?.cause?.name,
|
|
message: error?.cause?.message ?? String(error?.cause),
|
|
stack: error?.cause?.stack,
|
|
cause: error?.cause?.cause?.message ?? String(error?.cause?.cause),
|
|
},
|
|
}));
|
|
} finally {
|
|
if (recoveryOwner !== undefined) await recoveryOwner.close();
|
|
else if (commands !== undefined) await commands.close();
|
|
else await rm(scratch, { recursive: true, force: true });
|
|
}
|
|
process.stdout.write(JSON.stringify({ pids, liveAtRemoval, rootRemoved: !existsSync(scratch), failure, diagnostics }));
|
|
`,
|
|
);
|
|
try {
|
|
const result = await processes.run(process.execPath, [driver], {
|
|
timeoutMs: 20_000,
|
|
env: {
|
|
...process.env,
|
|
npm_execpath: entry,
|
|
OWNER_ROOT: scratch,
|
|
OWNER_DESCENDANT_ENTRY: descendant,
|
|
OWNER_DESCENDANT: receipt,
|
|
OWNER_RELEASE: release,
|
|
OWNER_READY: ready,
|
|
OWNER_EVENTS: events,
|
|
},
|
|
});
|
|
expect(result.exitCode, result.stderr).toBe(0);
|
|
expect(result.signal, result.stderr).toBeNull();
|
|
const parsed = reportSchema.safeParse(JSON.parse(result.stdout));
|
|
expect(parsed.success, result.stdout).toBe(true);
|
|
if (!parsed.success) throw parsed.error;
|
|
const report = parsed.data;
|
|
expect(report.failure).toEqual({
|
|
reason: "timeout",
|
|
stdout: "npm descendant ready\n",
|
|
stderr: "selected npm diagnostics\n",
|
|
});
|
|
expect(report.liveAtRemoval).toEqual([]);
|
|
expect(report.rootRemoved).toBe(true);
|
|
const receipts = await readFile(events, "utf8").catch(
|
|
(cause: unknown) => {
|
|
if (
|
|
cause instanceof Error &&
|
|
"code" in cause &&
|
|
cause.code === "ENOENT"
|
|
)
|
|
return "";
|
|
throw cause;
|
|
},
|
|
);
|
|
const stopped = receipts
|
|
.trim()
|
|
.split("\n")
|
|
.filter((line) => line !== "")
|
|
.map((line) => stopReceiptSchema.parse(JSON.parse(line)));
|
|
for (const receipt of stopped) expect(receipt.rootExists).toBe(true);
|
|
} finally {
|
|
await releaseDescendant(release, receipt);
|
|
}
|
|
},
|
|
);
|