Files
reverse-engineering-anything/src/providers/OwnedCommandCleanup.test.ts
T
morluto ab84ba6839 fix(process)!: retain inspection cancellation and cleanup ownership (#10113)
* fix(process): preserve producer-owned inspection cancellation

Prototype the explicit inspection origin and retained file/batch owner protocol. Downstream adapter migration and lease binding remain required before integration.

* fix(process): type inspection ownership handoffs precisely

* fix(process): retain plain inspection preparation cancellation

* fix(process): retain explicit inspection cleanup and cancellation facts

* fix(process): require explicit inspection facts and cleanup scope

* refactor(process): remove redundant cleanup forwarding wrapper

* fix(firmware): retain inspection cancellation and cleanup ownership

* fix(wakaru): retain typed inspection outcomes and caller scope

* fix(ghidra): preserve inspection facts and launch cleanup scope

* fix(providers): preserve typed process inspection outcomes

* refactor(process): remove obsolete inspection cancellation re-export

* fix(native): preserve inspection preparation cleanup and cancellation

* fix(registration): retain typed inspection cleanup during Git discovery

* fix(process): preserve inspection deadline origin in adapter handoff

* fix(process): consume the canonical inspection selector in binding

* fix(native): preserve inspection deadlines and cleanup retry completion

* test(process): narrow the retained Darwin preparation owner

* fix(providers): preserve preparation ownership and timeout policy

Narrow preparation facts through their canonical owner, preserve actual cleanup capabilities and recorded cancellation, and keep enclosing command deadlines authoritative. Retain raw inspection records through Java refusal and configuration projections without changing availability fallback policy.

* refactor(process): use one inspection failure type owner

* fix(providers): retain recorded secondary failures when rebinding

* fix(ghidra): preserve normalized inspection facts through public projection

* fix(process): normalize native process listing cancellation

* docs(development): clarify failure provenance and cleanup verification

* fix(process): preserve direct inspection cancellation facts

* fix(process): retain inspection owners through termination

* fix(process): preserve command inspection cleanup provenance

* fix(desktop): preserve inspection facts after cleanup retry

* refactor(process)!: migrate desktop capture with inspection producers

* fix(desktop)!: preserve capture failure origins and cleanup owners (#10115)

* fix(desktop): preserve capture failure origins and cleanup

* fix(desktop): type keyring cleanup cancellation

* fix(kuna)!: retain copy cleanup before retiring workspaces (#10114)

* fix(kuna): own copy cancellation and descriptor cleanup

* fix(kuna): close copy descriptors before workspace removal

* test(kuna): type copy lifecycle gates for project library

* docs(kuna): explain retained copy-file cleanup and staging ownership

* docs(kuna): separate cache publication, integrity and cleanup guidance

* fix(desktop): retain sampling inspection owners before release

* fix(process): preserve capture file read cancellation origin

* fix(process): retain capture inspection cancellation and cleanup owners

* fix(process): retain sampling inspection failures in capture scope

* fix(process): retain inspection failures through mandatory cleanup

* fix(process): distinguish sampler shutdown from capture cancellation

* fix(browser): retain startup EOF before later cancellation

* fix(process): bind capture inspection cleanup to the owning run

* test(process): migrate cancellation fixtures to typed owners

Record explicit preparation and command cancellation facts, retain exact opaque reasons and physical causes, and guard the mocked version probe from native preparation. Keep the live Wakaru command control while adding a recording equivalent for bounded source verification.

* fix(process): own sampling reads and settle inspection peers

* fix(errors): retain authored recovery through cleanup composition

* fix(process): retain all sampling inspection leaves for final cleanup

* test(browser): report recording cleanup signal outcome

* fix(process): keep inspection error context out of implicit JSON

* fix(desktop): retain normalized sampling cleanup ownership

* test(process): assert tagged startup cancellation
2026-11-11 16:37:48 -05:00

366 lines
12 KiB
TypeScript

import { systemProcessOwnershipHost } from "../process/ProcessOwnershipObservation.js";
import { access, mkdir } from "node:fs/promises";
import { join } from "node:path";
import { expect, it, onTestFinished, vi } from "vitest";
import {
processInspectionCancellationError,
retainOwnedCommandCleanup,
retainProcessInspectionCleanup,
} from "./OwnedCommandCleanup.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
import { primaryAnalysisError } from "../domain/analysisErrorBase.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { DarwinProcessOwnershipInspectionError } from "../process/DarwinProcessRunTokenReader.js";
import {
OwnedCommandFailure,
prepareOwnedCommand,
} from "../process/OwnedCommand.js";
import {
ProcessOwnershipIdentityBatchFailure,
ProcessOwnershipInspectionCancelled,
} from "../process/ProcessInspectionCancellation.js";
import { RetainedCommandCleanup } from "../process/RetainedCommandCleanup.js";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import { failedInspectionDescriptor } from "../../tests/fixtures/processInspectionCleanup.js";
it.each([false, true])(
"retains the exact descriptor preparation owner (command wrapper: %s)",
async (wrapped) => {
const file = await failedInspectionDescriptor();
const retained: {
close(): Promise<void>;
readonly cleanupResources?: readonly string[] | undefined;
}[] = [];
const cause = wrapped
? new OwnedCommandFailure("process", "inspection failed", null, null, {
preparationFailure: file.failure,
})
: file.failure;
const errors = retainOwnedCommandCleanup(
cause,
{ retain: (owner) => retained.push(owner), run: (work) => work() },
"fixture",
"inspect",
);
expect(errors).toHaveLength(1);
expect(retained).toHaveLength(1);
expect(errors[0]?.cleanupIncomplete).toBe(true);
expect(errors[0]?.cleanupResources).toEqual([file.path]);
await expect(retained[0]!.close()).rejects.toBeDefined();
expect(file.handle.fd).toBeGreaterThanOrEqual(0);
file.allow();
await retained[0]!.close();
expect(file.handle.fd).toBe(-1);
expect(errors[0]?.cleanupIncomplete).toBe(false);
},
);
it("drains every exact mixed-batch owner and keeps partial observations and physical primary", async () => {
const first = await failedInspectionDescriptor();
const second = await failedInspectionDescriptor();
const reason = { caller: "stop identity inspection" };
const observations = new Map([
[42, { state: "unavailable" as const, reason: "identity unknown" }],
]);
const batch = new ProcessOwnershipIdentityBatchFailure(
[
{ pid: 41, requestIndex: 0, cause: first.failure },
{ pid: 41, requestIndex: 1, cause: second.failure },
{
pid: 42,
requestIndex: 2,
cause: new ProcessOwnershipInspectionCancelled(reason),
},
],
observations,
[{ pid: 41, requestIndex: 0, cause: first.failure }],
);
const resources = new ArtifactResourceScope();
onTestFinished(async () => {
first.allow();
second.allow();
await resources.close();
});
const primary = new AnalysisOutputError(
"inspect",
"mixed identity batch failed",
{ cause: batch },
);
const error = retainProcessInspectionCleanup(
batch,
primary,
resources,
"inspect",
);
expect(primaryAnalysisError(error)).toBe(primary);
expect(primary.cause).toBe(batch);
expect(batch.observations).toBe(observations);
expect(batch.origin).toBe("physical");
expect(error.cleanupIncomplete).toBe(true);
expect(resources.cleanupResources).toEqual([first.path, second.path]);
first.allow();
await expect(resources.close()).rejects.toBeDefined();
expect(first.handle.fd).toBe(-1);
expect(second.handle.fd).toBeGreaterThanOrEqual(0);
expect(resources.cleanupResources).toEqual([second.path]);
expect(error.cleanupResources).toEqual([second.path]);
const firstCloses = first.closes();
second.allow();
await resources.close();
expect(first.closes()).toBe(firstCloses);
expect(second.handle.fd).toBe(-1);
expect(error.cleanupIncomplete).toBe(false);
});
it.each(["descriptor", "batch", "darwin", "command"] as const)(
"retains a scratch folder until its %s inspection owner confirms close",
async (kind) => {
const root = await createTestTempDirectory("rea-inspection-scratch-");
const folder = join(root, "scratch");
await mkdir(folder);
const file = await failedInspectionDescriptor(folder);
const failure =
kind === "descriptor"
? file.failure
: kind === "command"
? new OwnedCommandFailure(
"process",
"inspection failed",
null,
null,
{ preparationFailure: file.failure },
)
: kind === "batch"
? new ProcessOwnershipIdentityBatchFailure(
[{ pid: 42, requestIndex: 0, cause: file.failure }],
new Map(),
)
: new DarwinProcessOwnershipInspectionError(
"helper cleanup failed",
{
cleanupOwner: file.failure.owner,
cleanupFailure: new ProviderCleanupError(
"fixture",
[file.path],
{},
),
},
);
const owner = new RetainedCommandCleanup(folder, failure);
expect(owner.hasPendingCommandResources).toBe(true);
await expect(owner.close()).rejects.toBeDefined();
await expect(access(folder)).resolves.toBeUndefined();
expect(file.handle.fd).toBeGreaterThanOrEqual(0);
file.allow();
await owner.close();
expect(file.handle.fd).toBe(-1);
await expect(access(folder)).rejects.toMatchObject({ code: "ENOENT" });
},
);
it("does not acquire foreign owners from opaque cancellation reasons or generic causes", async () => {
const foreign = await failedInspectionDescriptor();
const cancellation = new ProcessOwnershipInspectionCancelled(
foreign.failure,
{ cause: foreign.failure },
);
const allCancelled = new ProcessOwnershipIdentityBatchFailure(
[{ pid: 42, requestIndex: 0, cause: cancellation }],
new Map(),
);
expect(allCancelled.origin).toBe("cancelled");
for (const cause of [
cancellation,
allCancelled,
new Error("opaque failure", { cause: foreign.failure }),
new OwnedCommandFailure("cancelled", "caller stopped", null, null, {
cancellation: { reason: foreign.failure },
cause: foreign.failure,
}),
]) {
const retained: unknown[] = [];
expect(
retainOwnedCommandCleanup(
cause,
{ retain: (owner) => retained.push(owner), run: (work) => work() },
"fixture",
"inspect",
),
).toEqual([]);
expect(retained).toEqual([]);
expect(foreign.handle.fd).toBeGreaterThanOrEqual(0);
}
});
it.each([undefined, NaN, false])(
"keeps the all-cancel batch and exact caller reason %s",
(reason) => {
const batch = new ProcessOwnershipIdentityBatchFailure(
[
{
pid: 42,
requestIndex: 0,
cause: new ProcessOwnershipInspectionCancelled(reason),
},
],
new Map([[43, { state: "unavailable", reason: "unknown" }]]),
);
const error = processInspectionCancellationError(batch, "inspect");
expect(error?.inspectionFailure).toBe(batch);
expect(error?.cause).toBe(reason);
expect(error?.diagnostics).toEqual({
retained_observation_count: 1,
rejected_request_count: 1,
});
expect(
processInspectionCancellationError(
new Error("opaque", { cause: batch }),
"inspect",
),
).toBeUndefined();
},
);
it("retains only explicit mixed cancellations once without an ambient abort", async () => {
const file = await failedInspectionDescriptor();
const batch = new ProcessOwnershipIdentityBatchFailure(
[
{ pid: 42, requestIndex: 0, cause: file.failure },
{
pid: 43,
requestIndex: 1,
cause: new ProcessOwnershipInspectionCancelled(undefined),
},
{
pid: 44,
requestIndex: 2,
cause: new ProcessOwnershipInspectionCancelled(undefined),
},
{
pid: 45,
requestIndex: 3,
cause: new ProcessOwnershipInspectionCancelled(NaN),
},
{
pid: 46,
requestIndex: 4,
cause: new ProcessOwnershipInspectionCancelled(NaN),
},
],
new Map(),
);
expect(processInspectionCancellationError(batch, "inspect")).toBeUndefined();
const primary = new AnalysisOutputError("inspect", "physical", {
cause: batch,
});
const resources = new ArtifactResourceScope();
const projected = retainProcessInspectionCleanup(
batch,
primary,
resources,
"inspect",
);
expect(primaryAnalysisError(projected)).toBe(primary);
expect(primary.secondaryFailures.map((failure) => failure.cause)).toEqual([
undefined,
NaN,
]);
expect(
primary.secondaryFailures.every(
(failure) =>
"inspectionFailure" in failure && failure.inspectionFailure === batch,
),
).toBe(true);
expect(projected.cleanupResources).toEqual([file.path]);
expect(projected.cleanupIncomplete).toBe(true);
file.allow();
await resources.close();
expect(projected.cleanupIncomplete).toBe(false);
expect(file.handle.fd).toBe(-1);
});
it("preserves Darwin cancellation output and does not invent cleanup authority", () => {
const cleanup = new ProviderCleanupError("fixture", ["exact helper"], {
reason: "failed close",
});
const cause = new DarwinProcessOwnershipInspectionError("cancelled helper", {
cancellation: { reason: false },
cleanupFailure: cleanup,
capturedOutput: {
stdout: "partial",
stderr: "",
truncated: false,
},
});
const error = processInspectionCancellationError(cause, "inspect");
expect(error?.inspectionFailure).toBe(cause);
expect(error?.cause).toBe(false);
expect(error?.capturedOutput).toEqual(cause.capturedOutput);
const retained: unknown[] = [];
expect(
retainOwnedCommandCleanup(
cause,
{ retain: (owner) => retained.push(owner), run: (work) => work() },
"fixture",
"inspect",
),
).toEqual([cleanup]);
expect(retained).toEqual([]);
expect(cleanup.cleanupIncomplete).toBe(true);
});
it("keeps executed-command cancellation outside the inspection factory and binder", () => {
const command = new OwnedCommandFailure(
"cancelled",
"execution stopped",
null,
null,
{
cancellation: { reason: false },
cause: new ProcessOwnershipInspectionCancelled(false),
},
);
expect(
processInspectionCancellationError(command, "inspect"),
).toBeUndefined();
const primary = new AnalysisOutputError("inspect", "physical execution");
const error = retainProcessInspectionCleanup(
command,
primary,
new ArtifactResourceScope(),
"inspect",
);
expect(error).toBe(primary);
expect(primary.secondaryFailures).toEqual([]);
});
it("preserves the actual plain preparation fact through the command and adapter owners", async () => {
const platform = Object.getOwnPropertyDescriptor(
systemProcessOwnershipHost,
"platform",
)!;
Object.defineProperty(systemProcessOwnershipHost, "platform", {
value: "darwin",
configurable: true,
});
const fact = new ProcessOwnershipInspectionCancelled(false);
const prepare = vi
.spyOn(systemProcessOwnershipHost, "prepare")
.mockRejectedValue(fact);
onTestFinished(() => {
prepare.mockRestore();
Object.defineProperty(systemProcessOwnershipHost, "platform", platform);
});
const command: unknown = await prepareOwnedCommand().catch(
(failure: unknown) => failure,
);
expect(command).toBeInstanceOf(OwnedCommandFailure);
if (!(command instanceof OwnedCommandFailure)) throw command;
expect(command.preparationFailure).toBe(fact);
const error = processInspectionCancellationError(command, "inspect");
expect(error?.inspectionFailure).toBe(fact);
expect(error?.cause).toBe(false);
});