Files
rea/scripts/verify-package-update.mjs
T
morluto 72ba1cd92b fix(verifiers): centralize failure and cleanup ownership
Own operation results and exact rejections in withVerifierCleanup, including
undefined, null and independent cleanup failures. Remove the optional-primary
cleanup contract and migrate every caller, preserving returns, annotations,
signal restoration and dependent cleanup order.

Make Frida/JEB reporters fail on reasonless rejections and retain JEB fixture
files while provider cleanup is incomplete. Own the nested packaged WASM
verifier and expose its composite process/root cleanup capability for retry.
Update runnable docs and isolated fixture dependencies.

Validation: 547 focused cases in 20 files, eight isolated boundary unit checks,
static/build/docs/discovery gates and duplication check (3.58%). Node process,
SDK and descriptor cleanup are exercised; provider and packaging results in
orchestration tests are simulated. Pack/install subprocess ownership remains
with the existing shared package runner outside the nested-command change.
2026-10-24 15:38:35 +08:00

307 lines
11 KiB
JavaScript

import { createHash, randomUUID } from "node:crypto";
import { createServer } from "node:http";
import { readFile, writeFile, mkdir } from "node:fs/promises";
import { dirname, join } from "node:path";
import { pathToFileURL } from "node:url";
import { exec, json } from "./lib/verify-package-core.mjs";
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
/** Keep the live updater owner and its workspace when process cleanup is unproven. */
export class PackageUpdateCleanupFailure extends AggregateError {
constructor(primary, cleanupOwner, result, workspace) {
const cleanup = new Error(result.reason);
super(
primary === undefined ? [cleanup] : [primary, cleanup],
`Package updater cleanup incomplete; retained ${workspace}`,
{ cause: primary },
);
this.cleanupOwner = cleanupOwner;
this.workspace = workspace;
}
}
/** Own the helper and installer descendants until output and process cleanup finish. */
async function runUpdater({
harness,
environment,
signal,
workspace,
retainWorkspace,
}) {
// Only updater execution needs built process ownership; importing this fixture
// does not make unrelated source-only verifier helpers depend on dist.
const { ProviderProcessSupervisor, spawnOwnedProviderProcess } =
await import("../dist/process/ProviderProcess.js");
const { cleanupOwnedProcessGroup } =
await import("../dist/process/ProcessOwnership.js");
const launch = await spawnOwnedProviderProcess({
command: process.execPath,
arguments: [harness],
expectedCommand: null,
hostEnvironment: environment,
cwd: workspace,
runId: randomUUID(),
sweepTokenOwnedProcesses: true,
signal,
});
let collectionFailure;
const supervisor = new ProviderProcessSupervisor(
{
...launch,
ownsProcessLifetime: true,
cleanup:
launch.cleanup ?? (() => cleanupOwnedProcessGroup(launch.ownership)),
},
{
maxDiagnosticBytes: 16 * 1024 * 1024,
onDiagnostic(event) {
if (event.type === "output" && event.truncated)
collectionFailure ??= new Error(
"Package updater exceeded its diagnostic capacity",
);
if (event.type === "error")
collectionFailure ??= new Error(event.message, {
cause: event.cause,
});
},
},
);
let failure;
let stdout;
try {
while (!(await supervisor.waitForOutputClose(50))) {
signal.throwIfAborted();
if (collectionFailure !== undefined) throw collectionFailure;
}
signal.throwIfAborted();
if (collectionFailure !== undefined) throw collectionFailure;
const snapshot = supervisor.snapshot();
if (
snapshot.diagnosticTruncated ||
snapshot.exitCode !== 0 ||
snapshot.signal !== null
)
throw new Error(
`Package updater failed: ${snapshot.stdout.text}\n${snapshot.stderr.text}`,
);
stdout = snapshot.stdout.text;
} catch (cause) {
failure = { cause };
}
const stopped = await supervisor.stop();
if (stopped.status === "incomplete") {
retainWorkspace?.();
throw new PackageUpdateCleanupFailure(
failure?.cause,
supervisor,
stopped,
workspace,
);
}
if (failure !== undefined) throw failure.cause;
return stdout;
}
/** Exercise the packaged updater with real npm and a local release registry fixture. */
export async function verifyPackageUpdate({
prefix,
packageRoot,
tarball,
workspace,
environment,
retainWorkspace,
}) {
const metadataPath = join(packageRoot, "package.json");
const metadata = json(await readFile(metadataPath, "utf8"));
const archive = await readFile(tarball);
const entry = join(packageRoot, "scripts", "rea.mjs");
const home = join(workspace, "update-home");
const codexPath = join(home, ".codex", "config.toml");
const originalConfig =
'[mcp_servers.rea]\ncommand = "npx"\nargs = ["-y", "rea-agents@0.0.0", "mcp"]\n';
await mkdir(dirname(codexPath), { recursive: true });
await writeFile(codexPath, originalConfig);
let fixtureFailure;
const fixture = new AbortController();
const server = createServer((request, response) => {
void serveRegistry(request, response).catch((error) => {
fixtureFailure ??= error;
fixture.abort(fixtureFailure);
if (response.headersSent) {
response.destroy();
return;
}
response.writeHead(500);
response.end(String(error));
});
});
let registry;
async function serveRegistry(request, response) {
const pathname = new URL(request.url, registry).pathname;
if (pathname === "/rea-agents") {
response.setHeader("Content-Type", "application/json");
response.end(
JSON.stringify({
name: metadata.name,
"dist-tags": { latest: metadata.version },
versions: {
[metadata.version]: {
...metadata,
dist: {
tarball: `${registry}/release.tgz`,
integrity: `sha512-${createHash("sha512").update(archive).digest("base64")}`,
},
},
},
}),
);
} else if (pathname === "/release.tgz") {
response.end(archive);
} else if (request.method === "POST") {
response.setHeader("Content-Type", "application/json");
response.end("{}");
} else {
const upstream = await fetch(`https://registry.npmjs.org${request.url}`);
const body = Buffer.from(await upstream.arrayBuffer());
response.writeHead(upstream.status, {
"Content-Type":
upstream.headers.get("content-type") ?? "application/octet-stream",
});
response.end(body);
}
}
await new Promise((resolveListen, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", resolveListen);
});
return withVerifierCleanup(async () => {
try {
const address = server.address();
if (typeof address !== "object" || address === null)
throw new Error("Registry fixture did not bind");
registry = `http://127.0.0.1:${address.port}`;
// Model an older installed release's identity while retaining the new updater
// under test. The registry supplies the unmodified packed target artifact.
const generatedPath = join(
packageRoot,
"dist",
"generatedPackageMetadata.js",
);
const generated = await readFile(generatedPath, "utf8");
const previousVersion = "0.0.0";
const previousGenerated = generated.replace(
`version: "${metadata.version}"`,
`version: "${previousVersion}"`,
);
if (previousGenerated === generated)
throw new Error("Could not create previous-release identity fixture");
await writeFile(generatedPath, previousGenerated);
await writeFile(
metadataPath,
JSON.stringify({ ...metadata, version: previousVersion }),
);
const runtimeEnvironment = {
...environment,
HOME: home,
USERPROFILE: home,
APPDATA: join(home, "AppData", "Roaming"),
LOCALAPPDATA: join(home, "AppData", "Local"),
CLAUDE_CONFIG_DIR: home,
CODEX_HOME: join(home, ".codex"),
COPILOT_HOME: join(home, ".copilot"),
GROK_HOME: join(home, ".grok"),
SAND_DATA_ROOT: join(home, ".grokbot"),
OMP_PROFILE: undefined,
PI_CODING_AGENT_DIR: undefined,
PI_CONFIG_DIR: undefined,
PI_PROFILE: undefined,
XDG_CONFIG_HOME: join(home, ".config"),
OPENCODE_CONFIG: join(home, ".config", "opencode", "opencode.json"),
npm_config_prefix: prefix,
npm_config_registry: registry,
};
const previous = (
await exec(process.execPath, [entry, "--version"], {
env: runtimeEnvironment,
})
).stdout.trim();
if (previous !== previousVersion)
throw new Error(`Previous-release fixture reported ${previous}`);
const harness = join(workspace, "verify-update.mjs");
await writeFile(
harness,
`
import { runUpdate } from ${JSON.stringify(pathToFileURL(join(packageRoot, "dist/application/Update.js")).href)};
import { systemUpdateHost } from ${JSON.stringify(pathToFileURL(join(packageRoot, "dist/application/UpdateRuntime.js")).href)};
const host = systemUpdateHost(${JSON.stringify(packageRoot)}, ${JSON.stringify(home)});
const result = await runUpdate(${JSON.stringify(previousVersion)}, host, "structured");
process.stdout.write(JSON.stringify(result));
if (result.status === "failed") process.exitCode = 1;
`,
);
const result = json(
await runUpdater({
harness,
environment: runtimeEnvironment,
signal: fixture.signal,
workspace,
retainWorkspace,
}),
);
if (
result.status !== "updated" ||
result.installedVersion !== metadata.version ||
result.command.at(-1) !== `${metadata.name}@${metadata.version}` ||
result.maintenance.status !== "planned" ||
result.maintenance.scope.clients.join(",") !== "codex" ||
result.maintenance.scope.skill !== false
)
throw new Error(
`Packaged updater failed its exact-release contract: ${JSON.stringify(result)}`,
);
if ((await readFile(codexPath, "utf8")) !== originalConfig)
throw new Error(
"Update applied integration maintenance without approval",
);
const current = (
await exec(process.execPath, [entry, "--version"], {
env: runtimeEnvironment,
})
).stdout.trim();
if (current !== metadata.version)
throw new Error(`Updated executable reported ${current}`);
const help = (
await exec(process.execPath, [entry, "--help"], {
env: runtimeEnvironment,
})
).stdout;
if (!/^\s{2}update\s/mu.test(help) || /^\s{2}upgrade\s/mu.test(help))
throw new Error(
"Updated CLI did not expose update as its sole maintenance command",
);
if (fixtureFailure !== undefined) throw fixtureFailure;
return {
installed_version: result.installedVersion,
exact_release: true,
maintenance_read_only: true,
};
} catch (error) {
// A retained process owner takes precedence over its triggering fixture
// failure; otherwise report the first registry/proxy failure exactly.
throw error instanceof PackageUpdateCleanupFailure
? error
: (fixtureFailure ?? error);
}
}, [
async () => {
server.closeAllConnections();
await new Promise((resolveClose, reject) =>
server.close((error) =>
error === undefined ? resolveClose() : reject(error),
),
);
},
]);
}