Files
reverse-engineering-anything/tests/boundary/mcp/batchSourcePublicationCleanup.test.ts
T
morlutoandCursor Agent 88cf498036 fix!: preserve results when source publication fails (#8444)
* fix: retain address context cleanup failures

* fix!: preserve address context input errors

BREAKING CHANGE: Strict clients that cached the previous full inspect_address_context output schema must refresh tools/list and accept the typed error field on unavailable facets.

* test: use session contract for address context

* fix!: preserve address context resource recovery

BREAKING CHANGE: Clients validating unavailable context facets against a cached strict full output schema must refresh tools/list to accept resource_constraint errors.

* fix: expose authenticated records refused by the evidence ledger (#8426)

* fix: preserve completed evidence on retention refusal

* fix: preserve context when unretained evidence exceeds delivery limits

* fix(mcp): recover oversized context from session evidence

* fix(evidence): preserve completed atomic write results

* fix: retain completed results when source publication fails

* test: cover bounded recovery with unknown retention

* fix(evidence): preserve recovery identity and finalization facts

* fix(evidence): clarify retention of omitted observations

* test(evidence): compare omission guidance with its owner

* fix(evidence): preserve completed composed workflows

* fix(evidence): clarify unretained recovery export

* test(evidence): cover mixed recovery guidance

* fix(evidence): preserve caller scope in secondary failures

* refactor(evidence): remove superseded cleanup projection

* test(evidence): align recovery checks with owners

* fix: preserve cleanup on partial recovery limits

* test: use owners for diagnostic wording

* fix: admit completed analysis before evidence construction

* fix: preserve selected paths when reusing evidence

* fix(cli): preserve caller path in unretained evidence

* fix(evidence): preserve recovery metadata and admission bounds

* refactor: consolidate evidence closure imports

* style(test): format CLI help default annotations (#9280)

* test(mcp): keep recovery checks focused on contract facts

* fix(evidence): preserve feature trace replay revision

* test: align evidence recovery checks with ownership

* test: align partial recovery and cleanup diagnostics

* fix(evidence): preserve nested partials during cleanup

* fix(evidence): respect outer recovery ownership

* fix(capture): discard rejected nested observations

* test(frida): assert inline evidence recovery

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-11-09 02:41:54 +08:00

180 lines
6.2 KiB
TypeScript

import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import type { CallToolResult } from "@modelcontextprotocol/server";
import { access, mkdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it, vi } from "vitest";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import { BinarySession } from "../../../src/application/binary/BinarySession.js";
import { InvestigationRecords } from "../../../src/application/investigation/InvestigationRecords.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
import { err, ok } from "../../../src/domain/result.js";
import { createServer } from "../../../src/server/createServer.js";
import { createTestProviderRouter } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { awaitAcknowledgement } from "../../fixtures/lifecycleAcknowledgement.js";
const expectRecoveredBatch = (response: CallToolResult, owned: string) => {
expect(parseMcpToolError(response).error).toMatchObject({
code: "resource_constraint",
details: {
completed_observation: {
evidence: {
normalized_result: {
items: [
{ status: "ok" },
{
status: "error",
error: {
code: "cleanup_incomplete",
details: { resources: [owned] },
},
},
],
},
},
},
},
});
};
it("keeps a mixed batch cleanup owner after source retention fails and retries it", async () => {
const root = await createTestTempDirectory("rea-batch-owned-cleanup-");
const target = join(root, "fixture.com"),
owned = join(root, "owned");
await writeFile(target, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const provider = { id: "fixture", name: "Fixture", version: "1" };
const source = createEvidence(undefined, provider, {
operation: "health",
parameters: {},
result: "source",
});
const failure = new ProviderCleanupError(provider.id, [owned], {
reason: "controlled cleanup failure",
});
let count = 0,
pending = false,
allow = false,
closes = 0;
let entered!: () => void;
let release!: () => void;
const closing = new Promise<void>((resolve) => {
entered = resolve;
});
const gate = new Promise<void>((resolve) => {
release = resolve;
});
const session = new BinarySession(
createTestProviderRouter(() => ({
execute: async (operation) => {
if (operation === "procedure_pseudo_code" && ++count === 2) {
await mkdir(owned);
pending = true;
return err(failure);
}
return ok(
createAnalysisExecution(
operation === "procedure_address"
? "0x100"
: operation === "address_name"
? "example"
: operation === "health"
? null
: "int example(void) { return 1; }",
provider,
),
);
},
close: async () => {
closes++;
if (!pending) return ok(null);
if (closes === 2) {
entered();
await gate;
return err(failure);
}
if (!allow) return err(failure);
await rm(owned, { recursive: true });
pending = false;
failure.resolveCleanup();
return ok(null);
},
})),
new InvestigationRecords({ maximumRepresentationBytes: 0 }),
);
const admit = session.withAdmittedAnalysis.bind(session);
vi.spyOn(session, "withAdmittedAnalysis").mockImplementation(
(operation, signal, run) =>
admit(operation, signal, (analysis) =>
run({
...analysis,
execute: async (...args) => {
const result = await analysis.execute(...args);
if (args[0] === "procedure_pseudo_code" && result.ok)
return {
...result,
value: { ...result.value, sourceEvidence: [source] },
};
return result;
},
}),
),
);
const server = createServer({ kind: "session", session });
const client = new Client({ name: "batch-owned-cleanup", version: "1" });
const [ct, st] = InMemoryTransport.createLinkedPair();
try {
await server.connect(st);
await client.connect(ct);
const call = (name: string, args: Record<string, unknown> = {}) =>
client.callTool({ name, arguments: args }, { timeout: 120000 });
expect(
(await call("open_binary", { path: target, format: "dos-com" })).isError,
).not.toBe(true);
const response = await call("batch_decompile", {
addresses: ["first", "second"],
});
expectRecoveredBatch(response, owned);
expect(pending).toBe(true);
expect(failure.cleanupIncomplete).toBe(true);
await expect(access(owned)).resolves.toBeUndefined();
const close1 = await call("close_binary");
expect(close1.isError).toBe(true);
expect(parseMcpToolError(close1).error).toMatchObject({
code: "cleanup_incomplete",
recovery: { kind: "retry_cleanup", operation: "close_binary" },
details: { resources: [owned] },
});
expect(pending).toBe(true);
const controller = new AbortController();
const cancelledClose = client.callTool(
{ name: "close_binary", arguments: {} },
{ signal: controller.signal, timeout: 120000 },
);
try {
await awaitAcknowledgement(closing, cancelledClose, "final cleanup");
controller.abort();
await expect(cancelledClose).rejects.toThrow();
} finally {
controller.abort();
release();
await Promise.allSettled([cancelledClose]);
}
allow = true;
const close2 = await call("close_binary");
expect(close2.isError).not.toBe(true);
expect(pending).toBe(false);
expect(failure.cleanupIncomplete).toBe(false);
expect(closes).toBe(3);
await expect(access(owned)).rejects.toThrow();
} finally {
allow = true;
release();
await client.close();
await server.close();
await session.close();
vi.restoreAllMocks();
}
});