254 lines
8.4 KiB
TypeScript
254 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,
|
|
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",
|
|
});
|
|
});
|
|
});
|