Files
rea/tests/fixtures/evidencePublicationPreload.mjs
Kaoru0822andClaude Opus 5.5 4a4bbab3c3 fix(cli): keep committed evidence bundle receipts after cancellation (#6221)
Summary delivery with --evidence_output wrote the bundle, then let the
command's close/cancellation check replace the result. A SIGINT/SIGTERM
during staging cleanup after commit returned typed cancellation without
the receipt, and a persistent staging cleanup failure after commit was
rewrapped as a provider cleanup error that dropped published_path.

Every --evidence_output command now selects its view first and publishes
through publishEvidenceDelivery, the owner inspect-managed-members already
used. Post-commit cancellation keeps {path, bytes} under
details.diagnostics.evidence_bundle; incomplete cleanup keeps
details.published_path and bytes; a cleanup retry that succeeds returns
the normal receipt; pre-commit cancellation creates nothing and names
the command's operation.

Fixes #6095

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:32:26 -07:00

83 lines
2.8 KiB
JavaScript

import assert from "node:assert/strict";
import { createRequire, syncBuiltinESMExports } from "node:module";
import { writeFileSync } from "node:fs";
// Drives one real staged Evidence publication at an exact lifecycle point.
// "cleanup" queues the signal after the committed output's staging directory
// is removed; "staging" delivers it once the staging directory exists, before
// any byte can be committed. cleanup_failures rejects that many removals of
// the staging directory first, without removing it.
const input = JSON.parse(process.env.REA_TEST_EVIDENCE_PUBLICATION ?? "null");
assert.ok(
input && typeof input.prefix === "string" && typeof input.marker === "string",
);
const phase = input.phase ?? "cleanup";
assert.ok(["staging", "cleanup"].includes(phase));
if (input.signal !== undefined) {
assert.ok(["SIGINT", "SIGTERM"].includes(input.signal));
assert.ok(["event", "os"].includes(input.delivery));
}
let cleanupFailures = input.cleanup_failures ?? 0;
assert.ok(Number.isSafeInteger(cleanupFailures) && cleanupFailures >= 0);
const require = createRequire(import.meta.url);
const fs = require("node:fs/promises");
const owned = (path) => String(path).startsWith(input.prefix);
let signalled = input.signal === undefined;
const deliver = (observe) => {
writeFileSync(
input.marker,
JSON.stringify({
phase,
signal: input.signal,
delivery: input.delivery,
listeners: process.listenerCount(input.signal),
}),
);
// Registered after the marker so it never stands in for the CLI handler.
// Signal watchers do not keep the event loop alive; the timer does, and
// reports a signal that was never observed. It never selects the phase.
const observed = observe
? new Promise((resolve, reject) => {
const timeout = setTimeout(
() => reject(new Error(`${input.signal} was not observed`)),
5_000,
);
process.once(input.signal, () => {
clearTimeout(timeout);
resolve();
});
})
: undefined;
if (input.delivery === "os") process.kill(process.pid, input.signal);
else process.emit(input.signal);
return observed;
};
const remove = fs.rmdir;
fs.rmdir = async (...arguments_) => {
if (cleanupFailures > 0 && owned(arguments_[0])) {
cleanupFailures -= 1;
throw Object.assign(new Error("injected staging cleanup failure"), {
code: "EBUSY",
});
}
const result = await remove(...arguments_);
if (phase === "cleanup" && !signalled && owned(arguments_[0])) {
signalled = true;
setImmediate(() => deliver(false));
}
return result;
};
const stage = fs.mkdtemp;
fs.mkdtemp = async (...arguments_) => {
const directory = await stage(...arguments_);
if (phase === "staging" && !signalled && owned(directory)) {
signalled = true;
await deliver(true);
}
return directory;
};
syncBuiltinESMExports();