import { open, writeFile, type FileHandle } from "node:fs/promises"; import { join } from "node:path"; import { expect, it, vi, onTestFinished } from "vitest"; import { NativeMacOSProvider } from "../../../../src/native/NativeMacOSProvider.js"; import { ArtifactFileCleanupFailure } from "../../../../src/artifacts/ArtifactFileOperation.js"; import { NativeFixtureRunner, nativeMachoTarget, } from "../../../fixtures/nativeCommands.js"; import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; import { OwnedFileHandle } from "../../../../src/filesystem/OwnedFileHandle.js"; import { ProviderCleanupError } from "../../../../src/domain/providerCleanupError.js"; import { resolvedAnalysisError } from "../../../../src/domain/analysisErrorBase.js"; import { AnalysisCleanupLease } from "../../../../src/artifacts/AnalysisCleanupLease.js"; import { ArtifactReaderFailure } from "../../../../src/artifacts/ArtifactReader.js"; import { err, ok } from "../../../../src/domain/result.js"; import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js"; const fixture = async () => { const root = await createTestTempDirectory("rea-native-plist-lifetime-"); const path = join(root, "selected.plist"); await writeFile(path, "value"); const probe = await open(path, "r"); const prototype = Object.getPrototypeOf(probe) as FileHandle; await probe.close(); const runner = new NativeFixtureRunner({ file: "ASCII text", plutil: '"value"', }); const client = new NativeMacOSProvider({}, runner, "darwin").createClient( nativeMachoTarget(join(root, "target")), ); onTestFinished(async () => { await client.close(); }); return { path, prototype, runner, client }; }; it("does not adopt a command's arbitrary thrown cleanup marker as a file owner", async () => { const { client, path, runner } = await fixture(); const foreign = ArtifactReaderFailure.withCleanup( new Error("opaque command failure"), { resources: ["unowned"], reason: "foreign marker" }, ); const run = vi.spyOn(runner, "run").mockRejectedValueOnce(foreign); onTestFinished(() => run.mockRestore()); const result = await client.execute("inspect_plist", { path }); expect(result.ok).toBe(false); if (result.ok) throw new Error("Expected opaque command failure"); expect(result.error.cause).toBe(foreign); expect(result.error.cleanupIncomplete).toBe(false); expect(result.error.cleanupResources).toEqual([]); }); it("keeps mixed client cleanup live until both file and tracer owners resolve", async () => { const root = await createTestTempDirectory("rea-native-mixed-close-"); const path = join(root, "selected.plist"); await writeFile(path, "value"); const probe = await open(path, "r"); const prototype = Object.getPrototypeOf(probe) as FileHandle; await probe.close(); const traceFailure = new ProviderCleanupError( "native-macos", ["trace-owner"], {}, ); const traceLease = new AnalysisCleanupLease(async () => {}, ["trace-owner"]); traceLease.bindCleanupError(traceFailure); let traceAttempts = 0; const client = new NativeMacOSProvider( {}, new NativeFixtureRunner({ file: "ASCII text", plutil: '"value"' }), "darwin", () => ({ trace: async () => { throw new Error("Trace not requested"); }, close: async () => { traceAttempts += 1; if (traceAttempts < 3) return err(traceFailure); await traceLease.release(); return ok(null); }, }), ).createClient(nativeMachoTarget(join(root, "target"))); let release: (() => Promise) | undefined; const original = prototype.read; let fileAttempts = 0; const read = vi .spyOn(prototype, "read") .mockImplementationOnce(async function ( this: FileHandle, ...args: Parameters ) { release = this.close.bind(this); this.close = async () => { fileAttempts += 1; if (fileAttempts < 3) throw new Error("file close failed"); await release!(); }; return Reflect.apply(original, this, args); }); onTestFinished(async () => { read.mockRestore(); await release?.(); await traceLease.release(); await client.close(); }); expect((await client.execute("inspect_plist", { path })).ok).toBe(false); const first = await client.close(); expect(first.ok).toBe(false); if (first.ok) throw new Error("Expected both pending owners"); expect(new Set(first.error.cleanupResources)).toEqual( new Set([path, "trace-owner"]), ); expect((await client.close()).ok).toBe(false); expect(first.error.cleanupIncomplete).toBe(true); expect(first.error.cleanupResources).toEqual(["trace-owner"]); expect((await client.close()).ok).toBe(true); expect(first.error.cleanupIncomplete).toBe(false); expect(first.error.cleanupResources).toEqual([]); }); it.for(["path-input", "target"] as const)( "retains %s prefix read and close failures through repeated client cleanup", async (selection, { onTestFinished }) => { const root = await createTestTempDirectory("rea-native-plist-close-"); const path = join(root, "selected.plist"); await writeFile(path, "value"); const probe = await open(path, "r"); const prototype = Object.getPrototypeOf(probe) as FileHandle; await probe.close(); const primary = Object.assign(new Error("controlled prefix read failure"), { code: "EIO", }); const cleanup = new Error("controlled prefix close failure"); let attempts = 0; let release: (() => Promise) | undefined; const read = vi .spyOn(prototype, "read") .mockImplementationOnce(async function (this: FileHandle) { release = this.close.bind(this); this.close = async () => { attempts += 1; if (attempts <= 2) throw cleanup; await release!(); }; throw primary; }); onTestFinished(async () => { read.mockRestore(); await release?.(); }); const client = new NativeMacOSProvider( {}, new NativeFixtureRunner({ file: "ASCII text", plutil: '"value"' }), "darwin", ).createClient( selection === "target" ? { kind: "artifact", format: "plist", path, sha256: "0".repeat(64) } : nativeMachoTarget(join(root, "target")), ); onTestFinished(async () => { await client.close(); }); const result = await client.execute( "inspect_plist", selection === "target" ? {} : { path }, ); expect(result.ok).toBe(false); if (result.ok) throw new Error("Expected retained cleanup failure"); expect(result.error).toMatchObject({ cleanupIncomplete: true, operation: "inspect_plist", }); expect(result.error.cleanupResources).toContain(path); expect(result.error.capturedOutput?.stdout).toBe('"value"'); const projected = projectAnalysisError(result.error); expect(JSON.stringify(projected).match(/captured_output/gu)).toHaveLength( 1, ); expect(result.error.cause).toBeInstanceOf(ArtifactFileCleanupFailure); const failure = result.error.cause as ArtifactFileCleanupFailure; expect(failure.fileCleanupFailure.outcome).toEqual({ kind: "failed", cause: primary, }); expect(failure.fileCleanupFailure.cleanupCause).toBe(cleanup); const first = await client.close(); expect(first.ok).toBe(false); expect(attempts).toBe(2); const retried = await client.close(); expect(retried.ok).toBe(true); expect(attempts).toBe(3); expect(result.error.cleanupIncomplete).toBe(false); expect(resolvedAnalysisError(result.error).cause).toBe(primary); expect(resolvedAnalysisError(result.error).capturedOutput?.stdout).toBe( '"value"', ); if (!first.ok) expect(first.error.cleanupIncomplete).toBe(false); }, ); for (const readFails of [false, true]) it(`retains a ${readFails ? "failed" : "completed"} read when cancellation and close failure coincide`, async () => { const { client, path, prototype } = await fixture(); const controller = new AbortController(); const primary = new Error("physical read failed before cancellation"); const cleanup = new Error("physical close failed"); const reason = { caller: "final-close" }; const original = prototype.read; let release: (() => Promise) | undefined; let attempts = 0; const read = vi .spyOn(prototype, "read") .mockImplementationOnce(async function ( this: FileHandle, ...args: Parameters ) { release = this.close.bind(this); this.close = async () => { attempts += 1; if (attempts === 1) { controller.abort(reason); throw cleanup; } await release!(); }; if (readFails) throw primary; return Reflect.apply(original, this, args); }); onTestFinished(async () => { read.mockRestore(); await release?.(); }); const result = await client.execute( "inspect_plist", { path }, { signal: controller.signal }, ); expect(result.ok).toBe(false); if (result.ok) throw new Error("Expected retained close failure"); const failure = result.error.cause; expect(failure).toBeInstanceOf(ArtifactFileCleanupFailure); if (!(failure instanceof ArtifactFileCleanupFailure)) throw new Error("Expected physical owner"); expect(failure.fileCleanupFailure.outcome).toEqual( readFails ? { kind: "failed", cause: primary } : { kind: "completed", value: "xml" }, ); expect(result.error.cleanupIncomplete).toBe(true); expect((await client.close()).ok).toBe(true); expect(result.error.cleanupIncomplete).toBe(false); expect(resolvedAnalysisError(result.error).cause).toBe( readFails ? primary : reason, ); }); it.each([null, 0, new ProviderCleanupError("foreign", ["unowned"], {})])( "preserves pre-admission abort reason %s without adopting its resource authority", async (reason) => { const { client, path, runner } = await fixture(); const run = vi.spyOn(runner, "run"); onTestFinished(() => run.mockRestore()); const controller = new AbortController(); controller.abort(reason); const result = await client.execute( "inspect_plist", { path }, { signal: controller.signal }, ); expect(result.ok).toBe(false); if (result.ok) throw new Error("Expected pre-admission cancellation"); expect(result.error.cause).toBe(reason); expect(result.error.cleanupIncomplete).toBe(false); expect(result.error.cleanupResources).toEqual([]); expect(run).not.toHaveBeenCalled(); }, ); it("retains a failed admission close before any native command", async () => { const { client, path, runner } = await fixture(); const run = vi.spyOn(runner, "run"); const close = vi .spyOn(OwnedFileHandle.prototype, "close") .mockRejectedValueOnce(new Error("controlled admission close failure")); onTestFinished(() => { close.mockRestore(); run.mockRestore(); }); const result = await client.execute("inspect_plist", { path }); expect(result.ok).toBe(false); if (result.ok) throw new Error("Expected failed admission close"); expect(result.error.cleanupResources).toContain(path); expect(result.error.cleanupIncomplete).toBe(true); expect(run).not.toHaveBeenCalled(); expect((await client.close()).ok).toBe(true); expect(result.error.cleanupIncomplete).toBe(false); }); it("preserves an opaque cancellation observed during successful prefix close", async () => { const { client, path, prototype } = await fixture(); const controller = new AbortController(); const reason = new ProviderCleanupError("foreign", ["unowned-resource"], {}); const original = prototype.read; const read = vi .spyOn(prototype, "read") .mockImplementationOnce(async function ( this: FileHandle, ...args: Parameters ) { const closed = this.close.bind(this); this.close = async () => { await closed(); controller.abort(reason); }; return Reflect.apply(original, this, args); }); onTestFinished(() => read.mockRestore()); const result = await client.execute( "inspect_plist", { path }, { signal: controller.signal }, ); expect(result.ok).toBe(false); if (result.ok) throw new Error("Expected late cancellation"); expect(result.error).toMatchObject({ operation: "inspect_plist", cause: reason, cleanupIncomplete: false, }); expect(result.error.cleanupResources).not.toContain("unowned-resource"); expect((await client.close()).ok).toBe(true); }); it("joins an admitted prefix read when client close starts concurrently", async () => { const { client, path, prototype } = await fixture(); let enter!: () => void; let resume!: () => void; const entered = new Promise((resolve) => { enter = resolve; }); const gate = new Promise((resolve) => { resume = resolve; }); const original = prototype.read; const read = vi .spyOn(prototype, "read") .mockImplementationOnce(async function ( this: FileHandle, ...args: Parameters ) { enter(); await gate; return Reflect.apply(original, this, args); }); onTestFinished(() => { resume(); read.mockRestore(); }); const inspecting = client.execute("inspect_plist", { path }); await entered; let finished = false; const closing = client.close().then((result) => { finished = true; return result; }); await new Promise((resolve) => setImmediate(resolve)); expect(finished).toBe(false); resume(); expect((await inspecting).ok).toBe(true); expect((await closing).ok).toBe(true); });