Files
rea/tests/composition/directAnalysisCleanup.test.ts
morluto 66dcd94d09 fix(cli): route static artifact analysis without native engines
Static .app decoding failed under auto selection when multiple deep engines were installed. Compose the existing artifact provider without native candidates and reuse the shared one-shot target, Evidence and cleanup lifecycle. Preserve the selected provider environment.

Validation: real NIB filesystem and built CLI cases, artifact direct workflow and direct-analysis lifecycle tests pass (57 tests with MCP controls).
2026-10-14 13:58:12 +08:00

255 lines
8.4 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
runDirectAnalysis,
runManagedProviderExecution,
runProviderAnalysis,
} from "../../src/application/DirectAnalysis.js";
import type { DirectAnalysisDependencies } from "../../src/application/DirectAnalysisDependencies.js";
import { parseConfig } from "../../src/config/parseConfig.js";
import { analysisCliErrorEnvelopeSchema } from "../../src/contracts/errorSchemas.js";
import type { AnalysisError } from "../../src/domain/analysisErrorBase.js";
import { AnalysisTimeoutError } from "../../src/domain/analysisErrorCore.js";
import { projectAnalysisError } from "../../src/domain/analysisErrorProjection.js";
import { parseEvidence } from "../../src/domain/evidence.js";
import { ProviderCleanupError } from "../../src/domain/providerCleanupError.js";
import { err, ok } from "../../src/domain/result.js";
import { isCliOperationFailure } from "../../src/cliLogging.js";
import { observed } from "../fixtures/analysisExecution.js";
import {
createBinarySessionTargets,
createTestBinarySession,
} from "../fixtures/binarySession.js";
const cleanupError = new ProviderCleanupError(
"fixture",
["fixture-provider-process"],
{ reason: "process exit could not be confirmed" },
);
const primaryError = new AnalysisTimeoutError("read_bytes", 100, {
capturedOutput: {
stdout: "retained observation",
stderr: "",
truncated: false,
},
});
const factories = (
options: {
readonly executionError?: AnalysisError;
readonly thrownExecution?: Error;
readonly cleanupError?: AnalysisError;
readonly thrownCleanup?: Error;
} = {},
) => {
const session = createTestBinarySession(() => ({
execute: async (operation) => {
if (operation === "health") return observed(null);
if (options.thrownExecution !== undefined) throw options.thrownExecution;
return options.executionError === undefined
? observed({ operation, observation: "retained result" })
: err(options.executionError);
},
close: async () => {
if (options.thrownCleanup !== undefined) throw options.thrownCleanup;
return options.cleanupError === undefined
? ok(null)
: err(options.cleanupError);
},
}));
const dependencies: DirectAnalysisDependencies = {
readConfiguration: () => parseConfig({}),
createBinarySession: () => session,
createArtifactBinarySession: () => session,
createManagedBinarySession: () => session,
};
return { dependencies, session };
};
describe("one-shot analysis cleanup outcomes", () => {
it("retains successful evidence and reports CLI failure when close fails", async () => {
const [path] = await createBinarySessionTargets();
const { dependencies, session } = factories({ cleanupError });
const result = await runDirectAnalysis(
dependencies,
path,
"read_bytes",
{},
);
expect(result).toMatchObject({
error: "Analysis failed",
code: "cleanup_incomplete",
details: {
resources: ["fixture-provider-process"],
partial_observation: {
operation: "read_bytes",
subject: { local_path: path },
normalized_result: { observation: "retained result" },
raw_result: null,
},
},
});
expect(isCliOperationFailure(result)).toBe(true);
expect(session.activeTarget()).toBeUndefined();
});
it("preserves primary failure diagnostics alongside an unsuccessful close", async () => {
const [path] = await createBinarySessionTargets();
const { dependencies } = factories({
executionError: primaryError,
cleanupError,
});
const primary = projectAnalysisError(primaryError);
const result = await runDirectAnalysis(
dependencies,
path,
"read_bytes",
{},
);
const callerError = analysisCliErrorEnvelopeSchema.parse(result);
expect(callerError).toMatchObject({
code: "cleanup_incomplete",
category: primary.category,
message: primary.message,
details: {
execution_failure: primary.code,
captured_output: primary.details?.captured_output,
cleanup_error: projectAnalysisError(cleanupError),
},
});
expect(callerError.details).not.toHaveProperty("primary_error");
expect(
JSON.stringify(callerError).match(/retained observation/g),
).toHaveLength(1);
expect(isCliOperationFailure(result)).toBe(true);
});
it("reports provider close rejection with the successful observation", async () => {
const [path] = await createBinarySessionTargets();
const { dependencies } = factories({
thrownCleanup: new Error("unexpected close rejection"),
});
const result = await runDirectAnalysis(
dependencies,
path,
"read_bytes",
{},
);
expect(result).toMatchObject({
code: "cleanup_incomplete",
details: {
resources: ["provider-client"],
diagnostics: { reason: "provider client close rejected unexpectedly" },
partial_observation: {
normalized_result: { observation: "retained result" },
},
},
});
});
it("retains both unexpected execution failure and cleanup failure", async () => {
const [path] = await createBinarySessionTargets();
const thrownExecution = new Error("unexpected execution rejection");
const { dependencies } = factories({ thrownExecution, cleanupError });
await expect(
runDirectAnalysis(dependencies, path, "read_bytes", {}),
).rejects.toMatchObject({
cause: thrownExecution,
errors: [thrownExecution, cleanupError],
});
});
it("keeps successful evidence unchanged after confirmed cleanup", async () => {
const [path] = await createBinarySessionTargets();
const { dependencies, session } = factories();
const result = await runDirectAnalysis(
dependencies,
path,
"read_bytes",
{},
);
expect(parseEvidence(result)).toMatchObject({
operation: "read_bytes",
normalized_result: { observation: "retained result" },
});
expect(isCliOperationFailure(result)).toBe(false);
expect(session.activeTarget()).toBeUndefined();
});
});
describe("managed one-shot cleanup outcomes", () => {
it("returns a managed cleanup error retaining the complete successful execution", async () => {
const [path] = await createBinarySessionTargets();
const { dependencies } = factories({ cleanupError });
const result = await runManagedProviderExecution(
dependencies,
path,
"inspect_managed_artifact",
);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(projectAnalysisError(result.error)).toMatchObject({
code: "cleanup_incomplete",
details: {
resources: ["fixture-provider-process"],
diagnostics: {
cleanup_error: projectAnalysisError(cleanupError),
partial_observation: {
result: {
operation: "inspect_managed_artifact",
observation: "retained result",
},
rawResult: null,
provider: { id: "fixture" },
},
},
},
});
});
it("retains the typed managed primary error when close also fails", async () => {
const [path] = await createBinarySessionTargets();
const { dependencies } = factories({
executionError: primaryError,
cleanupError,
});
const result = await runManagedProviderExecution(
dependencies,
path,
"inspect_managed_artifact",
);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.cause).toBeInstanceOf(AggregateError);
expect(result.error.cause).toMatchObject({
cause: primaryError,
errors: [primaryError, cleanupError],
});
expect(projectAnalysisError(result.error)).toMatchObject({
code: "cleanup_incomplete",
details: {
diagnostics: {
primary_error: projectAnalysisError(primaryError),
cleanup_error: projectAnalysisError(cleanupError),
},
},
});
});
it("exposes managed cleanup failure to the CLI error classifier", async () => {
const [path] = await createBinarySessionTargets();
const { dependencies } = factories({ cleanupError });
const result = await runProviderAnalysis(
dependencies,
path,
"inspect_managed_artifact",
{},
);
expect(isCliOperationFailure(result)).toBe(true);
expect(result).toMatchObject({
error: "Analysis failed",
code: "cleanup_incomplete",
});
});
});