diff --git a/docs/mcp-contracts.md b/docs/mcp-contracts.md index c7f74dd9a6..281a9f95c9 100644 --- a/docs/mcp-contracts.md +++ b/docs/mcp-contracts.md @@ -210,8 +210,13 @@ unresolved relationship reads. Cancellation remains a tool error. `trace_feature` validates each reference's containing-procedure resolution using the same contract as navigation context. Explicit absence remains a successful -negative observation; malformed provider replies return `unreadable_output` -for `resolve_containing_procedure` before successful-result projection. +negative observation. If the provider reports a non-retryable capability refusal +attributed to the owner lookup without incomplete cleanup, the reference remains visible +with `containing_procedure.status: "unavailable"` and a residual unknown. That +unknown does not imply references were truncated. +Malformed provider replies return `unreadable_output` for +`resolve_containing_procedure` before successful-result projection; +cancellation and incomplete cleanup remain errors. REA prepares complete MCP results within the pinned stdio client's 10 MiB receive-buffer budget, including both text and structured representations and diff --git a/scripts/lib/kuna-reference-verifier.mjs b/scripts/lib/kuna-reference-verifier.mjs index 89258ee826..ff393f441a 100644 --- a/scripts/lib/kuna-reference-verifier.mjs +++ b/scripts/lib/kuna-reference-verifier.mjs @@ -21,6 +21,8 @@ export const verifyKunaReferences = async ({ address, expectedCaller, nativeIncoming, + verifyNamedTrace = false, + minimumTraceReferences = 1, }) => { const before = await Promise.all([ entries(join(cacheRoot, "projects")), @@ -80,7 +82,44 @@ export const verifyKunaReferences = async ({ direction: "forward", }); assert.equal(path.value.goal_status, "reached"); - composed = { graph_ms: graph.ms, path_ms: path.ms }; + let traceMs; + if (verifyNamedTrace) { + const trace = await call("trace_feature", { query: name }); + traceMs = trace.ms; + assert.ok( + trace.value.matches.some( + (match) => match.type === "procedure" && match.address === address, + ), + `${name} was not retained in its feature trace`, + ); + assert.ok( + incoming.value.length >= minimumTraceReferences, + `${name} fixture did not produce the expected incoming references`, + ); + const references = trace.value.references.filter( + (row) => row.target_address === address, + ); + assert.deepEqual( + references.map((row) => row.source_address).sort(), + [...incoming.value].sort(), + `${name} lost or duplicated an observed incoming reference`, + ); + for (const reference of references) { + assert.equal(reference.containing_procedure.status, "unavailable"); + assert.equal(reference.containing_procedure.provider_id, "kuna"); + assert.ok( + trace.value.residual_unknowns.some((unknown) => + unknown.includes(reference.source_address), + ), + ); + } + assert.equal(trace.value.truncated, false); + } + composed = { + graph_ms: graph.ms, + path_ms: path.ms, + ...(traceMs === undefined ? {} : { feature_trace_ms: traceMs }), + }; } assert.deepEqual( await Promise.all([ diff --git a/scripts/verify-real-kuna.mjs b/scripts/verify-real-kuna.mjs index 926e085c94..b301013c6d 100644 --- a/scripts/verify-real-kuna.mjs +++ b/scripts/verify-real-kuna.mjs @@ -115,7 +115,9 @@ __attribute__((noinline)) int rea_classify(int c) { } int main(int argc, char **argv) { if (argc < 2) return 2; - printf("%u %d\n", rea_checksum((const unsigned char *)argv[1], strlen(argv[1])), + size_t length = strlen(argv[1]); + printf("%u %u %d\n", rea_checksum((const unsigned char *)argv[1], length), + rea_checksum((const unsigned char *)argv[1], length ? length - 1 : 0), rea_classify(argv[1][0])); return 0; } @@ -282,8 +284,12 @@ const quality = (text) => ({ const verifyKuna = async ( path, functions, - expectedCaller, - expectedStrings = [], + { + expectedCaller, + expectedStrings = [], + verifyNamedTrace = false, + minimumTraceReferences = {}, + } = {}, ) => { const sha256 = createHash("sha256") .update(await readFile(path)) @@ -320,6 +326,8 @@ const verifyKuna = async ( name, address, expectedCaller, + verifyNamedTrace, + minimumTraceReferences: minimumTraceReferences[name], nativeIncoming: (targetAddress) => nativeKunaIncomingSites({ executable: kuna, @@ -549,10 +557,12 @@ try { for (const binary of [built, stripped]) results.push({ binary: binary.slice(workspace.length + 1), - kuna: await verifyKuna(binary, [...fixtureFunctions], fixtureMain, [ - "_φ( °-°)/ REA unicode marker", - "REA wide marker", - ]), + kuna: await verifyKuna(binary, [...fixtureFunctions], { + expectedCaller: fixtureMain, + expectedStrings: ["_φ( °-°)/ REA unicode marker", "REA wide marker"], + verifyNamedTrace: binary === built, + minimumTraceReferences: { rea_checksum: 2 }, + }), }); // Caller-built matrix. diff --git a/src/application/CapabilityInventory.test.ts b/src/application/CapabilityInventory.test.ts index d742d6c2f3..6c965c26c9 100644 --- a/src/application/CapabilityInventory.test.ts +++ b/src/application/CapabilityInventory.test.ts @@ -776,6 +776,23 @@ describe("trace_feature requirements", () => { reason: null, })); + it("remains available when owner lookup is unavailable", () => { + expect( + entry( + "trace_feature", + status({ + open: true, + kind: "executable", + capabilities: capabilities([ + "search_strings", + "search_procedures", + "xrefs", + ]), + }), + ), + ).toMatchObject({ available: true, reason: "available" }); + }); + it("needs the search operations the workflow actually calls", () => { expect( entry( diff --git a/src/application/CapabilityInventory.ts b/src/application/CapabilityInventory.ts index 193459d3ef..ca01a2b86d 100644 --- a/src/application/CapabilityInventory.ts +++ b/src/application/CapabilityInventory.ts @@ -215,12 +215,7 @@ const ENHANCED_REQUIREMENTS: ReadonlyMap = new Map( ], analyze_function: ["analyze_function"], inspect_native_api: ["analyze_function"], - trace_feature: [ - "search_strings", - "search_procedures", - "xrefs", - "resolve_containing_procedure", - ], + trace_feature: ["search_strings", "search_procedures", "xrefs"], } satisfies Readonly>), ); diff --git a/src/application/EnhancedLiteralTracing.ts b/src/application/EnhancedLiteralTracing.ts index d76a627a37..d46c241f5f 100644 --- a/src/application/EnhancedLiteralTracing.ts +++ b/src/application/EnhancedLiteralTracing.ts @@ -4,12 +4,16 @@ import type { AnalysisExecution, AnalysisOperation, } from "./AnalysisProvider.js"; -import { AnalysisOutputError } from "../domain/analysisErrorCore.js"; +import { + AnalysisCancelledError, + AnalysisCapabilityUnavailableError, + AnalysisOutputError, +} from "../domain/analysisErrorCore.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; import type { JsonValue } from "../domain/jsonValue.js"; import { parseContainingProcedureResolution, - type ContainingProcedureResolution, + type TraceContainingProcedure, } from "../domain/native/nativeObservation.js"; import { err, ok, type Result } from "../domain/result.js"; import type { EnhancedResult, TraceReference } from "./EnhancedToolTypes.js"; @@ -70,7 +74,7 @@ export const traceLiteralFeature = async ( search_mode: "literal", matches: searched.value.matches, references: traced.value.references, - truncated: traced.value.residual.length > 0, + truncated: traced.value.truncated, residual_unknowns: traced.value.residual, }); }; @@ -121,14 +125,18 @@ const traceReferences = async ( observe: ObserveCall, request: ReferenceTraceRequest, ): Promise< - Result<{ references: TraceReference[]; residual: string[] }, AnalysisError> + Result< + { references: TraceReference[]; residual: string[]; truncated: boolean }, + AnalysisError + > > => { const references: TraceReference[] = []; const residual: string[] = []; + let truncated = false; // References identify targets by address, independently of search alternatives. const tracedTargets = new Set(); // A source address is resolved once however many matches reference it. - const containing = new Map(); + const containing = new Map(); for (const match of request.matches) { if (tracedTargets.has(match.address)) continue; tracedTargets.add(match.address); @@ -140,10 +148,12 @@ const traceReferences = async ( if (!observed.ok) return observed; // Only an explicit producer report of truncation is a coverage gap; a // provider that reports nothing keeps its previous, unqualified result. - if (observed.value.referencesTruncated === true) + if (observed.value.referencesTruncated === true) { + truncated = true; residual.push( `References to ${match.address} were truncated by the provider; additional references may exist.`, ); + } const sources = observed.value.result; if (!Array.isArray(sources)) return err( @@ -167,10 +177,34 @@ const traceReferences = async ( { address: source }, request.signal, ); - if (!resolved.ok) return resolved; - const parsed = parseContainingProcedureResolution(resolved.value); - if (!parsed.ok) return parsed; - containingProcedure = parsed.value; + if (!resolved.ok) { + if ( + !(resolved.error instanceof AnalysisCapabilityUnavailableError) || + resolved.error.operation !== "resolve_containing_procedure" || + resolved.error.cleanupIncomplete || + resolved.error.retryable + ) + return resolved; + if (request.signal?.aborted === true) + return err( + new AnalysisCancelledError("trace_feature", { + cause: request.signal.reason, + }), + ); + containingProcedure = { + query_address: source, + status: "unavailable", + provider_id: resolved.error.providerId, + reason: resolved.error.reason, + }; + residual.push( + `Containing procedure for ${source} is unavailable: ${resolved.error.reason}`, + ); + } else { + const parsed = parseContainingProcedureResolution(resolved.value); + if (!parsed.ok) return parsed; + containingProcedure = parsed.value; + } containing.set(source, containingProcedure); } references.push({ @@ -180,5 +214,5 @@ const traceReferences = async ( }); } } - return ok({ references, residual }); + return ok({ references, residual, truncated }); }; diff --git a/src/application/EnhancedToolTypes.ts b/src/application/EnhancedToolTypes.ts index 1850434439..006313b250 100644 --- a/src/application/EnhancedToolTypes.ts +++ b/src/application/EnhancedToolTypes.ts @@ -3,7 +3,7 @@ import type { z } from "zod"; import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; import type { JsonValue } from "../domain/jsonValue.js"; -import type { ContainingProcedureResolution } from "../domain/native/nativeObservation.js"; +import type { TraceContainingProcedure } from "../domain/native/nativeObservation.js"; import { err, type Result } from "../domain/result.js"; import { enhancedInputSchemas, @@ -14,7 +14,7 @@ export type EnhancedResult = Promise>; export type TraceReference = { readonly target_address: string; readonly source_address: string; - readonly containing_procedure: ContainingProcedureResolution; + readonly containing_procedure: TraceContainingProcedure; }; /** One enhanced call whose input was parsed at its owning adapter boundary. */ diff --git a/src/application/WorkflowAnalysisProfile.ts b/src/application/WorkflowAnalysisProfile.ts index bc4be62a4b..f06cf5a215 100644 --- a/src/application/WorkflowAnalysisProfile.ts +++ b/src/application/WorkflowAnalysisProfile.ts @@ -17,7 +17,9 @@ export const workflowAnalysisProfile = ( ? { workflow_revision: 5 } : operation === "batch_decompile" ? { workflow_revision: 4 } - : operation === "get_call_graph" || operation === "trace_feature" - ? { workflow_revision: 3 } - : { workflow_revision: 1 }), + : operation === "trace_feature" + ? { workflow_revision: 4 } + : operation === "get_call_graph" + ? { workflow_revision: 3 } + : { workflow_revision: 1 }), }); diff --git a/src/application/binary/AnalysisSnapshotCache.test.ts b/src/application/binary/AnalysisSnapshotCache.test.ts index 9fe7d51c6d..d3396d4a6a 100644 --- a/src/application/binary/AnalysisSnapshotCache.test.ts +++ b/src/application/binary/AnalysisSnapshotCache.test.ts @@ -1248,6 +1248,11 @@ describe("executions that report truncated references", () => { it("revises the trace_feature workflow profile so earlier results are not replayed", () => { const profile = ANALYSIS_SNAPSHOT_PROFILE; + expect( + workflowAnalysisProfile(profile, "trace_feature").parameters + .workflow_revision, + ).toBe(4); + expect(workflowAnalysisProfile(profile, "trace_feature")).not.toEqual( workflowAnalysisProfile(profile), ); diff --git a/src/contracts/enhancedToolContracts.ts b/src/contracts/enhancedToolContracts.ts index 364b793aab..2a4087899e 100644 --- a/src/contracts/enhancedToolContracts.ts +++ b/src/contracts/enhancedToolContracts.ts @@ -80,7 +80,7 @@ export const ENHANCED_TOOL_CONTRACTS = [ ...FUNCTION_WORKFLOW_TOOL_CONTRACTS, enhanced( "trace_feature", - "Trace the complete literal query, preserving spaces, through every matching string and procedure, their xrefs, and truthful containing-procedure resolution. Returns observations and residual unknowns without inferring reference kinds.", + "Trace the complete literal query, preserving spaces, through every matching string and procedure and their xrefs. Preserve an explicit unavailable owner for a non-retryable containment capability refusal. Returns observations and residual unknowns without inferring reference kinds; truncated only reports an observed reference cap.", enhancedInputSchemas.trace_feature, ), enhanced( diff --git a/src/contracts/toolOutputSchemaGroups.ts b/src/contracts/toolOutputSchemaGroups.ts index 9141b13f60..4b8cab98b3 100644 --- a/src/contracts/toolOutputSchemaGroups.ts +++ b/src/contracts/toolOutputSchemaGroups.ts @@ -1,7 +1,7 @@ import { nativeAddressListSchema, nativeObservationSourceSchemas, - containingProcedureResolutionSchema, + traceContainingProcedureSchema, } from "../domain/native/nativeObservation.js"; import { overviewInventoryCoverageSchema } from "../domain/native/inventoryCoverage.js"; import { @@ -261,7 +261,7 @@ const literalTraceOutput = evidenceResultOf( z.object({ target_address: z.string(), source_address: z.string(), - containing_procedure: containingProcedureResolutionSchema, + containing_procedure: traceContainingProcedureSchema, }), ), truncated: z.boolean(), diff --git a/src/domain/native/nativeObservation.ts b/src/domain/native/nativeObservation.ts index 4b8c614727..e09d51e31f 100644 --- a/src/domain/native/nativeObservation.ts +++ b/src/domain/native/nativeObservation.ts @@ -27,6 +27,20 @@ export type ContainingProcedureResolution = z.infer< typeof containingProcedureResolutionSchema >; +/** A trace can retain a reference when its provider cannot resolve ownership. */ +export const traceContainingProcedureSchema = z.union([ + containingProcedureResolutionSchema, + z.strictObject({ + query_address: z.string(), + status: z.literal("unavailable"), + provider_id: z.string(), + reason: z.string(), + }), +]); +export type TraceContainingProcedure = z.infer< + typeof traceContainingProcedureSchema +>; + /** Validate provider procedure facts before a workflow interprets absence. */ export const parseContainingProcedureResolution = ( value: JsonValue, diff --git a/tests/boundary/mcp/enhancedTools.traceFeatureCoverage.test.ts b/tests/boundary/mcp/enhancedTools.traceFeatureCoverage.test.ts index c9df3b2383..7b672f183b 100644 --- a/tests/boundary/mcp/enhancedTools.traceFeatureCoverage.test.ts +++ b/tests/boundary/mcp/enhancedTools.traceFeatureCoverage.test.ts @@ -3,7 +3,10 @@ import { afterEach, describe, expect, it } from "vitest"; import type { AnalysisOperationPort } from "../../../src/application/AnalysisProvider.js"; import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js"; import { EnhancedTools } from "../../../src/application/EnhancedTools.js"; -import { AnalysisOutputError } from "../../../src/domain/analysisErrorCore.js"; +import { + AnalysisCapabilityUnavailableError, + AnalysisOutputError, +} from "../../../src/domain/analysisErrorCore.js"; import { err, ok } from "../../../src/domain/result.js"; import { closeEnhancedToolResources, connect } from "./enhancedToolsHarness.js"; @@ -137,6 +140,195 @@ const trace = async (analysis: AnalysisOperationPort) => { return result.structuredContent as { normalized_result: unknown }; }; +it("preserves references and explicitly unknown owners without claiming truncation", async () => { + const baseline = provider(); + const output = await trace({ + execute: (operation, input, options) => + operation === "resolve_containing_procedure" && input.address === "0x20" + ? Promise.resolve( + err( + new AnalysisCapabilityUnavailableError( + "fixture", + operation, + "not supported", + ), + ), + ) + : baseline.execute(operation, input, options), + }); + expect(output.normalized_result).toMatchObject({ + truncated: false, + residual_unknowns: [expect.stringContaining("0x20")], + references: [ + { + target_address: "0x10", + source_address: "0x20", + containing_procedure: { + query_address: "0x20", + status: "unavailable", + provider_id: "fixture", + reason: "not supported", + }, + }, + { + target_address: "0x11", + source_address: "0x21", + containing_procedure: containing("0x21"), + }, + ], + }); + expect(output.normalized_result).toMatchObject({ + residual_unknowns: [expect.stringContaining("not supported")], + }); +}); + +it("keeps distinct targets when a capped reference source has an unavailable owner", async () => { + const calls: string[] = []; + const baseline = provider({ duplicateProcedure: true, calls }); + const output = await trace({ + execute: (operation, input, options) => { + if (operation === "xrefs") { + calls.push(`xrefs:${String(input.address)}`); + return Promise.resolve( + ok( + createAnalysisExecution(["0x20"], PROVIDER, { + referencesTruncated: input.address === "0x10", + }), + ), + ); + } + if (operation === "resolve_containing_procedure") { + calls.push(`resolve_containing_procedure:${String(input.address)}`); + return Promise.resolve( + err( + new AnalysisCapabilityUnavailableError( + "fixture", + operation, + "not supported", + ), + ), + ); + } + return baseline.execute(operation, input, options); + }, + }); + + expect(calls.filter((call) => call.startsWith("xrefs:"))).toEqual([ + "xrefs:0x10", + "xrefs:0x11", + ]); + expect( + calls.filter((call) => call.startsWith("resolve_containing_procedure:")), + ).toEqual(["resolve_containing_procedure:0x20"]); + expect(output.normalized_result).toMatchObject({ + truncated: true, + residual_unknowns: [ + expect.stringContaining("0x10"), + expect.stringContaining("0x20"), + ], + references: [ + { + target_address: "0x10", + source_address: "0x20", + containing_procedure: { + status: "unavailable", + reason: "not supported", + }, + }, + { + target_address: "0x11", + source_address: "0x20", + containing_procedure: { + status: "unavailable", + reason: "not supported", + }, + }, + ], + }); +}); + +it("does not swallow an unavailable owner with incomplete cleanup", async () => { + const baseline = provider(); + const result = await new EnhancedTools({ + execute: (operation, input, options) => + operation === "resolve_containing_procedure" + ? Promise.resolve( + err( + new AnalysisCapabilityUnavailableError( + "fixture", + operation, + "cleanup failed", + { + cleanup: { + reason: "cleanup failed", + resources: ["owned worker"], + }, + }, + ), + ), + ) + : baseline.execute(operation, input, options), + }).execute("trace_feature", { query: "hello" }); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error.cleanupIncomplete).toBe(true); +}); + +it("preserves cancellation that arrives during an unavailable owner lookup", async () => { + const baseline = provider(); + const controller = new AbortController(); + const reason = { request: "cancel during owner lookup" }; + const result = await new EnhancedTools({ + execute: (operation, input, options) => { + if (operation !== "resolve_containing_procedure") + return baseline.execute(operation, input, options); + controller.abort(reason); + return Promise.resolve( + err( + new AnalysisCapabilityUnavailableError( + "fixture", + operation, + "not supported", + ), + ), + ); + }, + }).execute("trace_feature", { query: "hello" }, controller.signal); + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error._tag).toBe("AnalysisCancelledError"); + expect(result.error.cause).toBe(reason); + } +}); + +it("retains incomplete cleanup when cancellation arrives during owner lookup", async () => { + const baseline = provider(); + const controller = new AbortController(); + const result = await new EnhancedTools({ + execute: (operation, input, options) => { + if (operation !== "resolve_containing_procedure") + return baseline.execute(operation, input, options); + controller.abort(); + return Promise.resolve( + err( + new AnalysisCapabilityUnavailableError( + "fixture", + operation, + "cleanup failed", + { + cleanup: { + reason: "cleanup failed", + resources: ["owned worker"], + }, + }, + ), + ), + ); + }, + }).execute("trace_feature", { query: "hello" }, controller.signal); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.error.cleanupIncomplete).toBe(true); +}); + describe("trace_feature reference coverage", () => { it("marks the trace truncated for the target whose references the provider capped", async () => { const output = await trace( diff --git a/tests/boundary/mcp/idaReadinessFeatureTrace.test.ts b/tests/boundary/mcp/idaReadinessFeatureTrace.test.ts new file mode 100644 index 0000000000..430b29d2f3 --- /dev/null +++ b/tests/boundary/mcp/idaReadinessFeatureTrace.test.ts @@ -0,0 +1,87 @@ +import { rm } from "node:fs/promises"; +import { afterEach, expect, it } from "vitest"; +import { AnalysisCapabilityUnavailableError } from "../../../src/domain/analysisErrorCore.js"; +import { IdaSessionClient } from "../../../src/ida/IdaSessionClient.js"; +import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; +import { createIdaTarget, RecordingIdaMcp } from "../../fixtures/idaMcp.js"; +import { closeEnhancedToolResources, connect } from "./enhancedToolsHarness.js"; + +afterEach(closeEnhancedToolResources); + +it.each([2, 3])( + "preserves unknown IDA readiness at owner health check %s", + async (failureCheck) => { + const { root, target } = await createIdaTarget("arm64"); + const producer = new RecordingIdaMcp(target, "headless"); + let xrefsSeen = false; + let healthAfterXrefs = 0; + producer.overrideCall = (name) => { + if (name === "xrefs_to") xrefsSeen = true; + if ( + name === "server_health" && + xrefsSeen && + ++healthAfterXrefs >= failureCheck + ) + return { + status: "ok", + input_path: producer.inputPath, + idb_path: `${producer.inputPath}.i64`, + module: "target.elf", + imagebase: producer.imageBase, + auto_analysis_ready: null, + }; + return undefined; + }; + const ida = new IdaSessionClient( + { + command: "fixture", + args: [], + env: {}, + mode: "headless", + timeoutMs: 1000, + workspaceRoot: root, + }, + target, + producer, + ); + let refusal: AnalysisCapabilityUnavailableError | undefined; + const mcp = await connect({ + execute: async (operation, input, options) => { + const result = await ida.execute(operation, input, options); + if ( + operation === "resolve_containing_procedure" && + !result.ok && + result.error instanceof AnalysisCapabilityUnavailableError + ) + refusal = result.error; + return result; + }, + }); + try { + const result = await mcp.callTool({ + name: "trace_feature", + arguments: { query: "a.b" }, + }); + const error = parseMcpToolError(result).error; + expect(error).toMatchObject({ + code: "capability_unavailable", + details: { + operation: "server_health", + diagnostics: { auto_analysis_ready: null }, + }, + }); + expect(refusal?.remediationAction).toBeDefined(); + expect(error.remediation?.action).toBe(refusal?.remediationAction); + expect(healthAfterXrefs).toBe(failureCheck); + if (failureCheck === 3) { + expect(error.details?.partial_observation).toMatchObject({ + operation: "resolve_containing_procedure", + provider: { id: "ida" }, + }); + } + } finally { + expect((await ida.close()).ok).toBe(true); + await rm(root, { recursive: true, force: true }); + } + }, +); diff --git a/tests/boundary/providers/kuna/fakeKuna.ts b/tests/boundary/providers/kuna/fakeKuna.ts index ed04cf0631..98189c276d 100644 --- a/tests/boundary/providers/kuna/fakeKuna.ts +++ b/tests/boundary/providers/kuna/fakeKuna.ts @@ -71,6 +71,8 @@ export interface FakeKunaBehavior { /** Bytes of an extra root member in the graph JSON. */ readonly graphPadding?: number; readonly xrefs?: "ok" | "hang" | "not-json"; + /** Additional exact-target incoming call-site addresses. */ + readonly extraXrefSources?: readonly number[]; readonly referenceDelayMs?: number; readonly referencePadding?: number; /** `invalid-then-hang` writes a contradictory first row, then never finishes. */ @@ -155,7 +157,7 @@ if (args[0] === "--version") { const identity = (name, value) => ({ name, address: value, address_hex: "0x" + value.toString(16) }); const aliases = target === "0x2000" ? [identity("puts.slot", 12288)] : []; const row = (from, to, kind) => ({ address: from, address_hex: "0x" + from.toString(16), from_address: from, from_address_hex: "0x" + from.toString(16), to_address: to, to_address_hex: "0x" + to.toString(16), kind, from_function: identity("main", 4096), to_function: null, instruction: "fixture reference" + "x".repeat(behavior.referencePadding ?? 0) }); - const rows = [row(4100, parseInt(target, 16), "call")]; + const rows = [4100, ...(behavior.extraXrefSources ?? [])].map((source) => row(source, parseInt(target, 16), "call")); if (aliases.length) rows.push(row(4164, 12288, "call")); setTimeout(() => process.stdout.write(JSON.stringify({ binary: args[1], direction: "to", count: rows.length, target: { ...identity(null, parseInt(target, 16)), aliases }, xrefs: rows })), behavior.referenceDelayMs ?? 0); } diff --git a/tests/boundary/providers/kuna/kunaVerifier.test.ts b/tests/boundary/providers/kuna/kunaVerifier.test.ts index f006771e38..fffc66a356 100644 --- a/tests/boundary/providers/kuna/kunaVerifier.test.ts +++ b/tests/boundary/providers/kuna/kunaVerifier.test.ts @@ -146,6 +146,7 @@ it.each([ async ({ rejectClose, rejectRead, wrongXrefs, wrongStrings }) => { const root = await createTestTempDirectory("rea-kuna-verifier-cleanup-"); const kuna = await writeFakeKuna(root, { + extraXrefSources: [4104], stringRows: [ { address: 12288, text: "REA fixture prompt" }, {