245 lines
7.8 KiB
TypeScript
245 lines
7.8 KiB
TypeScript
import { expect, it } from "vitest";
|
|
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
|
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
|
import { wabtFailure } from "./WabtFailures.js";
|
|
import { WABT_LIMITS } from "./WabtRelease.js";
|
|
import { OwnedCommandFailure } from "../process/OwnedCommand.js";
|
|
|
|
it.each(["configuration", "producer"] as const)(
|
|
"retains timeout recovery under %s cleanup failure without making cleanup retryable",
|
|
(phase) => {
|
|
const cause = new OwnedCommandFailure(
|
|
"timeout",
|
|
"Command deadline elapsed.",
|
|
null,
|
|
"owned stop unconfirmed",
|
|
undefined,
|
|
["pid:owned-fixture"],
|
|
);
|
|
const failure = wabtFailure(cause, phase, "/module.wasm", {
|
|
commandTimeoutMs: 1234,
|
|
});
|
|
expect(failure.cause).toBe(cause);
|
|
const projected = projectAnalysisError(failure);
|
|
expect(projected).toMatchObject({
|
|
code: "cleanup_incomplete",
|
|
retryable: false,
|
|
details: {
|
|
resources: ["pid:owned-fixture"],
|
|
diagnostics: {
|
|
previous_error: {
|
|
code: "provider_timeout",
|
|
details: { timeout_ms: 1234 },
|
|
recovery: { kind: "inspect_timeout_state", timeout_ms: 1234 },
|
|
remediation: {
|
|
action: expect.stringContaining("command_timeout_ms above 1234"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
expect(projected.remediation.action).not.toContain("Increase");
|
|
},
|
|
);
|
|
|
|
it("retains timeout command output once, including when cleanup wraps its recovery", () => {
|
|
for (const cleanup of [null, "stop unconfirmed"]) {
|
|
const cause = new OwnedCommandFailure(
|
|
"timeout",
|
|
"deadline",
|
|
{
|
|
stdout: { text: "partial-owned-output", bytes: 20, observedBytes: 20 },
|
|
stderr: { text: "", bytes: 0, observedBytes: 0 },
|
|
exitCode: null,
|
|
signal: null,
|
|
},
|
|
cleanup,
|
|
);
|
|
const projected = projectAnalysisError(
|
|
wabtFailure(cause, "producer", "/module.wasm", {
|
|
commandTimeoutMs: 1234,
|
|
}),
|
|
);
|
|
expect(
|
|
JSON.stringify(projected).split("partial-owned-output"),
|
|
).toHaveLength(2);
|
|
expect(projected.code).toBe(
|
|
cleanup === null ? "provider_timeout" : "cleanup_incomplete",
|
|
);
|
|
}
|
|
});
|
|
|
|
it.each([1234, 2_147_483_647])(
|
|
"reports the actual %i ms command budget without an unsupported higher timer",
|
|
(commandTimeoutMs) => {
|
|
const projected = projectAnalysisError(
|
|
wabtFailure(
|
|
new OwnedCommandFailure("timeout", "deadline"),
|
|
"producer",
|
|
"/module.wasm",
|
|
{ commandTimeoutMs },
|
|
),
|
|
);
|
|
expect(projected).toMatchObject({
|
|
code: "provider_timeout",
|
|
details: { timeout_ms: commandTimeoutMs },
|
|
recovery: { kind: "inspect_timeout_state", timeout_ms: commandTimeoutMs },
|
|
});
|
|
expect(projected.remediation.action).toContain("no server restart");
|
|
expect(projected.remediation.action).toContain("cleanup");
|
|
if (commandTimeoutMs === 2_147_483_647) {
|
|
expect(projected.remediation.action).toContain(
|
|
"no higher command deadline",
|
|
);
|
|
expect(projected.remediation.action).not.toContain("Increase");
|
|
} else
|
|
expect(projected.remediation.action).toContain("--command-timeout-ms");
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{ reason: "timeout" as const, cleanup: null, code: "provider_timeout" },
|
|
{ reason: "cancelled" as const, cleanup: null, code: "cancelled" },
|
|
{
|
|
reason: "process" as const,
|
|
cleanup: "owned stop failed",
|
|
code: "cleanup_incomplete",
|
|
},
|
|
])(
|
|
"retains version-probe facts without changing $reason cleanup=$cleanup",
|
|
({ reason, cleanup, code }) => {
|
|
const cause = new OwnedCommandFailure(
|
|
reason,
|
|
"version command failed",
|
|
null,
|
|
cleanup,
|
|
);
|
|
const probe = {
|
|
configuration_key: "REA_WABT_BIN_DIRECTORY" as const,
|
|
executable_path: "/tools/wasm2wat",
|
|
tool: "wasm2wat",
|
|
expected_version: "1.0.42" as const,
|
|
};
|
|
const failure = wabtFailure(cause, "configuration", "/selected.wasm", {
|
|
commandTimeoutMs: 1234,
|
|
versionProbe: probe,
|
|
});
|
|
const original = wabtFailure(cause, "configuration", "/selected.wasm");
|
|
expect(failure._tag).toBe(original._tag);
|
|
expect(failure.cleanupIncomplete).toBe(original.cleanupIncomplete);
|
|
expect(failure.cause).toBe(original.cause);
|
|
expect(projectAnalysisError(failure)).toMatchObject({
|
|
code,
|
|
details: { diagnostics: probe },
|
|
});
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{ code: "EACCES", action: "read and execute access" },
|
|
{
|
|
code: "ENOENT",
|
|
action: "contains wasm-validate, wasm-objdump and wasm2wat",
|
|
},
|
|
{ code: "ENOTDIR", action: "directory, not an individual executable" },
|
|
])(
|
|
"explains $code configuration recovery without blaming the WASM input",
|
|
({ code, action }) => {
|
|
const failure = wabtFailure(
|
|
Object.assign(new Error(`tool admission failed: ${code}`), { code }),
|
|
"configuration",
|
|
"/selected.wasm",
|
|
);
|
|
const projected = projectAnalysisError(failure);
|
|
expect(projected).toMatchObject({
|
|
code: "capability_unavailable",
|
|
retryable: false,
|
|
remediation: { action: expect.stringContaining(action) },
|
|
});
|
|
expect(projected.remediation.action).toContain("REA_WABT_BIN_DIRECTORY");
|
|
expect(projected.remediation.action).toContain("restart");
|
|
expect(projected.remediation.action).toContain("Do not repeat");
|
|
},
|
|
);
|
|
|
|
it.each([false, true])(
|
|
"preserves the file-size boundary with cleanup=%s when an input exceeds its fixed allowance",
|
|
(cleanup) => {
|
|
const cause = new ArtifactReaderFailure(
|
|
"limit",
|
|
"Selected artifact exceeds its byte allowance",
|
|
{
|
|
boundary: "file-size",
|
|
...(cleanup
|
|
? {
|
|
cleanup: { reason: "close denied", resources: ["selected.wasm"] },
|
|
}
|
|
: {}),
|
|
},
|
|
);
|
|
const projected = projectAnalysisError(
|
|
wabtFailure(cause, "artifact", "selected.wasm"),
|
|
);
|
|
expect(projected).toMatchObject({
|
|
code: cleanup ? "cleanup_incomplete" : "resource_constraint",
|
|
category: "resource_constraint",
|
|
retryable: false,
|
|
details: {
|
|
resource: "file-size",
|
|
reported_limits: { maximum_input_bytes: WABT_LIMITS.inputBytes },
|
|
},
|
|
});
|
|
if (!cleanup) {
|
|
expect(projected.remediation.action).toContain("input byte allowance");
|
|
expect(projected.remediation.action).not.toContain("evidence reply");
|
|
}
|
|
expect(
|
|
wabtFailure(cause, "artifact", "selected.wasm").cleanupResources,
|
|
).toEqual(cleanup ? ["selected.wasm"] : []);
|
|
},
|
|
);
|
|
|
|
it("keeps oversized tool admission distinct from selected artifact size", () => {
|
|
const cause = new ArtifactReaderFailure(
|
|
"limit",
|
|
"Configured tool exceeds its byte allowance",
|
|
{ boundary: "file-size" },
|
|
);
|
|
expect(
|
|
projectAnalysisError(wabtFailure(cause, "configuration", "selected.wasm"))
|
|
.code,
|
|
).toBe("capability_unavailable");
|
|
});
|
|
|
|
it.each([null, { stopped: "caller" }])(
|
|
"keeps the command-owned cancellation reason as the direct typed cause: %j",
|
|
(reason) => {
|
|
const cause = new OwnedCommandFailure(
|
|
"cancelled",
|
|
"cancelled",
|
|
null,
|
|
null,
|
|
{ cancellation: { reason } },
|
|
);
|
|
const error = wabtFailure(cause, "producer", "/module.wasm");
|
|
expect(error._tag).toBe("AnalysisCancelledError");
|
|
expect(error.cause).toBe(reason);
|
|
},
|
|
);
|
|
it("attributes optional artifact admission to its selected caller field", () => {
|
|
const cause = Object.assign(new Error("missing candidate"), {
|
|
code: "ENOENT",
|
|
});
|
|
expect(
|
|
projectAnalysisError(
|
|
wabtFailure(cause, "artifact", "/missing.wasm", {
|
|
commandTimeoutMs: 30000,
|
|
inputField: ["candidate_paths", 2],
|
|
}),
|
|
),
|
|
).toMatchObject({
|
|
code: "invalid_request",
|
|
details: { issues: [{ path: ["candidate_paths", 2] }] },
|
|
});
|
|
});
|