* 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
175 lines
6.3 KiB
TypeScript
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;
|
|
}
|
|
});
|