Reuse managed snapshot admission and hashing in sequential path comparisons, retaining caller paths and completed inspection Evidence on later failure. Preserve exact failed-close owners alongside typed read and cancellation failures, and retry them through caller-owned resource scopes. Require canonical Hopper lease identities, remove signed offset coercion, and repair stale setup cleanup expectations that failed CI.
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",
|
|
});
|
|
});
|
|
});
|