mirror of
https://github.com/morluto/rea.git
synced 2026-11-09 08:17:02 +00:00
* fix(tests): restore reliable snapshot and container CI checks Keep exact cancellation causes and partial snapshot facts in regression oracles. Match fault injection to real directory handles and isolated MCP module ownership; await terminal cleanup progress within the existing test deadline. Reuse the valid ASAR Pickle fixture and preserve truncated-container rejection. * fix(tests): isolate runtime faults and own CLI subprocesses Keep the runtime provider mock in the isolated project after reused CLI graphs exposed lost fault injection. Use the existing owned CLI fixture for Inspector parity and update command checks, retain caller environment choices, and require the removed alias to fail with COMMAND_NOT_FOUND. Keep current test deadlines and production behavior. * fix(process): retain executable handles through read cancellation Use the existing borrowed descriptor reader so stream cancellation cannot invalidate a handle owned by capture. Preserve the exact cancellation and permit immediate scope reuse after cleanup. Reproduce the native-stream failure with actual file IO and retain completed/failed-close controls. Synchronize runtime cancellation and backwards-clock injection with observed running progress. The corrected clock oracle now rejects a Date.now-based deadline under controlled rollback; keep test deadlines and narrow host cleanup checks unchanged. * test(process): observe prelaunch cancellation at descriptor reads * fix(process): retain read failures alongside observed cancellation * fix(flutter): advertise portable Unicode ABI selectors
175 lines
6.2 KiB
TypeScript
175 lines
6.2 KiB
TypeScript
import { mkdir, mkdtemp, open, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join, resolve } from "node:path";
|
|
|
|
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
|
import { expect, it, vi } from "vitest";
|
|
|
|
const injected = vi.hoisted(() => ({
|
|
path: "",
|
|
failClose: false,
|
|
opens: 0,
|
|
handle: undefined as Awaited<ReturnType<typeof open>> | 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 (resolve(String(args[0])) !== injected.path) return handle;
|
|
injected.opens += 1;
|
|
injected.handle = handle;
|
|
const nativeClose = handle.close.bind(handle);
|
|
handle.close = async () => {
|
|
if (injected.failClose)
|
|
throw new Error("controlled file close failure");
|
|
await nativeClose();
|
|
};
|
|
return handle;
|
|
},
|
|
};
|
|
});
|
|
|
|
import { createTestBinarySession } from "../fixtures/binarySession.js";
|
|
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
|
|
import type { AnalysisClient } from "../../src/application/AnalysisProvider.js";
|
|
import { observed } from "../fixtures/analysisExecution.js";
|
|
import { createServer } from "../../src/server/createServer.js";
|
|
import { silentLogger } from "../../src/logger.js";
|
|
import { ToolResultDelivery } from "../../src/server/toolResult.js";
|
|
import { ok } from "../../src/domain/result.js";
|
|
|
|
const connectSession = async () => {
|
|
const session = createTestBinarySession(
|
|
() =>
|
|
({
|
|
execute: () => Promise.resolve(observed(null)),
|
|
close: () => Promise.resolve(ok(null)),
|
|
}) satisfies AnalysisClient,
|
|
);
|
|
const server = createServer(
|
|
{ kind: "session", session },
|
|
{ logger: silentLogger },
|
|
);
|
|
const client = new Client({ name: "process-snapshot-cleanup", version: "1" });
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
return { client, server, session };
|
|
};
|
|
|
|
it("retains and retries the exact snapshot file handle through public MCP capture", async () => {
|
|
const root = await mkdtemp(join(tmpdir(), "rea-process-snapshot-cleanup-"));
|
|
const observedRoot = join(root, "observed");
|
|
const filePath = join(observedRoot, "input.txt");
|
|
await mkdir(observedRoot);
|
|
await writeFile(filePath, "snapshot bytes");
|
|
injected.path = resolve(filePath);
|
|
injected.failClose = true;
|
|
injected.opens = 0;
|
|
const resources = await connectSession();
|
|
const delivery = vi.spyOn(ToolResultDelivery.prototype, "toCallToolResult");
|
|
const capture = () =>
|
|
resources.client.callTool({
|
|
name: "capture_process_scenario",
|
|
arguments: {
|
|
executable: process.execPath,
|
|
arguments: ["-e", "process.stdout.write('captured')"],
|
|
working_directory: root,
|
|
filesystem_observation_paths: [observedRoot],
|
|
timeout_ms: 10_000,
|
|
finalization_ms: 250,
|
|
},
|
|
});
|
|
|
|
try {
|
|
const failed = parseMcpToolError(await capture());
|
|
expect(injected.opens).toBe(1);
|
|
expect(injected.handle?.fd).toBeGreaterThanOrEqual(0);
|
|
expect(failed.error).toMatchObject({
|
|
code: "cleanup_incomplete",
|
|
details: {
|
|
resources: [injected.path],
|
|
partial_observation: {
|
|
observations: {
|
|
filesystem_snapshots: {
|
|
before: {
|
|
state: "available",
|
|
value: {
|
|
files: expect.arrayContaining([
|
|
expect.objectContaining({ path: "root_0:input.txt" }),
|
|
]),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const firstDelivered = delivery.mock.calls.at(-1)?.[0];
|
|
expect(firstDelivered?.ok).toBe(false);
|
|
if (firstDelivered === undefined || firstDelivered.ok)
|
|
throw new Error("Expected a delivered snapshot failure");
|
|
expect(firstDelivered.error.cleanupResources).toContain(injected.path);
|
|
|
|
const retryBlocked = parseMcpToolError(await capture());
|
|
expect(retryBlocked.error.code).toBe("cleanup_incomplete");
|
|
expect(retryBlocked.error.details).toMatchObject({
|
|
resources: [injected.path],
|
|
});
|
|
expect(injected.opens).toBe(1);
|
|
expect(injected.handle?.fd).toBeGreaterThanOrEqual(0);
|
|
|
|
injected.failClose = false;
|
|
const exactFd = injected.handle?.fd;
|
|
const closed = await resources.client.callTool({
|
|
name: "close_binary",
|
|
arguments: {},
|
|
});
|
|
expect(closed.isError, JSON.stringify(closed.content)).not.toBe(true);
|
|
expect(injected.handle?.fd).toBe(-1);
|
|
expect(exactFd).toBeGreaterThanOrEqual(0);
|
|
if (injected.handle === undefined)
|
|
throw new Error("Expected the retained snapshot file descriptor");
|
|
await expect(injected.handle.stat()).rejects.toMatchObject({
|
|
code: "EBADF",
|
|
});
|
|
expect(firstDelivered.error.cleanupIncomplete).toBe(false);
|
|
expect(firstDelivered.error.cleanupResources).toEqual([]);
|
|
|
|
const recovered = await capture();
|
|
expect(recovered.isError, JSON.stringify(recovered.content)).not.toBe(true);
|
|
expect(injected.opens).toBe(3);
|
|
expect(recovered.structuredContent).toMatchObject({
|
|
normalized_result: {
|
|
files_before: expect.arrayContaining([
|
|
expect.objectContaining({ path: "root_0:input.txt" }),
|
|
]),
|
|
files_after: expect.arrayContaining([
|
|
expect.objectContaining({ path: "root_0:input.txt" }),
|
|
]),
|
|
},
|
|
});
|
|
} finally {
|
|
injected.failClose = false;
|
|
if (injected.handle?.fd !== undefined && injected.handle.fd >= 0) {
|
|
const cleanup = await resources.client.callTool({
|
|
name: "close_binary",
|
|
arguments: {},
|
|
});
|
|
expect(cleanup.isError, JSON.stringify(cleanup.content)).not.toBe(true);
|
|
}
|
|
delivery.mockRestore();
|
|
await resources.client.close();
|
|
await resources.server.close();
|
|
const closed = await resources.session.close();
|
|
expect(closed.ok).toBe(true);
|
|
await rm(root, { recursive: true, force: true });
|
|
injected.path = "";
|
|
injected.handle = undefined;
|
|
}
|
|
});
|