Files
reverse-engineering-anything/tests/boundary/mcp/sourcePublicationContext.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

393 lines
13 KiB
TypeScript

import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server";
import { afterAll, expect, it } from "vitest";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { createTestProviderRouter } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { BinarySession } from "../../../src/application/binary/BinarySession.js";
import { InvestigationRecords } from "../../../src/application/investigation/InvestigationRecords.js";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { AnalysisCapabilityUnavailableError } from "../../../src/domain/analysisErrorCore.js";
import { err, ok } from "../../../src/domain/result.js";
import { createServer } from "../../../src/server/createServer.js";
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js";
const validation = createMcpSchemaValidation();
afterAll(() => validation.clear());
it.each(
["full", "compact"].flatMap((profile) =>
["large-source", "large-primary", "invalid-source"].map((mode) => ({
profile,
mode,
})),
),
)(
"keeps sibling context with $mode ($profile schema)",
async ({ mode, profile }) => {
const id = { id: "fixture", name: "Fixture", version: "1" };
const original = createEvidence(
undefined,
id,
{
operation: "health",
parameters: {},
result:
mode === "large-source"
? "s".repeat(STDIO_DEFAULT_MAX_BUFFER_SIZE + 65536)
: "small source",
},
{ immutable: true },
);
const source =
mode === "invalid-source"
? { ...original, normalized_result: "tampered" }
: original;
const name =
mode === "large-primary"
? "p".repeat(STDIO_DEFAULT_MAX_BUFFER_SIZE + 65536)
: "primary function";
const target = join(
await createTestTempDirectory("rea-source-context-"),
"fixture.com",
);
await writeFile(target, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const router = createTestProviderRouter(() => ({
execute: async (op) =>
op === "health"
? ok(createAnalysisExecution(null, id))
: op === "address_name"
? ok(createAnalysisExecution("entry", id))
: op === "resolve_containing_procedure"
? ok(
createAnalysisExecution(
{
query_address: "0x100",
found: true,
procedure: {
address: "0x100",
name,
classification: null,
body: { available: false, reason: "not recorded" },
},
},
id,
{ sourceEvidence: [source] },
),
)
: err(
new AnalysisCapabilityUnavailableError(
id.id,
op,
"not provided",
),
),
close: async () => ok(null),
}));
const records = new InvestigationRecords({ maximumRepresentationBytes: 0 });
const session = new BinarySession(router, records);
const server = createServer(
{ kind: "session", session },
{
environment: {
...process.env,
REA_MCP_TOOL_FAMILIES: "session",
REA_MCP_OUTPUT_SCHEMA_PROFILE: profile,
},
},
);
const client = new Client({ name: "source-context", version: "1" });
const [ct, st] = InMemoryTransport.createLinkedPair();
try {
await server.connect(st);
await client.connect(ct);
const opened = await client.callTool({
name: "open_binary",
arguments: { path: target, format: "dos-com" },
});
expect(opened.isError, JSON.stringify(opened.content)).not.toBe(true);
const tool = (await client.listTools()).tools.find(
({ name }) => name === "inspect_address_context",
);
if (tool?.outputSchema === undefined)
throw new Error("Missing context output schema");
expect(validation.assertions.validateSchema(tool.outputSchema)).toBe(
true,
);
const result = await client.callTool(
{
name: "inspect_address_context",
arguments: { address: "0x100", document: "fixture" },
},
{ timeout: 120000, toolDefinition: tool },
);
expect(result.isError, JSON.stringify(result.content)).not.toBe(true);
expect(
validation.assertions.validate(
tool.outputSchema,
result.structuredContent,
),
JSON.stringify(validation.assertions.errors),
).toBe(true);
expect(result.structuredContent).toMatchObject({
result: {
name: { state: "available", value: "entry" },
procedure: {
state: "unavailable",
error: {
code:
mode === "invalid-source"
? "evidence_integrity_mismatch"
: "resource_constraint",
details:
mode === "large-source"
? {
partial_observation_omitted: {
reason: "mcp-response-limit",
},
completed_observation: { kind: "unretained-evidence" },
}
: mode === "large-primary"
? {
partial_observation: { kind: "unretained-evidence" },
completed_observation_omitted: {
reason: "mcp-response-limit",
},
}
: {
completed_observation: { kind: "unretained-evidence" },
},
},
},
},
});
} finally {
await client.close();
await server.close();
await session.close();
}
},
120000,
);
it.each([false, true])(
"retains a sibling source failure when cleanup fails (large source: %s)",
async (large) => {
const provider = { id: "fixture", name: "Fixture", version: "1" };
const source = createEvidence(
undefined,
provider,
{
operation: "health",
parameters: {},
result: large
? "s".repeat(STDIO_DEFAULT_MAX_BUFFER_SIZE + 65536)
: "source",
},
{ immutable: true },
);
const target = join(
await createTestTempDirectory("rea-mixed-context-"),
"fixture.com",
);
await writeFile(target, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const session = new BinarySession(
createTestProviderRouter(() => ({
execute: async (operation) =>
operation === "health"
? ok(createAnalysisExecution(null, provider))
: operation === "address_name"
? ok(
createAnalysisExecution("completed name", provider, {
sourceEvidence: [source],
}),
)
: operation === "comment"
? err(
new ProviderCleanupError(
provider.id,
["fixture-owner"],
{},
),
)
: err(
new AnalysisCapabilityUnavailableError(
provider.id,
operation,
"not provided",
),
),
close: async () => ok(null),
})),
new InvestigationRecords({ maximumRepresentationBytes: 0 }),
);
const server = createServer({ kind: "session", session });
const client = new Client({ name: "source-cleanup-context", version: "1" });
const [ct, st] = InMemoryTransport.createLinkedPair();
try {
await server.connect(st);
await client.connect(ct);
expect(
(
await client.callTool({
name: "open_binary",
arguments: { path: target, format: "dos-com" },
})
).isError,
).not.toBe(true);
const result = await client.callTool(
{
name: "inspect_address_context",
arguments: { address: "0x100", document: "fixture" },
},
{ timeout: 120000 },
);
const error = parseMcpToolError(result).error;
expect(error).toMatchObject({
code: "cleanup_incomplete",
details: {
resources: ["fixture-owner"],
secondary_failures: [
{
code: "resource_constraint",
details: {
...(large
? {
partial_observation_omitted: {
reason: "mcp-response-limit",
},
}
: {
partial_observation: {
kind: "unretained-evidence",
evidence: { evidence_id: source.evidence_id },
},
}),
completed_observation: {
kind: "unretained-evidence",
evidence: { normalized_result: "completed name" },
source_closure: "not-established",
},
},
},
],
},
});
} finally {
await client.close();
await server.close();
await session.close();
}
},
120000,
);
it("keeps a smaller sibling recovery when a later completed observation exceeds delivery", async () => {
const provider = { id: "fixture", name: "Fixture", version: "1" };
const source = createEvidence(
undefined,
provider,
{
operation: "health",
parameters: {},
result: "source",
},
{ immutable: true },
);
const target = join(
await createTestTempDirectory("rea-mixed-recoveries-"),
"fixture.com",
);
await writeFile(target, Buffer.from([0xb8, 0, 0x4c, 0xcd, 0x21]));
const session = new BinarySession(
createTestProviderRouter(() => ({
execute: async (operation) => {
if (operation === "health")
return ok(createAnalysisExecution(null, provider));
if (operation === "address_name")
return ok(
createAnalysisExecution("small", provider, {
sourceEvidence: [source],
}),
);
if (operation === "inline_comment")
return ok(
createAnalysisExecution(
"p".repeat(STDIO_DEFAULT_MAX_BUFFER_SIZE + 65536),
provider,
{ sourceEvidence: [source] },
),
);
if (operation === "comment")
return err(
new ProviderCleanupError(provider.id, ["fixture-owner"], {}),
);
return err(
new AnalysisCapabilityUnavailableError(
provider.id,
operation,
"not provided",
),
);
},
close: async () => ok(null),
})),
new InvestigationRecords({ maximumRepresentationBytes: 0 }),
);
const server = createServer({ kind: "session", session });
const client = new Client({
name: "multiple-recovery-context",
version: "1",
});
const [ct, st] = InMemoryTransport.createLinkedPair();
try {
await server.connect(st);
await client.connect(ct);
expect(
(
await client.callTool({
name: "open_binary",
arguments: { path: target, format: "dos-com" },
})
).isError,
).not.toBe(true);
const result = await client.callTool(
{
name: "inspect_address_context",
arguments: { address: "0x100", document: "fixture" },
},
{ timeout: 120000 },
);
expect(parseMcpToolError(result).error).toMatchObject({
code: "cleanup_incomplete",
details: {
resources: ["fixture-owner"],
secondary_failures: [
{
details: {
partial_observation: { kind: "unretained-evidence" },
completed_observation: {
evidence: { normalized_result: "small" },
},
},
},
{
details: {
partial_observation: { kind: "unretained-evidence" },
completed_observation_omitted: { reason: "mcp-response-limit" },
},
},
],
},
});
} finally {
await client.close();
await server.close();
await session.close();
}
}, 120000);