rea/src/domain/analysisErrorProjection.tes...

393 lines
12 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
AnalysisCapabilityUnavailableError,
AnalysisUnsupportedTargetError,
AnalysisInputError,
AnalysisOutputError,
AnalysisCancelledError,
AnalysisTimeoutError,
AnalysisResourceConstraintError,
} from "./analysisErrorCore.js";
import { ArtifactOperationError } from "./artifactOperationError.js";
import { BinaryTargetError } from "./configurationErrors.js";
import { BrowserObservationError } from "./browserObservationError.js";
import {
HopperProcessError,
HopperRemoteError,
HopperStartError,
HopperTimeoutError,
} from "./hopperErrors.js";
import { ProviderAdapterError } from "./providerAdapterError.js";
import { UnknownRegistryError } from "./unknownRegistryError.js";
import { projectAnalysisError } from "./analysisErrorProjection.js";
import { ProviderSelectionError } from "./providerSelectionError.js";
import { analysisErrorProjectionSchema } from "../contracts/errorSchemas.js";
const retainedOutput = {
stdout: "selected output\u0000",
stderr: "upstream warning",
truncated: true,
};
it("distinguishes unsupported target formats from unavailable provider capabilities", () => {
const projected = projectAnalysisError(
new AnalysisUnsupportedTargetError(
"inspect_evm_interface",
"/selected/runtime.hex",
"EOF-style EF00 container is unsupported.",
{ capturedOutput: retainedOutput },
),
);
expect(projected).toMatchObject({
code: "unsupported_target",
category: "unsupported_target",
retryable: false,
details: {
operation: "inspect_evm_interface",
path: "/selected/runtime.hex",
reason: "EOF-style EF00 container is unsupported.",
captured_output: retainedOutput,
},
});
expect(projected.message).toContain("EF00");
expect(projected.remediation.action).toContain("target format");
expect(projected.remediation.action).not.toContain("doctor");
expect(analysisErrorProjectionSchema.safeParse(projected).success).toBe(true);
});
it.each([
["memory", null],
["memory", { address_space_bytes: 67108864 }],
["cpu", null],
["cpu", { cpu_seconds: 1 }],
["file-size", null],
["file-size", { file_size_bytes: 1024 }],
["transport", { result_budget_bytes: 10485760 }],
] as const)(
"projects reported %s constraints with observed or unknown limits: %j",
(resource, limits) => {
const projected = projectAnalysisError(
new AnalysisResourceConstraintError(
"inspect_binary_layout",
resource,
resource === "memory"
? "Memory allocation failed; exact cause unknown."
: "Observed SIGXCPU; exact signal cause unknown.",
limits,
{ capturedOutput: retainedOutput },
),
);
expect(projected).toMatchObject({
code: "resource_constraint",
category: "resource_constraint",
retryable: false,
details: {
resource,
reported_limits: limits,
captured_output: retainedOutput,
},
});
expect(projected.remediation.action).toContain(
resource === "cpu"
? "CPU"
: resource === "transport"
? "Evidence reference"
: resource === "file-size"
? "file-size"
: "memory",
);
expect(projected.remediation.action).not.toContain("doctor");
expect(analysisErrorProjectionSchema.safeParse(projected).success).toBe(
true,
);
},
);
it.each([
new AnalysisInputError("inspect", { capturedOutput: retainedOutput }),
new AnalysisOutputError("inspect", "invalid reply", {
capturedOutput: retainedOutput,
}),
new AnalysisCapabilityUnavailableError(
"provider",
"inspect",
"unsupported profile",
{ capturedOutput: retainedOutput },
),
new AnalysisCancelledError("inspect", { capturedOutput: retainedOutput }),
new AnalysisTimeoutError("inspect", 30, { capturedOutput: retainedOutput }),
new ProviderSelectionError({
operation: "inspect",
reason: "provider_unavailable",
requestedProviderId: "provider",
candidateIds: [],
capturedOutput: retainedOutput,
}),
])(
"retains captured output without changing the typed primary failure: $._tag",
(failure) => {
expect(projectAnalysisError(failure).details?.captured_output).toEqual(
retainedOutput,
);
},
);
describe("analysis error projection: provider failures", () => {
it("projects the primary browser failure alongside incomplete cleanup", () => {
const projected = projectAnalysisError(
new BrowserObservationError(
"observe_javascript_runtime",
"cleanup_failed",
{
cause: new AggregateError([
new BrowserObservationError(
"observe_javascript_runtime",
"target_changed",
),
new Error("socket close failed"),
]),
},
),
);
expect(projected).toMatchObject({
code: "cleanup_incomplete",
details: {
operation: "observe_javascript_runtime",
reason: "cleanup_failed",
cleanup: "incomplete",
resources: ["browser_transport"],
primary_reason: "target_changed",
},
});
});
it("reports a timed-out active Hopper request and actionable retry guidance", () => {
const projected = projectAnalysisError(
new HopperTimeoutError(30_000, "trace_feature", 42, "busy"),
);
expect(projected).toMatchObject({
code: "provider_timeout",
category: "timeout",
retryable: true,
details: {
stage: "analysis",
operation: "trace_feature",
request_id: 42,
provider_state: "busy",
timeout_ms: 30_000,
},
});
expect(projected.message).toContain("binary_session.analysis_activity");
expect(projected.remediation.action).toContain(
"wait for the active Hopper request",
);
});
it("identifies the REA session that already owns a Hopper target", () => {
const error = new HopperStartError({
ownerRunId: "session-owner",
userMessage:
"This Hopper target is already open in REA session session-owner. Use that REA session or close it before opening this target again.",
});
expect(projectAnalysisError(error)).toMatchObject({
category: "unavailable",
message: expect.stringContaining("session-owner"),
remediation: {
action: expect.stringContaining(
"Use the active REA session session-owner",
),
},
});
});
it("retains the Hopper request and provider diagnostic on a remote failure", () => {
expect(
projectAnalysisError(
new HopperRemoteError(
7,
"Hopper analysis failed in the selected document",
{
diagnosticType: "bridge_exception",
operation: "trace_feature",
requestId: 43,
},
),
),
).toMatchObject({
code: "execution_failure",
details: {
stage: "analysis",
provider_code: 7,
diagnostic_type: "bridge_exception",
operation: "trace_feature",
request_id: 43,
},
message: expect.stringContaining("Hopper analysis failed"),
});
});
it("preserves detached, actionable provider diagnostics", () => {
const diagnostics = {
runtime_root: "/tmp/rea-ghidra-fixture",
profile_digest: "a".repeat(64),
exit_code: 1,
};
const error = new ProviderAdapterError("ghidra", "health", {
diagnostics,
});
expect(error.diagnostics).not.toBe(diagnostics);
expect(projectAnalysisError(error)).toMatchObject({
details: {
provider_id: "ghidra",
operation: "health",
diagnostics,
},
});
});
it("maps the Linux startup compatibility exit codes without host internals", () => {
const expected = [
[70, "private_display_unavailable"],
[71, "x11_authorization_failed"],
[72, "unsupported_hopper_build"],
[73, "invalid_launch_command"],
[74, "process_ownership_mismatch"],
[75, "hopper_exited_during_startup"],
[76, "unsupported_demo_dialog"],
[77, "unexpected_display_geometry"],
[78, "x11_input_failed"],
[79, "runtime_dependency_unavailable"],
[80, "x11_socket_directory_unusable"],
] as const;
for (const [exitCode, code] of expected) {
const projected = projectAnalysisError(new HopperProcessError(exitCode));
expect(projected).toMatchObject({
code: "provider_unavailable",
details: { failure_code: code, exit_code: exitCode },
});
expect(projected.message).toMatch(/\S/u);
expect(JSON.stringify(projected)).not.toContain("/proc/");
}
});
});
describe("analysis error projection: caller contract", () => {
it("preserves the reported output constraint without exposing its internal cause", () => {
const projected = projectAnalysisError(
new AnalysisOutputError(
"extract_firmware",
"Report exceeded the declared byte budget",
{ cause: new Error("private internal cause") },
),
);
expect(projected).toMatchObject({
code: "unreadable_output",
details: {
operation: "extract_firmware",
reason: "Report exceeded the declared byte budget",
},
});
expect(JSON.stringify(projected)).not.toContain("private internal cause");
});
it("maps representative failures without exposing causes", () => {
const secretCause = new Error("secret-token");
const projected = [
projectAnalysisError(
new AnalysisInputError("overview", { cause: secretCause }),
),
projectAnalysisError(
new AnalysisCapabilityUnavailableError("fixture", "overview", "absent"),
),
projectAnalysisError(
new BinaryTargetError("/local/targets/app", "invalid", {
cause: secretCause,
}),
),
];
expect(projected.map(({ code }) => code)).toEqual([
"invalid_request",
"capability_unavailable",
"target_unavailable",
]);
expect(projected[1]).toMatchObject({
category: "unsupported_provider",
message:
"This analysis is unavailable for the current target. Choose another analysis or target.",
details: {
provider_id: "fixture",
operation: "overview",
reason: "absent",
},
});
expect(projected[2]).toMatchObject({
details: { path: "/local/targets/app" },
});
expect(JSON.stringify(projected)).not.toContain("secret-token");
});
it("uses explicit capability recovery while retaining the constraint", () => {
const projected = projectAnalysisError(
new AnalysisCapabilityUnavailableError(
"fixture",
"capture",
"host constraint",
{
userMessage: "Use the supported host workflow.",
cause: new Error("private internal cause"),
},
),
);
expect(projected).toMatchObject({
code: "capability_unavailable",
category: "unsupported_provider",
message: "Use the supported host workflow.",
details: {
provider_id: "fixture",
operation: "capture",
reason: "host constraint",
},
});
expect(JSON.stringify(projected)).not.toContain("private internal cause");
});
it("preserves exact artifact-integrity coordinates", () => {
expect(
projectAnalysisError(
new ArtifactOperationError("inventory_artifact", "integrity", {
logicalPath: "main.js",
declaredSha256: "a".repeat(64),
calculatedSha256: "b".repeat(64),
unpacked: true,
}),
),
).toMatchObject({
category: "integrity_mismatch",
details: {
logical_path: "main.js",
declared_sha256: "a".repeat(64),
calculated_sha256: "b".repeat(64),
unpacked: true,
},
});
});
it("gives missing residual unknowns lookup-specific remediation", () => {
expect(
projectAnalysisError(new UnknownRegistryError("not-found")),
).toMatchObject({
code: "execution_failure",
message:
"The requested residual unknown does not exist in this session. Check the unknown_id and try again.",
remediation: {
action:
"Check that the unknown_id belongs to this session, then retry.",
},
details: { reason: "not-found" },
});
});
});