Files
rea/tests/process-global/processSnapshotCleanupMcp.test.ts
T
ShuaixinHuang e9fc8fb30c fix(javascript): preserve runtime path bytes and Electron ownership (#4941)
* fix(javascript): preserve literal percent runtime paths

* fix(javascript): ignore resolve prefixes before the final anchor

* fix(electron): retain application roots for loadFile declarations

* fix(electron): resolve loadFile from enclosing application package

* fix(electron): retain outer application root across package scopes

* fix(electron): derive application ownership from inventoried packages

* fix(electron): retain resolved package entrypoint ownership

* fix(electron): preserve competing application ownership

* fix(electron): retain invalid and omitted runtime ownership

* fix(electron): preserve omitted module ownership and runtime paths

* fix(electron): preserve proven roots before confining page paths

* fix(electron): retain possible owners behind missing imports

* fix(electron): distinguish absent imports and owned page candidates

* fix(electron): keep unavailable JSON out of owner gaps

* fix(electron): retain exact loader formats and default entry status

* fix(electron): retain unavailable entrypoint ownership candidates

* test(ghidra): preserve Windows Node probe prerequisites

* test(ghidra): assert cancellation during profile shutdown

* fix(electron): separate examined imports from frontend ownership

* fix(electron): retain unavailable directory entrypoint owners

* test(jeb): retain Linux ownership prerequisite in isolation

* fix(javascript): retain unchecked loader ownership and literal main paths

* fix(javascript): preserve runtime paths and call-site module aliases

* fix(javascript): bound module alias traversal and preserve projections

* fix(javascript): retain host module and nested artifact path boundaries

* fix(javascript): preserve ESM and archive module loader provenance

* test(javascript): distinguish unmapped containers from missing exports

* fix: retain default module aliases and Electron builtin imports

* fix: preserve host file URL and inward ASAR ownership

* fix: preserve CommonJS file URL classification in CI fixtures

* fix: retain bound module loaders and Windows entrypoint uncertainty

* fix: retain ASAR uncertainty and invalidate changed Electron app roots

* fix(javascript): retain ownership uncertainty across loader boundaries

* fix(javascript): preserve loader sources and bound alias queries

* fix(javascript): retain destructured loader sources

* fix(javascript): preserve loader uncertainty at resolution boundaries

* fix(javascript): preserve callable and package launch ownership

* fix(javascript): preserve captured values across member reads

* test(process): assert tagged filesystem cancellation failures

* fix(javascript): preserve inventory bases and invocation read timing

* fix(javascript): preserve ownership across indirect and unread loads

* test(javascript): order fixture directory creation on Windows

* test(javascript): share ordered filesystem fixture writer

* fix(javascript): unify loader provenance and package validation

* fix(javascript): preserve captured Node require loader values

* fix(javascript): preserve loader invocation authority

* fix(javascript): verify generic loader invocation authority

* fix(javascript): preserve conditional loader method alternatives

* fix(javascript): require proven factories for source association

* fix(javascript): honor replaced Electron app path setters

* fix(javascript): prove path and assignment source ownership

* fix(javascript): retain native delegate eligibility through shared writes

* fix(process): settle stale baseline errors after confirmed absence

* fix(javascript): preserve assignment and path boundary evidence

* fix(javascript): prove source-directory resolve anchors

* fix: preserve application path provenance and portable schemas

* fix(javascript): retain CommonJS exported setter effects

* fix: isolate Linux identity observations and retain native module exports

* fix(javascript): preserve published app and helper provenance

* fix(javascript): normalize typed stores and native path fixtures
2026-10-20 17:25:19 +08:00

175 lines
6.3 KiB
TypeScript

import { mkdir, open, rm, writeFile } from "node:fs/promises";
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 { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
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 createTestTempDirectory("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, JSON.stringify(failed)).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, JSON.stringify(retryBlocked)).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;
}
});