Files
reverse-engineering-anything/tests/boundary/mcp/addressContextInputErrors.test.ts
T
morluto 5a1f75461a fix(mcp): guide address context recovery (#8766)
* fix(mcp): guide function comment recovery from address context

* fix(mcp): retain specific comment refusal recovery

* docs(mcp): advertise paged function comments

* fix(mcp): preserve address facet recovery

* test(mcp): verify session-specific facet recovery

* test(mcp): extract address recovery fixture

* fix(mcp): direct provider recovery to complete status

* fix(mcp): qualify address recovery across providers

* fix(mcp): make coordinate recheck actionable

* test(mcp): correct address recovery fixtures

* test(ghidra): verify address comment recovery
2026-11-05 01:48:08 +08:00

404 lines
14 KiB
TypeScript

import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { expect, it } from "vitest";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import { inspectAddressContext } from "../../../src/application/AnalysisContextQueries.js";
import {
AnalysisCapabilityUnavailableError,
AnalysisInputError,
AnalysisProtocolError,
} from "../../../src/domain/analysisErrorCore.js";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import { err, ok } from "../../../src/domain/result.js";
import { SESSION_TOOL_CONTRACTS } from "../../../src/contracts/sessionToolContracts.js";
import { createServer } from "../../../src/server/createServer.js";
const createContextRecoverySession = () => {
const identity = { id: "fixture", name: "Fixture", version: "1" };
return createTestBinarySession(() => ({
execute: async (operation, parameters) => {
if (operation === "health")
return ok(createAnalysisExecution(null, identity));
if (operation === "comment" && parameters.address === "0x405000")
return err(
new AnalysisCapabilityUnavailableError(
"fixture",
operation,
"configuration unavailable",
{
remediationAction: "Restart REA.",
continuingCallerGuidance: {
remediationAction: "Refresh the current session.",
},
},
),
);
if (
operation === "resolve_containing_procedure" &&
parameters.address === "0x404000"
)
return err(
new AnalysisCapabilityUnavailableError(
"fixture",
operation,
"not supported",
),
);
if (operation === "comment" && parameters.address === "0x403000")
return err(
new AnalysisCapabilityUnavailableError(
"fixture",
operation,
"temporarily unavailable",
{
retryable: true,
remediationAction: "Wait for the fixture to become ready.",
},
),
);
if (operation === "comment" || operation === "inline_comment")
return err(
new AnalysisCapabilityUnavailableError(
"fixture",
operation,
"not supported",
),
);
if (
operation !== "resolve_containing_procedure" &&
operation !== "address_name" &&
operation !== "list_bookmarks"
)
throw new Error(`Unexpected provider operation: ${operation}`);
const result =
operation === "resolve_containing_procedure"
? parameters.address === "0x402000"
? {
query_address: "0x402000",
found: false,
procedure: null,
reason: "not_in_procedure",
}
: {
query_address: parameters.address,
found: true,
procedure: {
address: "0x401000",
name: "entry",
classification: null,
body: { available: false, reason: "not recorded" },
},
}
: operation === "address_name"
? "entry"
: [];
return ok(createAnalysisExecution(result, identity));
},
close: async () => ok(null),
}));
};
it.each(["compact", "full"] as const)(
"offers a conditional containing-function comment route (%s)",
async (profile) => {
const directory = await createTestTempDirectory("rea-context-comment-");
const target = join(directory, "fixture.com");
await writeFile(target, Buffer.from([0xb8, 0x00, 0x4c, 0xcd, 0x21]));
const session = createContextRecoverySession();
const server = createServer(
{ kind: "session", session },
{
environment: {
...process.env,
REA_MCP_OUTPUT_SCHEMA_PROFILE: profile,
REA_MCP_TOOL_FAMILIES: "session",
},
},
);
const client = new Client({
name: "context-comment-recovery",
version: "1",
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
expect(
(
await client.callTool({
name: "open_binary",
arguments: { path: target, format: "dos-com" },
})
).isError,
).not.toBe(true);
const response = await client.callTool({
name: "inspect_address_context",
arguments: { address: "0x401001", document: "fixture" },
});
const contract = SESSION_TOOL_CONTRACTS.find(
(candidate) => candidate.name === "inspect_address_context",
);
if (contract === undefined)
throw new Error("inspect_address_context contract is missing");
const result = contract.outputSchema.parse(
response.structuredContent,
).result;
for (const facet of [result.comment, result.inline_comment]) {
expect(facet).toMatchObject({
state: "unavailable",
remediation: expect.stringContaining('"procedure":"0x401000"'),
});
if (facet.state !== "unavailable")
throw new Error("expected unavailable facet");
expect(facet.remediation).toContain("same document");
expect(facet.remediation).toContain("exact requested address");
expect(facet.remediation).toContain("parent_evidence_id");
expect(facet.remediation).toContain("inspect_analysis_view");
}
const outside = await client.callTool({
name: "inspect_address_context",
arguments: { address: "0x402000", document: "fixture" },
});
const outsideResult = contract.outputSchema.parse(
outside.structuredContent,
).result;
expect(outsideResult.procedure).toMatchObject({
state: "available",
value: {
query_address: "0x402000",
found: false,
procedure: null,
reason: "not_in_procedure",
},
});
expect(outsideResult.comment).toMatchObject({
state: "unavailable",
remediation: expect.stringContaining("binary_session"),
});
if (outsideResult.comment.state !== "unavailable")
throw new Error("expected unavailable facet");
expect(outsideResult.comment.remediation).toContain("detail complete");
expect(outsideResult.comment.remediation).toContain("search_procedures");
expect(outsideResult.comment.remediation).not.toContain(
"analyze_function",
);
const transient = await client.callTool({
name: "inspect_address_context",
arguments: { address: "0x403000", document: "fixture" },
});
expect(transient.isError, JSON.stringify(transient)).not.toBe(true);
const transientResult = contract.outputSchema.parse(
transient.structuredContent,
).result;
expect(transientResult.comment).toMatchObject({
state: "unavailable",
remediation: "Wait for the fixture to become ready.",
});
const continuing = await client.callTool({
name: "inspect_address_context",
arguments: { address: "0x405000", document: "fixture" },
});
const continuingResult = contract.outputSchema.parse(
continuing.structuredContent,
).result;
expect(continuingResult.comment).toMatchObject({
state: "unavailable",
remediation: "Refresh the current session.",
});
const unsupportedProcedure = await client.callTool({
name: "inspect_address_context",
arguments: { address: "0x404000", document: "fixture" },
});
const unsupportedResult = contract.outputSchema.parse(
unsupportedProcedure.structuredContent,
).result;
expect(unsupportedResult.procedure).toMatchObject({
state: "unavailable",
remediation: expect.stringContaining("binary_session"),
});
if (unsupportedResult.procedure.state !== "unavailable")
throw new Error("expected unavailable facet");
expect(unsupportedResult.procedure.remediation).toContain(
"detail complete",
);
expect(unsupportedResult.procedure.remediation).toContain(
"search_procedures",
);
} finally {
await client.close();
await server.close();
}
},
);
it.each([
["compact", false, "comment"],
["compact", true, "comment"],
["full", false, "comment"],
["full", true, "comment"],
["full", false, "list_bookmarks"],
] as const)(
"preserves a local input refusal alongside successful facets (%s, conforms=%s, operation=%s)",
async (profile, conforms, rejectedOperation) => {
const identity = { id: "fixture", name: "Fixture", version: "1" };
const directory = await createTestTempDirectory("rea-context-input-");
const target = join(directory, "fixture.com");
await writeFile(target, Buffer.from([0xb8, 0x00, 0x4c, 0xcd, 0x21]));
const inputFailure = new AnalysisInputError(
rejectedOperation,
{
userMessage: "Controlled facet refusal",
conformsToInputContract: conforms,
diagnostics: { producer_fact: "retained" },
},
[
{
path: [rejectedOperation === "comment" ? "address" : "document"],
reason: "invalid_value",
expected: "controlled-value",
},
],
);
const session = createTestBinarySession(() => ({
execute: async (operation) => {
if (operation === rejectedOperation) return err(inputFailure);
if (operation === "inline_comment")
return err(new AnalysisProtocolError("controlled provider failure"));
const result =
operation === "address_name"
? "entry"
: operation === "resolve_containing_procedure"
? {
query_address: "0x401001",
found: true,
procedure: {
address: "0x401000",
name: "entry",
classification: null,
body: { available: false, reason: "not recorded" },
},
}
: operation === "list_bookmarks"
? []
: null;
return ok(createAnalysisExecution(result, identity));
},
close: async () => ok(null),
}));
const server = createServer(
{ kind: "session", session },
{
environment: {
...process.env,
REA_MCP_OUTPUT_SCHEMA_PROFILE: profile,
REA_MCP_TOOL_FAMILIES: "session",
},
},
);
const client = new Client({ name: "context-input-error", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const definitions = new Map(
(await client.listTools()).tools.map((tool) => [tool.name, tool]),
);
const contextDefinition = definitions.get("inspect_address_context");
if (contextDefinition === undefined)
throw new Error("inspect_address_context was not advertised");
expect(
(
await client.callTool({
name: "open_binary",
arguments: { path: target, format: "dos-com" },
})
).isError,
).not.toBe(true);
const response = await client.callTool(
{
name: "inspect_address_context",
arguments: { address: "0x401001", document: "fixture" },
},
{ toolDefinition: contextDefinition },
);
expect(response.isError).not.toBe(true);
const contract = SESSION_TOOL_CONTRACTS.find(
(
candidate,
): candidate is Extract<
(typeof SESSION_TOOL_CONTRACTS)[number],
{ readonly name: "inspect_address_context" }
> => candidate.name === "inspect_address_context",
);
if (contract === undefined)
throw new Error("inspect_address_context contract is missing");
const parsed = contract.outputSchema.parse(
response.structuredContent,
).result;
const expected = projectAnalysisError(inputFailure);
expect(parsed.name).toEqual({ state: "available", value: "entry" });
expect(parsed.procedure).toMatchObject({
state: "available",
value: { found: true, query_address: "0x401001" },
});
const rejected =
rejectedOperation === "comment" ? parsed.comment : parsed.bookmarks;
expect(rejected).toEqual({
state: "unavailable",
reason: expected.message,
remediation: expected.remediation.action,
error: expected,
});
if (rejectedOperation === "comment")
expect(parsed.bookmarks).toEqual({ state: "available", value: [] });
else expect(parsed.comment).toEqual({ state: "available", value: null });
expect(parsed.inline_comment).toMatchObject({ state: "unavailable" });
expect(parsed.inline_comment).not.toHaveProperty("error");
expect(expected.details).toMatchObject({
issues: [
{
path: [rejectedOperation === "comment" ? "address" : "document"],
expected: "controlled-value",
},
],
diagnostics: { producer_fact: "retained" },
});
expect(Object.hasOwn(expected, "recovery")).toBe(!conforms);
} finally {
await client.close();
await server.close();
}
},
);
it("does not demote an input refusal with incomplete cleanup into a local facet", async () => {
const identity = { id: "fixture", name: "Fixture", version: "1" };
const failure = new AnalysisInputError(
"comment",
{
cleanup: { reason: "controlled failure", resources: ["owned-resource"] },
},
[{ path: ["address"], reason: "invalid_value" }],
);
const result = await inspectAddressContext(
{
execute: async (operation) =>
operation === "comment"
? err(failure)
: ok(createAnalysisExecution(null, identity)),
},
{ address: "0x401001", document: "fixture" },
);
if (result.ok) throw new Error("expected incomplete cleanup");
expect(result.error).toBe(failure);
expect(projectAnalysisError(result.error)).toMatchObject({
code: "cleanup_incomplete",
details: { resources: ["owned-resource"] },
});
});