mirror of
https://github.com/morluto/rea
synced 2026-10-27 14:42:18 -04:00
245 lines
8.3 KiB
TypeScript
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");
|
|
});
|