Files
reverse-engineering-anything/tests/process-global/cutterDescriptorLifetime.test.ts
T
morluto 2b7b698db8 fix(cutter): retain descriptor owners and join discovery cancellation
Refs #4471, #3946.

Read only admitted regular descriptors without blocking on pipes. Retain failed closes, join discovery workers, and propagate cancellation through CLI and MCP. Preserve command and descriptor cleanup as independent owners.
2026-10-17 00:44:02 +08:00

266 lines
9.1 KiB
TypeScript

import { execFile } from "node:child_process";
import { writeFile, type FileHandle } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { afterEach, beforeEach, expect, it, vi } from "vitest";
const fault = vi.hoisted(() => ({
root: "",
blocked: new Set<FileHandle>(),
handles: [] as FileHandle[],
onOpen: undefined as (() => void) | undefined,
onClose: undefined as ((handle: FileHandle) => Promise<void>) | undefined,
}));
vi.mock("node:fs/promises", async (importOriginal) => {
const original = await importOriginal<typeof import("node:fs/promises")>();
return {
...original,
open: async (...args: Parameters<typeof original.open>) => {
const handle = await original.open(...args);
if (!fault.root || !String(args[0]).startsWith(fault.root)) return handle;
const close = handle.close.bind(handle);
fault.handles.push(handle);
fault.blocked.add(handle);
handle.close = async () => {
await fault.onClose?.(handle);
if (fault.blocked.has(handle))
throw new Error("controlled Cutter descriptor close failure");
await close();
};
fault.onOpen?.();
return handle;
},
};
});
import { CutterBridgeClient } from "../../src/cutter/CutterBridgeClient.js";
import { AnalysisCancelledError } from "../../src/domain/analysisErrorCore.js";
import { AnalysisError } from "../../src/domain/analysisErrorBase.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { runCliJson } from "../fixtures/cliJsonProcess.js";
beforeEach(() => {
fault.root = "";
fault.blocked.clear();
fault.handles = [];
fault.onClose = undefined;
fault.onOpen = undefined;
});
afterEach(async () => {
fault.blocked.clear();
fault.onClose = undefined;
for (const handle of fault.handles) await handle.close();
});
const descriptor = async (root: string, index: number) => {
const session_id = `00000000-0000-4000-8000-${String(index).padStart(12, "0")}`;
const path = join(root, `cutter-${process.pid}-${session_id}.json`);
await writeFile(
path,
JSON.stringify({
session_id,
pid: process.pid,
host: "127.0.0.1",
port: 9,
token: "fixture-token-never-sent-to-network",
document_generation: 0,
current_file: null,
cutter_version: null,
identity_status: "partial",
}),
{ mode: 0o600 },
);
return path;
};
it.skipIf(process.platform === "win32")(
"public CLI skips a named-pipe descriptor without waiting for a writer",
async () => {
const root = await createTestTempDirectory("rea-cutter-fifo-");
const fifo = join(
root,
"cutter-1-00000000-0000-4000-8000-000000000001.json",
);
await promisify(execFile)("mkfifo", [fifo]);
const result = await runCliJson(["list-cutter-sessions", "--json"], {
...process.env,
REA_CUTTER_BRIDGE_DIR: root,
});
expect(result).toMatchObject({
sessions: [],
discovery_status: "no_live_bridge_found",
bridge_directory_security: "private_verified",
});
},
);
it("retains exact failed descriptors and keeps aggregate status until every close succeeds", async () => {
const root = await createTestTempDirectory("rea-cutter-descriptor-owners-");
await Promise.all([descriptor(root, 1), descriptor(root, 2)]);
fault.root = root;
const client = new CutterBridgeClient({ REA_CUTTER_BRIDGE_DIR: root });
try {
const original = await client
.listDescriptors()
.catch((cause: unknown) => cause);
if (!(original instanceof AnalysisError))
throw new Error("Expected typed cleanup aggregate");
expect(original.cleanupIncomplete).toBe(true);
expect(original.cleanupResources).toHaveLength(2);
expect(fault.handles).toHaveLength(2);
const retry = await client
.listDescriptors()
.catch((cause: unknown) => cause);
if (!(retry instanceof AnalysisError))
throw new Error("Expected blocked retry");
expect(fault.handles).toHaveLength(2);
const [first, second] = fault.handles;
if (first === undefined || second === undefined)
throw new Error("Missing exact descriptors");
fault.blocked.delete(first);
const mixed = await client.close().catch((cause: unknown) => cause);
if (!(mixed instanceof AnalysisError))
throw new Error("Expected mixed cleanup failure");
expect(first.fd).toBe(-1);
expect(second.fd).toBeGreaterThanOrEqual(0);
expect(original.cleanupIncomplete).toBe(true);
expect(retry.cleanupIncomplete).toBe(true);
expect(mixed.cleanupResources).toHaveLength(1);
fault.blocked.clear();
await client.close();
expect(second.fd).toBe(-1);
expect(original.cleanupIncomplete).toBe(false);
expect(retry.cleanupIncomplete).toBe(false);
expect(mixed.cleanupIncomplete).toBe(false);
} finally {
fault.blocked.clear();
await client.close();
}
});
it("shutdown waits for descriptor finalization before retrying retained cleanup", async () => {
const root = await createTestTempDirectory(
"rea-cutter-descriptor-finalization-",
);
await descriptor(root, 1);
fault.root = root;
let entered!: () => void;
let release!: () => void;
const closing = new Promise<void>((resolve) => {
entered = resolve;
});
const gate = new Promise<void>((resolve) => {
release = resolve;
});
fault.onClose = async () => {
entered();
await gate;
};
const client = new CutterBridgeClient({ REA_CUTTER_BRIDGE_DIR: root });
const reading = client.listDescriptors().catch((cause: unknown) => cause);
await closing;
let settled = false;
const shutdown = client.close().then(() => {
settled = true;
});
await new Promise((resolve) => setImmediate(resolve));
expect(settled).toBe(false);
fault.blocked.clear();
release();
await reading;
await shutdown;
expect(fault.handles.every((handle) => handle.fd === -1)).toBe(true);
});
it.each([null, "primitive abort", new Error("selected abort")])(
"preserves admission cancellation %s when descriptor close fails",
async (reason) => {
const root = await createTestTempDirectory("rea-cutter-cancel-admission-");
await descriptor(root, 1);
fault.root = root;
const controller = new AbortController();
fault.onOpen = () => controller.abort(reason);
const client = new CutterBridgeClient({ REA_CUTTER_BRIDGE_DIR: root });
try {
const failure = await client
.listSessions({ signal: controller.signal })
.catch((cause: unknown) => cause);
if (!(failure instanceof AnalysisError))
throw new Error("Expected typed cancellation with cleanup");
expect(failure.cleanupIncomplete).toBe(true);
const aggregate = failure.cause;
if (!(aggregate instanceof AggregateError))
throw new Error("Missing primary cancellation");
const primary = aggregate.errors[0];
expect(primary).toBeInstanceOf(AnalysisCancelledError);
expect(primary.cause).toBe(reason);
fault.blocked.clear();
await client.close();
expect(failure.cleanupIncomplete).toBe(false);
expect(primary.cause).toBe(reason);
} finally {
fault.blocked.clear();
await client.close();
}
},
);
it("does not assign a peer's later cancellation to a descriptor that already failed close", async () => {
const root = await createTestTempDirectory("rea-cutter-peer-finalization-");
await Promise.all([descriptor(root, 1), descriptor(root, 2)]);
fault.root = root;
const controller = new AbortController();
const reason = new Error("cancel after the first descriptor failure");
let entered!: () => void;
let release!: () => void;
const waiting = new Promise<void>((resolve) => {
entered = resolve;
});
const gate = new Promise<void>((resolve) => {
release = resolve;
});
fault.onClose = async (handle) => {
if (handle === fault.handles[0]) return;
entered();
await gate;
};
const client = new CutterBridgeClient({ REA_CUTTER_BRIDGE_DIR: root });
const reading = client
.listSessions({ signal: controller.signal })
.catch((cause: unknown) => cause);
try {
await waiting;
await new Promise((resolve) => setImmediate(resolve));
controller.abort(reason);
release();
const failure = await reading;
if (
!(failure instanceof AnalysisError) ||
!(failure.cause instanceof AggregateError)
)
throw new Error("Expected mixed descriptor cleanup failures");
const failures: unknown[] = failure.cause.errors;
expect(failures).toHaveLength(2);
const cancelled = failures.filter(
(error): error is AnalysisError =>
error instanceof AnalysisError &&
error.cause instanceof AggregateError &&
error.cause.errors[0] instanceof AnalysisCancelledError,
);
expect(cancelled).toHaveLength(1);
expect((cancelled[0]?.cause as AggregateError).errors[0].cause).toBe(
reason,
);
expect(failure.cleanupIncomplete).toBe(true);
fault.blocked.clear();
await client.close();
expect(failure.cleanupIncomplete).toBe(false);
} finally {
release();
fault.onClose = undefined;
fault.blocked.clear();
await reading;
await client.close();
}
});