mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
5f8af4ee5f
* test(mcp): expect trace_feature to report references the provider truncated A provider that reports a capped xrefs read makes the trace truncated with a residual unknown for that target. An explicit complete report and a provider that reports nothing leave the trace as before. * fix(mcp): report references a provider truncated in trace_feature Executions gain an optional flag for a capped xrefs read, set by the IDA adapter from the upstream cap. trace_feature marks the trace truncated with a residual unknown for that target only when the provider reports a truncation; a provider that reports nothing keeps its previous result. The trace_feature workflow profile is revised so earlier results are not replayed. * refactor(ida): report reference truncation whenever the runner observed an xrefs cap * test(mcp): cover session forwarding, per-target state, native reports and failures of the trace coverage * test(mcp): format the session forwarding test * test(ida): expect headless IDA to resolve containing procedures from lookups and segment facts Cover positive and negative containment, segment boundaries including addresses above 2^53, failures of the lookup and of the segment survey, cancellation, input validation and the admitted program as the only document. * test(ida): expect feature tracing and address context to work on headless IDA Run trace_feature, including a capped reference read, and inspect_address_context with and without an explicit document through the IDA session client. * test(mcp): expect trace_feature availability to follow the search operations it calls * fix(mcp): derive trace_feature availability from the search operations it calls * feat(ida): resolve containing procedures and report the admitted program as the document The modern headless profile resolves an address through the function lookup and, when no function contains it, tells a mapped address from an unmapped one using the database-scoped minimal survey. A survey that is missing, truncated or inconsistent fails the read. list_documents reports the single admitted program. The attached profiles do not declare either operation. * docs(ida): describe containing procedures, the admitted document and truncated feature traces * test(ida): verify containing procedures, the admitted document and feature tracing on real IDA * docs(testing): list the headless IDA workflows the real lane verifies * test(ida): make each survey failure case isolate a single defect * test(ida): pin large addresses, segment order, document identity, attached refusals and each trace_feature prerequisite * test(mcp): expect a repeated reference source to be resolved once in trace_feature * fix(mcp): resolve the containing procedure of a repeated reference source once * test(ida): trace the fixture procedure and derive probe addresses from its facts in the real lane * test(process): format processCaptureLifecycle.test.ts after the merge * test(ida): declare the alignment each synthetic universal slice offset satisfies * refactor(ida): move the real lane's feature trace assertions into a helper * test(ida): expect the real lane's feature trace check to accept references to every matched address * fix(ida): check each trace reference against its own matched address in the real lane * test(ida): pin that the new operations reject a document other than the admitted program * refactor(ida): move the real lane's mapped non-function probe into a helper * test(ida): expect the real lane's mapped data probe to skip strings inside functions * fix(ida): try the target's strings in order for the mapped non-function probe * test(ida): declare the real lane helper types for the verifier tests * test(ida): expect the real lane to reject a listed string that resolves as unmapped * fix(ida): fail the real lane when a listed string resolves as unmapped
140 lines
4.6 KiB
TypeScript
140 lines
4.6 KiB
TypeScript
import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server";
|
|
import { rm } from "node:fs/promises";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { createIdaTarget, RecordingIdaMcp } from "../../fixtures/idaMcp.js";
|
|
import { IdaSessionClient } from "../../../src/ida/IdaSessionClient.js";
|
|
import {
|
|
IDA_OPERATIONS,
|
|
idaCapabilities,
|
|
} from "../../../src/ida/IdaProviderCapabilities.js";
|
|
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
|
import { createEvidence } from "../../../src/domain/evidence.js";
|
|
import type { JsonValue } from "../../../src/domain/jsonValue.js";
|
|
import { ToolResultDelivery } from "../../../src/server/toolResult.js";
|
|
|
|
const delivery = new ToolResultDelivery(STDIO_DEFAULT_MAX_BUFFER_SIZE);
|
|
|
|
const roots: string[] = [];
|
|
afterEach(async () => {
|
|
for (const root of roots.splice(0))
|
|
await rm(root, { recursive: true, force: true });
|
|
});
|
|
/** Pinned upstream tools that navigate an active IDA GUI as a side effect. */
|
|
const GUI_NAVIGATING_TOOLS: ReadonlySet<string> = new Set([
|
|
"disassemble_function",
|
|
"decompile_function",
|
|
]);
|
|
|
|
const parametersFor = (
|
|
operation: string,
|
|
): Readonly<Record<string, JsonValue>> => {
|
|
if (operation.startsWith("search_")) return { pattern: "a.b" };
|
|
if (operation === "xrefs" || operation === "resolve_containing_procedure")
|
|
return { address: "0x1000" };
|
|
if (operation.startsWith("list_")) return {};
|
|
return { procedure: "main" };
|
|
};
|
|
|
|
/** Operations only the headless profile declares. */
|
|
const HEADLESS_ONLY: ReadonlySet<string> = new Set([
|
|
"resolve_containing_procedure",
|
|
"list_documents",
|
|
]);
|
|
|
|
describe.each(["attached", "headless"] as const)(
|
|
"IDA %s caller-visible contracts",
|
|
(mode) => {
|
|
const operations = IDA_OPERATIONS.filter(
|
|
(operation) => mode === "headless" || !HEADLESS_ONLY.has(operation),
|
|
);
|
|
it.each(operations)(
|
|
"declares %s UI effects matching the upstream tools it reaches",
|
|
async (operation) => {
|
|
const { root, target } = await createIdaTarget();
|
|
roots.push(root);
|
|
const producer = new RecordingIdaMcp(target, mode);
|
|
const client = new IdaSessionClient(
|
|
{
|
|
command: "fixture",
|
|
args: [],
|
|
env: {},
|
|
mode,
|
|
workspaceRoot: root,
|
|
timeoutMs: 1000,
|
|
},
|
|
target,
|
|
producer,
|
|
);
|
|
try {
|
|
await client.execute(operation, parametersFor(operation));
|
|
} finally {
|
|
await client.close();
|
|
}
|
|
const descriptor = idaCapabilities(mode, true).find(
|
|
(capability) => capability.operation === operation,
|
|
);
|
|
expect(descriptor?.effects.mayShowUi).toBe(
|
|
producer.calls.some(({ name }) => GUI_NAVIGATING_TOOLS.has(name)),
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each(operations)(
|
|
"serializes %s against its named advertised output contract",
|
|
async (operation) => {
|
|
const { root, target } = await createIdaTarget();
|
|
roots.push(root);
|
|
const client = new IdaSessionClient(
|
|
{
|
|
command: "fixture",
|
|
args: [],
|
|
env: {},
|
|
mode,
|
|
workspaceRoot: root,
|
|
timeoutMs: 1000,
|
|
},
|
|
target,
|
|
new RecordingIdaMcp(target, mode),
|
|
);
|
|
try {
|
|
const parameters = parametersFor(operation);
|
|
const execution = await client.execute(operation, parameters);
|
|
if (mode === "headless" && operation === "procedure_callers") {
|
|
expect(execution.ok).toBe(false);
|
|
if (!execution.ok)
|
|
expect(execution.error._tag).toBe(
|
|
"AnalysisCapabilityUnavailableError",
|
|
);
|
|
return;
|
|
}
|
|
if (!execution.ok) throw execution.error;
|
|
const evidence = createEvidence(target, execution.value.provider, {
|
|
operation,
|
|
parameters,
|
|
result: execution.value.result,
|
|
rawResult: execution.value.rawResult,
|
|
locations: execution.value.locations,
|
|
limitations: execution.value.limitations,
|
|
});
|
|
const contract = toolContract(operation);
|
|
const serialized = delivery.toEvidenceToolResult(
|
|
evidence,
|
|
contract,
|
|
undefined,
|
|
);
|
|
expect(
|
|
contract.outputSchema.safeParse(serialized.structuredContent)
|
|
.success,
|
|
).toBe(true);
|
|
expect(serialized.content[0]).toEqual({
|
|
type: "text",
|
|
text: JSON.stringify(serialized.structuredContent),
|
|
});
|
|
} finally {
|
|
await client.close();
|
|
}
|
|
},
|
|
);
|
|
},
|
|
);
|