Files
rea/src/process/capture/ProcessCaptureError.test.ts
T

245 lines
8.3 KiB
TypeScript

import { expect, it } from "vitest";
import { AnalysisCleanupLease } from "../../artifacts/AnalysisCleanupLease.js";
import { ProcessCaptureReadFailure } from "./ProcessCaptureRead.js";
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
import { DarwinProcessOwnershipInspectionError } from "../DarwinProcessRunTokenReader.js";
import {
normalizeCaptureFailure,
ProcessCaptureError,
} from "./ProcessCaptureError.js";
import {
resolveProcessResult,
type ProcessCaptureCleanupOutcome,
} from "./ProcessCaptureLifecycle.js";
import type { ProcessCaptureCleanupReport } from "../../domain/process/processCapture.js";
const cleanupReport = {
owned_process_group: { state: "not_required" as const, reason: null },
terminal_renderer: { state: "not_required" as const, reason: null },
temporary_root: { state: "cleaned" as const, reason: null },
};
const historicalCleanupOutcome = (
report: ProcessCaptureCleanupReport,
): ProcessCaptureCleanupOutcome => ({ report, cleanupLease: undefined });
const fixture = () => {
const cleanup = new ProviderCleanupError(
"process-ownership",
["/owned/helper-root"],
{ reason: "controlled removal failure" },
);
const compiler = new DarwinProcessOwnershipInspectionError(
"compiler failed",
{
cleanupFailure: cleanup,
capturedOutput: {
stdout: "observed stdout",
stderr: "controlled compiler diagnostic",
truncated: false,
stdout_bytes: 15,
stderr_bytes: 30,
exit_code: 7,
signal: null,
},
compilerOutcome: {
outcome: "exited",
code: 7,
signal: null,
stderr: "controlled compiler diagnostic",
stderrBytes: 30,
stderrTruncated: false,
outputTruncated: false,
killed: false,
},
},
);
const error = normalizeCaptureFailure(compiler, undefined);
if (!(error instanceof ProcessCaptureError))
throw new Error("Expected normalized capture failure");
return { cleanup, compiler, error };
};
it("preserves the compiler output and live cleanup owner at capture normalization", () => {
const { cleanup, compiler, error } = fixture();
expect(error.cause).toBe(compiler);
expect(error).toMatchObject({
cleanupIncomplete: true,
cleanupResources: ["/owned/helper-root"],
capturedOutput: { stderr: "controlled compiler diagnostic", exit_code: 7 },
});
expect(projectAnalysisError(error)).toMatchObject({
code: "cleanup_incomplete",
});
cleanup.resolveCleanup();
expect(error.cleanupIncomplete).toBe(false);
expect(error.capturedOutput?.exit_code).toBe(7);
});
it("retains compiler facts and cleanup dependency through capture finalization", () => {
const { cleanup, error } = fixture();
let finalized: unknown;
try {
resolveProcessResult(
undefined,
error,
historicalCleanupOutcome(cleanupReport),
);
} catch (cause: unknown) {
finalized = cause;
}
expect(finalized).toBeInstanceOf(ProcessCaptureError);
expect(finalized).toMatchObject({
cleanupIncomplete: true,
capturedOutput: { stderr: "controlled compiler diagnostic", exit_code: 7 },
cleanupReport,
});
cleanup.resolveCleanup();
expect(finalized).toMatchObject({ cleanupIncomplete: false });
});
it("does not recreate unresolved cleanup after the exact owner already confirmed retry", () => {
const { cleanup, compiler } = fixture();
cleanup.resolveCleanup();
const normalized = normalizeCaptureFailure(compiler, undefined);
expect(normalized).toMatchObject({
cleanupIncomplete: false,
capturedOutput: { exit_code: 7 },
});
});
it("keeps later capture cleanup unresolved after compiler cleanup succeeds", () => {
const { cleanup, error } = fixture();
let finalized: unknown;
try {
resolveProcessResult(
undefined,
error,
historicalCleanupOutcome({
...cleanupReport,
temporary_root: {
state: "failed",
reason: "capture root removal denied",
},
}),
);
} catch (cause: unknown) {
finalized = cause;
}
expect(finalized).toMatchObject({
cleanupIncomplete: true,
capturedOutput: { exit_code: 7 },
cleanupResources: expect.arrayContaining([
"/owned/helper-root",
"temporary_root",
]),
});
cleanup.resolveCleanup();
expect(finalized).toMatchObject({ cleanupIncomplete: true });
});
it.each(["compiler first", "capture first"] as const)(
"keeps both cleanup owners through %s retry",
(order) => {
const { cleanup, error } = fixture();
const captureCleanup = new ProviderCleanupError(
"process-capture",
["/owned/capture-root"],
{ reason: "controlled capture root failure" },
);
const finalized = error.withFinalization({
cleanupFailure: captureCleanup,
cleanupReport,
});
const first = order === "compiler first" ? cleanup : captureCleanup;
const second = order === "compiler first" ? captureCleanup : cleanup;
first.resolveCleanup();
expect(finalized.cleanupIncomplete).toBe(true);
second.resolveCleanup();
expect(finalized.cleanupIncomplete).toBe(false);
expect(finalized.capturedOutput?.exit_code).toBe(7);
},
);
it("does not infer captured stdout from partial compiler diagnostics", () => {
const { compiler } = fixture();
const partial = new DarwinProcessOwnershipInspectionError(
"partial diagnostics",
{
compilerOutcome: compiler.compilerOutcome!,
},
);
expect(normalizeCaptureFailure(partial, undefined)).toMatchObject({
capturedOutput: undefined,
});
});
it("retains the typed completed read through finalization and confirmed cleanup", async () => {
const value = { sha256: "completed digest", identity: { dev: 1n, ino: 2n } };
const lease = new AnalysisCleanupLease(
async () => undefined,
["selected-file"],
);
const original = new ProcessCaptureReadFailure({
path: "selected-file",
outcome: { kind: "completed", value },
cleanupCause: new Error("close denied after completed read"),
cleanupLease: lease,
signal: undefined,
});
const finalized = original.withFinalization({ cleanupReport });
expect(finalized.cleanupPrimary).toBe(original);
expect(original.outcome).toEqual({ kind: "completed", value });
await lease.release();
expect(finalized.cleanupIncomplete).toBe(false);
expect(projectAnalysisError(finalized)).toMatchObject({
code: "process_capture_failed",
details: { cleanup_report: cleanupReport },
});
expect(finalized.cleanupPrimary).toBe(original);
expect(original.outcome).toEqual({ kind: "completed", value });
});
it("retains an undefined physical read rejection as an explicit typed outcome", async () => {
const lease = new AnalysisCleanupLease(
async () => undefined,
["selected-file"],
);
const original = new ProcessCaptureReadFailure({
path: "selected-file",
outcome: { kind: "failed", cause: undefined },
cleanupCause: new Error("close failure following undefined read rejection"),
cleanupLease: lease,
signal: undefined,
});
const finalized = original.withFinalization({ cleanupReport });
expect(finalized.cleanupPrimary).toBe(original);
expect(original.outcome).toEqual({ kind: "failed", cause: undefined });
expect(Object.hasOwn(original.outcome, "cause")).toBe(true);
expect(finalized.cleanupIncomplete).toBe(true);
expect(projectAnalysisError(finalized)).toMatchObject({
code: "cleanup_incomplete",
details: { cleanup_report: cleanupReport },
});
await lease.release();
expect(finalized.cleanupIncomplete).toBe(false);
expect(finalized.cleanupPrimary).toBe(original);
expect(original.outcome).toEqual({ kind: "failed", cause: undefined });
expect(projectAnalysisError(finalized)).toMatchObject({
details: { cleanup_report: cleanupReport },
});
});
it("inherits primary diagnostics without duplicating finalization history", async () => {
const primary = new ProcessCaptureError("read failed", {
diagnostics: { read_stage: "metadata" },
});
const finalized = primary.withFinalization({ cleanupReport });
const projected = projectAnalysisError(finalized);
expect(projected.details).toMatchObject({
diagnostics: { read_stage: "metadata" },
cleanup_report: cleanupReport,
});
expect(projected.details).not.toHaveProperty("cleanup_history");
});