Files
rea/tests/boundary/providers/artifactReadCleanupRetry.test.ts
T
morluto b955b825fc fix(artifacts): retain read cleanup ownership through CLI and MCP shutdown
Keep failed stable-read handles and WABT runtime roots under retryable resource scopes. Preserve cancellation causes and primary errors, close the same service instances bound to MCP tools, and consolidate CLI operation/cleanup projection.

Validation: fast typecheck and lint, cached runtime build, docs checks, SDK schema validation, focused service and real-file cleanup tests, CLI cleanup regressions, and MCP lifecycle/shutdown workflows. Provider process fixtures simulate engine output; real WABT and Linux EVM execution are not established by this pass.
2026-10-13 16:08:41 +08:00

101 lines
3.8 KiB
TypeScript

import type { FileHandle } from "node:fs/promises";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, it, vi } from "vitest";
const cleanupState = vi.hoisted(() => ({
failFirstClose: false,
abortController: undefined as AbortController | undefined,
firstHandle: undefined as FileHandle | undefined,
firstHandleCloseAttempts: 0,
}));
vi.mock("node:fs/promises", async (importOriginal) => {
const fs = await importOriginal<typeof import("node:fs/promises")>();
return {
...fs,
open: async (...args: Parameters<typeof fs.open>) => {
const handle = await fs.open(...args);
const close = handle.close.bind(handle);
if (cleanupState.abortController !== undefined)
setImmediate(() => {
cleanupState.abortController?.abort();
cleanupState.abortController = undefined;
});
handle.close = async () => {
if (handle === cleanupState.firstHandle)
cleanupState.firstHandleCloseAttempts += 1;
else if (cleanupState.firstHandle === undefined)
cleanupState.firstHandle = handle;
if (
cleanupState.failFirstClose &&
handle === cleanupState.firstHandle
) {
cleanupState.failFirstClose = false;
throw new Error("injected descriptor close failure");
}
return close();
};
return handle;
},
};
});
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import { GoBinaryProvider } from "../../../src/go/GoBinaryProvider.js";
import { PeResourcesProvider } from "../../../src/native/pe/PeResourcesProvider.js";
import { createGoBinaryFixture } from "../../fixtures/go/image.js";
import { peResourceFixture } from "../../../src/native/pe/PeResources.fixture.js";
it.each(["go", "pe"] as const)(
"%s provider retains a failed descriptor close and retries that owner before the next read",
async (kind) => {
const root = await mkdtemp(join(tmpdir(), "rea-read-cleanup-"));
const path = join(root, kind === "go" ? "program.elf" : "program.exe");
const bytes =
kind === "go" ? createGoBinaryFixture().bytes : peResourceFixture().bytes;
await writeFile(path, bytes);
const provider =
kind === "go" ? new GoBinaryProvider() : new PeResourcesProvider();
cleanupState.failFirstClose = true;
const controller = new AbortController();
cleanupState.abortController = controller;
cleanupState.firstHandle = undefined;
cleanupState.firstHandleCloseAttempts = 0;
try {
const input = {
path,
max_file_bytes: 8 * 1024 * 1024,
max_entries: 4096,
};
const first = await provider.inspect(input, {
signal: controller.signal,
});
expect(first.ok).toBe(false);
if (first.ok) throw new Error("Injected close failure was not returned");
expect(projectAnalysisError(first.error)).toMatchObject({
code: "cleanup_incomplete",
details: { diagnostics: { primary_error: { code: "cancelled" } } },
});
expect(first.error.cause).toBeInstanceOf(AggregateError);
if (!(first.error.cause instanceof AggregateError))
throw first.error.cause;
expect(first.error.cause.errors[1]).toMatchObject({
cleanupResources: [path],
});
const retry = await provider.inspect(input);
expect(retry.ok).toBe(true);
expect(cleanupState.firstHandleCloseAttempts).toBe(2);
} finally {
await provider.close();
cleanupState.failFirstClose = false;
cleanupState.abortController = undefined;
cleanupState.firstHandle = undefined;
cleanupState.firstHandleCloseAttempts = 0;
await rm(root, { recursive: true, force: true });
}
},
);