fix(evm): preserve command owner cancellation reasons

Copy the exact opaque reason already recorded by OwnedCommand rather than discarding it at EVM failure conversion. Keep physical, timeout and incomplete cleanup outcomes distinct.

Validation: direct compiled probe and eight source controls; four reason controls failed before the fix. Twenty-one overlapping service/adapter checks, check:fast and docs:check passed. No real Linux x64 worker cancellation or public delivery exercised. Refs #4686.
This commit is contained in:
morluto
2026-10-17 13:18:05 +08:00
parent d694aef227
commit 0ec4826633
2 changed files with 72 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
import { expect, it } from "vitest";
import { evmInterfaceFailure } from "./EvmoleFailures.js";
import { OwnedCommandFailure } from "../process/OwnedCommand.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import type { ProviderProcessSnapshot } from "../process/ProviderProcess.js";
const snapshot: ProviderProcessSnapshot = {
stdout: { text: "selected output", bytes: 15, observedBytes: 15 },
stderr: { text: "warning", bytes: 7, observedBytes: 7 },
exitCode: 0,
signal: null,
};
it.each([
undefined,
null,
"caller stopped",
{ requested: "stop" },
new AnalysisCancelledError("foreign", {
cleanup: { reason: "foreign owner", resources: ["foreign-resource"] },
}),
])(
"retains the exact command cancellation reason as opaque data: %j",
(reason) => {
const source = new OwnedCommandFailure(
"cancelled",
"cancelled",
snapshot,
null,
{
cancellation: { reason },
},
);
const result = evmInterfaceFailure(source, "worker", "/selected.hex");
expect(result).toBeInstanceOf(AnalysisCancelledError);
expect(result.cause).toBe(reason);
expect(result.capturedOutput).toEqual({
stdout: "selected output",
stderr: "warning",
truncated: false,
});
expect(result.cleanupIncomplete).toBe(false);
},
);
it.each([
{ failure: "process" as const, cleanup: null, tag: "ProviderAdapterError" },
{ failure: "timeout" as const, cleanup: null, tag: "AnalysisTimeoutError" },
{
failure: "cancelled" as const,
cleanup: "close refused",
tag: "ProviderAdapterError",
},
])("keeps $tag ahead of recorded cancellation", ({ failure, cleanup, tag }) => {
const source = new OwnedCommandFailure(
failure,
"physical outcome",
snapshot,
cleanup,
{
cancellation: { reason: "later interruption" },
},
["exact-owner"],
);
const result = evmInterfaceFailure(source, "worker", "/selected.hex");
expect(result._tag).toBe(tag);
if (cleanup !== null) {
expect(result.cleanupIncomplete).toBe(true);
expect(result.cleanupResources).toEqual(["exact-owner"]);
}
});
+1
View File
@@ -68,6 +68,7 @@ export const evmInterfaceFailure = (
);
if (cause.reason === "cancelled")
return new AnalysisCancelledError("inspect_evm_interface", {
cause: cause.cancellation?.reason,
...outputOptions,
...cleanupOptions,
});