Files
reverse-engineering-anything/src/application/binary/AnalysisClientCleanup.test.ts
T

144 lines
4.9 KiB
TypeScript

import { expect, it } from "vitest";
import {
projectAnalysisError,
projectAnalysisErrorWithoutCapturedOutput,
} from "../../domain/analysisErrorProjection.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import { err } from "../../domain/result.js";
import type { AnalysisClient } from "../AnalysisProvider.js";
import { closeAnalysisClient } from "../AnalysisClientCleanup.js";
import { analysisErrorWithCleanupFailure } from "../../domain/analysisErrorCleanup.js";
const clientClosingWith = (cause: unknown): AnalysisClient => ({
execute: () => Promise.resolve(err(new ProviderCleanupError("x", [], {}))),
close: () => Promise.reject(cause),
});
it("keeps the leftover resources a typed cleanup failure names", async () => {
const leftover = "/tmp/rea-ghidra-fixture";
const failure = new ProviderCleanupError("ghidra", [leftover], {
reason: "runtime directory retained",
});
const closed = await closeAnalysisClient(
clientClosingWith(failure),
"ghidra",
);
expect(closed.ok).toBe(false);
if (!closed.ok)
expect(projectAnalysisError(closed.error)).toMatchObject({
code: "cleanup_incomplete",
details: { resources: [leftover] },
});
});
it("still reports an untyped close rejection as the provider client", async () => {
const closed = await closeAnalysisClient(
clientClosingWith(new Error("unexpected")),
"ghidra",
);
expect(closed.ok).toBe(false);
if (!closed.ok)
expect(projectAnalysisError(closed.error)).toMatchObject({
code: "cleanup_incomplete",
details: { resources: ["provider-client"] },
});
});
it("preserves the original explanation and collected output alongside cleanup uncertainty", () => {
const primary = new ProviderAdapterError("fixture", "capture", {
userMessage: "Select the running target process and retry the capture.",
capturedOutput: {
stdout: "partial observation",
stderr: "native reason",
truncated: false,
},
});
const cleanup = new ProviderCleanupError("fixture", ["owned-process"], {
reason: "Process termination was not confirmed",
});
const combined = analysisErrorWithCleanupFailure(primary, cleanup);
const projected = projectAnalysisError(combined);
expect(projected).toMatchObject({
code: "cleanup_incomplete",
message: primary.userMessage,
details: {
resources: ["owned-process"],
captured_output: primary.capturedOutput,
diagnostics: {
primary_error: projectAnalysisErrorWithoutCapturedOutput(primary),
cleanup_error: projectAnalysisError(cleanup),
},
},
});
expect(projected).not.toHaveProperty(
"details.diagnostics.primary_error.details.captured_output",
);
expect(JSON.stringify(projected).split("partial observation")).toHaveLength(
2,
);
expect(JSON.stringify(projected).split("native reason")).toHaveLength(2);
expect(combined.cause).toBeInstanceOf(AggregateError);
expect(combined.cause).toMatchObject({
cause: primary,
errors: [primary, cleanup],
});
});
it.each(["return", "reject"] as const)(
"reports a nested owner's failure once when close also %ss it",
async (outcome) => {
const failure = new ProviderCleanupError("fixture", ["owned-process"], {});
const notifications: unknown[] = [];
const child: AnalysisClient = {
execute: clientClosingWith(failure).execute,
close: (options) => {
options?.onCleanupFailure?.(failure);
return outcome === "return"
? Promise.resolve(err(failure))
: Promise.reject(failure);
},
};
const parent: AnalysisClient = {
execute: child.execute,
close: (options) => closeAnalysisClient(child, "fixture", options),
};
const closed = await closeAnalysisClient(parent, "composite", {
onCleanupFailure: (error) => notifications.push(error),
});
expect(closed).toEqual(err(failure));
expect(notifications).toEqual([failure]);
},
);
it("reports distinct failures during final cleanup and reports again on retry", async () => {
const first = new ProviderCleanupError("first", ["first-resource"], {});
const final = new ProviderCleanupError("second", ["second-resource"], {});
const notifications: unknown[] = [];
const client: AnalysisClient = {
execute: clientClosingWith(first).execute,
close: (options) => {
options?.onCleanupFailure?.(first);
return Promise.resolve(err(final));
},
};
const options = {
onCleanupFailure: (error: unknown) => notifications.push(error),
};
expect(await closeAnalysisClient(client, "composite", options)).toEqual(
err(final),
);
expect(notifications).toEqual([first, final]);
expect(await closeAnalysisClient(client, "composite", options)).toEqual(
err(final),
);
expect(notifications).toEqual([first, final, first, final]);
});