Files
rea/src/domain/analysisErrorCleanup.test.ts
T
morluto 3e8236639a fix(errors): restore typed primary after confirmed cleanup
Retain the primary error through cleanup composition and resolve its presentation only after live ownership confirms release. Preserve historical diagnostics separately from producer facts.

Validate domain primary types, nested owners, denied retry and late observations; exercise public CLI and MCP projection plus firmware descriptor retry. Focused tests, source controls, static checks and docs pass. Native providers were not verified.

Refs: #4628
2026-10-17 09:04:09 +08:00

173 lines
6.1 KiB
TypeScript

import { expect, it } from "vitest";
import {
AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError,
AnalysisResourceConstraintError,
AnalysisTimeoutError,
} from "./analysisErrorCore.js";
import { analysisErrorWithCleanupFailure } from "./analysisErrorCleanup.js";
import { projectAnalysisError } from "./analysisErrorProjection.js";
import {
analysisErrorCategory,
analysisErrorRemediationAction,
analysisErrorUserMessage,
} from "./analysisErrorPresentation.js";
import { ProviderCleanupError } from "./providerCleanupError.js";
import { createEvidence } from "./evidence.js";
it("keeps primary diagnostics separate from historical cleanup diagnostics with the same keys", () => {
const primary = new AnalysisInputError("inspect", {
diagnostics: {
primary_error: "source observation",
cleanup_error: "another source observation",
},
});
const cleanup = new ProviderCleanupError("fixture", ["owned-input"], {});
const composed = analysisErrorWithCleanupFailure(
primary,
cleanup,
).bindCleanupDependencies([primary, cleanup]);
cleanup.resolveCleanup();
const projected = projectAnalysisError(composed);
expect(projected.details?.diagnostics).toEqual(primary.diagnostics);
expect(projected.details?.cleanup_history).toMatchObject({
primary_error: projectAnalysisError(primary),
cleanup_error: { code: "cleanup_incomplete" },
});
expect(composed.cleanupPrimary).toBe(primary);
expect(Object.hasOwn(composed, "cleanupPrimary")).toBe(false);
});
it.each([
new AnalysisInputError("inspect", { diagnostics: { input: "original" } }, [
{
path: ["path"],
reason: "out_of_range",
message: "fixed input allowance",
},
]),
new AnalysisCancelledError("inspect", { cause: "caller reason" }),
new AnalysisTimeoutError("inspect", 1234),
new AnalysisResourceConstraintError(
"inspect",
"file-size",
"fixed input allowance",
{ maximum_input_bytes: 123 },
{ retryable: false, remediationAction: "Select a smaller suitable input." },
),
new AnalysisOutputError("inspect", "invalid output", {
capturedOutput: {
stdout: "original output",
stderr: "original warning",
truncated: true,
},
partialObservation: createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: null },
{
operation: "inspect",
parameters: {},
result: { original_fact: true },
confidence: "observed",
locations: [],
limitations: [],
},
),
}),
])("restores the typed $_tag primary after confirmed cleanup", (primary) => {
const original = projectAnalysisError(primary);
const cleanup = new ProviderCleanupError(
"fixture",
["owned-input"],
{ reason: "close denied" },
{ operation: "inspect" },
);
const composed = analysisErrorWithCleanupFailure(
primary,
cleanup,
"inspect",
).bindCleanupDependencies([primary, cleanup]);
expect(projectAnalysisError(composed).code).toBe("cleanup_incomplete");
cleanup.resolveCleanup();
const projected = projectAnalysisError(composed);
expect(projected).toMatchObject(original);
expect(projected.details?.cleanup_history).toMatchObject({
primary_error: original,
cleanup_error: { code: "cleanup_incomplete" },
});
expect(projected.details).not.toHaveProperty("cleanup", "incomplete");
expect(projected.details).not.toHaveProperty("resources");
expect(analysisErrorCategory(composed)).toBe(original.category);
expect(analysisErrorUserMessage(composed)).toBe(original.message);
expect(analysisErrorRemediationAction(composed)).toBe(
original.remediation.action,
);
expect(composed.cause).toBeInstanceOf(AggregateError);
expect((composed.cause as AggregateError).errors[0]).toBe(primary);
});
it.each(["inner", "outer"] as const)(
"waits for both nested cleanup owners when %s resolves first",
(first) => {
const primary = new AnalysisInputError("inspect");
const innerCleanup = new ProviderCleanupError("fixture", ["input"], {});
const outerCleanup = new ProviderCleanupError("fixture", ["workspace"], {});
const inner = analysisErrorWithCleanupFailure(
primary,
innerCleanup,
).bindCleanupDependencies([primary, innerCleanup]);
const outer = analysisErrorWithCleanupFailure(
inner,
outerCleanup,
).bindCleanupDependencies([inner, outerCleanup]);
const released = first === "inner" ? innerCleanup : outerCleanup;
const pending = first === "inner" ? outerCleanup : innerCleanup;
released.resolveCleanup();
expect(outer.cleanupIncomplete).toBe(true);
expect(projectAnalysisError(outer).code).toBe("cleanup_incomplete");
pending.resolveCleanup();
expect(outer.cleanupIncomplete).toBe(false);
expect(projectAnalysisError(outer)).toMatchObject(
projectAnalysisError(primary),
);
},
);
it("does not infer cleanup success when a composition has no live owner binding", () => {
const primary = new AnalysisInputError("inspect");
const cleanup = new ProviderCleanupError("fixture", ["input"], {});
const composed = analysisErrorWithCleanupFailure(primary, cleanup);
cleanup.resolveCleanup();
expect(projectAnalysisError(composed).code).toBe("cleanup_incomplete");
});
it("retains an observation attached to the composite after the primary failed", () => {
const primary = new AnalysisInputError("inspect");
const cleanup = new ProviderCleanupError("fixture", ["input"], {});
const composed = analysisErrorWithCleanupFailure(
primary,
cleanup,
).bindCleanupDependencies([primary, cleanup]);
const observation = createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: null },
{
operation: "inspect",
parameters: {},
result: { late_fact: true },
confidence: "observed",
locations: [],
limitations: [],
},
);
composed.retainPartialObservation(observation);
composed.resolveCleanup();
expect(projectAnalysisError(composed).code).toBe("cleanup_incomplete");
cleanup.resolveCleanup();
expect(projectAnalysisError(composed)).toMatchObject({
code: "invalid_request",
details: { partial_observation: observation },
});
});