fix(cutter): retain command attribution through descriptor cleanup

Pass the requesting operation into the descriptor failure owner. Discovery retains its own identity; command execution and completion confirmation retain cutter_command with exact cleanup dependencies.

Validated 61 focused tests across five files, including denied retry and successful native descriptor release. Controlled close faults; no native Cutter engine verified.

Refs: #4634
This commit is contained in:
morluto committed 2026-10-17 09:22:54 +08:00
1 parent 5c0472c571
commit 199396f61c
2 files changed
+61 -5

No files matched your search

+8 -5
View File
@@ -338,7 +338,7 @@ export class CutterBridgeClient {
options: CutterDiscoveryOptions = {},
): Promise<CutterBridgeDiscovery> {
const { signal } = options;
const loaded = await this.#readDescriptors(signal);
const loaded = await this.#readDescriptors("list_cutter_sessions", signal);
if (loaded.error !== undefined)
return {
sessions: [],
@@ -653,7 +653,7 @@ export class CutterBridgeClient {
sessionId: string,
signal?: AbortSignal,
): Promise<z.infer<typeof descriptorSchema>> {
const loaded = await this.#readDescriptors(signal);
const loaded = await this.#readDescriptors("cutter_command", signal);
if (loaded.error === "bridge_directory_capacity_exceeded")
throw new AnalysisResourceConstraintError(
"cutter_command",
@@ -681,17 +681,20 @@ export class CutterBridgeClient {
async listDescriptors(): Promise<
readonly z.infer<typeof descriptorSchema>[]
> {
const loaded = await this.#readDescriptors();
const loaded = await this.#readDescriptors("list_cutter_sessions");
return loaded.descriptors;
}
async #readDescriptors(signal?: AbortSignal) {
async #readDescriptors(
operation: "list_cutter_sessions" | "cutter_command",
signal?: AbortSignal,
) {
try {
return await this.#descriptorResources.run(() =>
this.#loadDescriptors(signal),
);
} catch (cause: unknown) {
throw descriptorFailure(cause, "list_cutter_sessions");
throw descriptorFailure(cause, operation);
}
}
@@ -36,6 +36,7 @@ vi.mock("node:fs/promises", async (importOriginal) => {
import { CutterBridgeClient } from "../../src/cutter/CutterBridgeClient.js";
import { AnalysisCancelledError } from "../../src/domain/analysisErrorCore.js";
import { AnalysisError } from "../../src/domain/analysisErrorBase.js";
import { projectAnalysisError } from "../../src/domain/analysisErrorProjection.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { runCliJson } from "../fixtures/cliJsonProcess.js";
@@ -263,3 +264,55 @@ it("does not assign a peer's later cancellation to a descriptor that already fai
await client.close();
}
});
it.each(["command", "completion"] as const)(
"attributes retained descriptor cleanup to the requesting %s",
async (operation) => {
const root = await createTestTempDirectory(
"rea-cutter-command-descriptor-",
);
await descriptor(root, 1);
fault.root = root;
const client = new CutterBridgeClient({ REA_CUTTER_BRIDGE_DIR: root });
try {
const original =
operation === "command"
? await client
.execute({
sessionId: "00000000-0000-4000-8000-000000000001",
expectedGeneration: 0,
command: "ij",
json: false,
})
.catch((cause: unknown) => cause)
: await client
.confirmCommandCompletion(
"00000000-0000-4000-8000-000000000001",
"4b18ce4d-bf31-456e-ae31-e8c547b2f9db",
)
.then((result) => {
if (result.ok) throw new Error("Expected descriptor failure");
return result.error;
});
if (!(original instanceof AnalysisError))
throw new Error("Expected typed descriptor cleanup");
expect(projectAnalysisError(original)).toMatchObject({
code: "cleanup_incomplete",
details: { operation: "cutter_command" },
});
expect(fault.handles).toHaveLength(1);
expect(fault.handles[0]?.fd).toBeGreaterThanOrEqual(0);
await expect(client.retryDescriptorCleanup()).rejects.toMatchObject({
cleanupIncomplete: true,
});
expect(original.cleanupIncomplete).toBe(true);
fault.blocked.clear();
await client.close();
expect(fault.handles[0]?.fd).toBe(-1);
expect(original.cleanupIncomplete).toBe(false);
} finally {
fault.blocked.clear();
await client.close();
}
},
);