Files
reverse-engineering-anything/tests/boundary/providers/ida/containment.test.ts
T
Mykola Dementiev 5f8af4ee5f feat(ida): resolve containing procedures and report the admitted program as the document (#2374)
* 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
2026-10-13 20:56:20 +08:00

145 lines
4.3 KiB
TypeScript

import { rm } from "node:fs/promises";
import { afterEach, describe, expect, it } from "vitest";
import { inspectAddressContext } from "../../../../src/application/AnalysisContextQueries.js";
import { EnhancedTools } from "../../../../src/application/EnhancedTools.js";
import { IdaSessionClient } from "../../../../src/ida/IdaSessionClient.js";
import { createIdaTarget, RecordingIdaMcp } from "../../../fixtures/idaMcp.js";
const roots: string[] = [];
afterEach(async () => {
for (const root of roots.splice(0))
await rm(root, { recursive: true, force: true });
});
const session = async (cappedReferences = false) => {
const { root, target } = await createIdaTarget("arm64");
roots.push(root);
const producer = new RecordingIdaMcp(target, "headless");
if (cappedReferences) {
const inner = producer.overrideCall;
producer.overrideCall = (name, args) =>
name === "xrefs_to"
? [
{
xrefs: [
{
addr: "0x1004",
type: "code",
fn: { addr: "0x1000", name: "main", size: "0x20" },
},
],
more: true,
},
]
: inner?.(name, args);
}
const client = new IdaSessionClient(
{
command: "fixture",
args: [],
env: {},
mode: "headless",
timeoutMs: 1000,
workspaceRoot: root,
},
target,
producer,
);
return { client, target };
};
const MAIN = {
found: true,
procedure: { address: "0x1000", name: "main" },
};
describe("trace_feature on headless IDA", () => {
it("attributes a reference to its containing procedure", async () => {
const { client } = await session();
try {
const result = await new EnhancedTools(client).execute("trace_feature", {
query: "a.b",
});
if (!result.ok) throw result.error;
expect(result.value).toMatchObject({
matches: [{ type: "string", address: "0x3000" }],
references: [
{
target_address: "0x3000",
source_address: "0x1004",
containing_procedure: MAIN,
},
],
truncated: false,
residual_unknowns: [],
});
} finally {
expect((await client.close()).ok).toBe(true);
}
});
it("keeps an upstream reference cap visible once containment makes the trace run", async () => {
const { client } = await session(true);
try {
const result = await new EnhancedTools(client).execute("trace_feature", {
query: "a.b",
});
if (!result.ok) throw result.error;
expect(result.value).toMatchObject({
references: [{ source_address: "0x1004", containing_procedure: MAIN }],
truncated: true,
residual_unknowns: [
"References to 0x3000 were truncated by the provider; additional references may exist.",
],
});
} finally {
expect((await client.close()).ok).toBe(true);
}
});
});
describe("inspect_address_context on headless IDA", () => {
it("resolves the admitted program as the document and reports the containing procedure", async () => {
const { client, target } = await session();
try {
const result = await inspectAddressContext(client, {
address: "0x1004",
});
if (!result.ok) throw result.error;
expect(result.value).toMatchObject({
address: "0x1004",
document: target.path,
procedure: { state: "available", value: MAIN },
name: { state: "unavailable" },
comment: { state: "unavailable" },
inline_comment: { state: "unavailable" },
bookmarks: { state: "unavailable" },
});
} finally {
expect((await client.close()).ok).toBe(true);
}
});
it("answers the same way for an explicit admitted document", async () => {
const { client, target } = await session();
try {
const implicit = await inspectAddressContext(client, {
address: "0x1004",
});
const explicit = await inspectAddressContext(client, {
address: "0x1004",
document: target.path,
});
if (!implicit.ok) throw implicit.error;
if (!explicit.ok) throw explicit.error;
expect(explicit.value).toEqual(implicit.value);
} finally {
expect((await client.close()).ok).toBe(true);
}
});
});