fix(native): retain partial call routes and omitted sites

Separate enumeration outcome from retained facts in the native call-route
owner. Keep every available typed call with its source provider and site,
supplement incomplete kinds with inferred fallback targets, and retain
known routes when fallback fails. Per-reference unknowns take precedence
over aggregate availability; cancellation remains terminal.

Project partial routes before enumeration failures, preserve each omitted
route's relation, and include its call site in the omitted-edge identity
so distinct observations to one target cannot overwrite each other.
Remove the superseded success-only provider/result representation.

Validation: 69 focused cases plus final 30-case rerun, including nine
public SDK MCP regressions; full static/architecture, docs and discovery
checks pass. Controlled-provider source REDs reproduce both route loss
and omitted-site collisions. Real native providers remain unverified.

Closes #5073
Closes #3922
Closes #5128
This commit is contained in:
morluto
2026-10-19 06:48:14 +08:00
parent 22ddd82014
commit 65884350ff
5 changed files with 961 additions and 156 deletions
+7
View File
@@ -216,6 +216,13 @@ sites remain distinct. A static route does not establish runtime reachability.
Swift/closure dispatch requires resolved static references; unavailable ABI
forms remain unknown.
Incomplete reference kinds do not discard known typed calls. Callee fallback
adds inferred targets while each retained route keeps its original provider
and call site. If enumeration fails, known routes remain alongside an unresolved
failure; cancellation still terminates the request. A node-limit omission keeps
the route's known relation while marking its target unresolved. Distinct call
sites to the same omitted target retain separate evidence.
Session discovery describes two automatic source routes for this trace:
`authored_ui_dispatch` needs Interface Builder decoding and dispatch metadata;
`native_seed_call_trace` needs native address resolution, dispatch metadata and
+411 -57
View File
@@ -4,9 +4,40 @@ import { createAnalysisExecution } from "../AnalysisProvider.js";
import { ghidraReferenceEdge } from "../../domain/ghidraValues.fixture.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import { err, ok } from "../../domain/result.js";
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
} from "../../domain/analysisErrorCore.js";
const provider = { id: "fixture", name: "Fixture", version: "1" };
const callReference = (source: string, target: string) => ({
source_address: source,
target_address: target,
target_procedure: { address: target },
kind: {
available: true as const,
provenance: "fixture-reference",
type: "UNCONDITIONAL_CALL",
flow: true,
call: true,
jump: false,
data: false,
read: false,
write: false,
indirect: false,
computed: false,
external: false,
conditional: false,
terminal: false,
primary: true,
operand_index: 0,
},
});
const unavailableCallKind = {
available: false as const,
reason: "Reference kind is unavailable.",
};
describe("untyped native call route fallback", () => {
it.each([{ callees: [] }, { callees: ["0x2000"] }])(
"preserves targetless calls and their provider when fallback callees are $callees",
@@ -50,35 +81,343 @@ describe("untyped native call route fallback", () => {
);
expect(calls).toEqual(["procedure_references", "procedure_callees"]);
expect(result).toEqual({
ok: true,
value: {
routes: callees.map((address) => ({
provider: fallbackProvider,
routes: callees.map((address) => ({
address,
site: "0x1000",
relation: "provider_call",
resolution: "inferred",
})),
unknowns: [{ ...unknown, provider: referenceProvider }],
limitations: [
{
provider: referenceProvider,
operation: "procedure_references",
limitation: "Reference-kind metadata is incomplete.",
},
{
provider: fallbackProvider,
operation: "procedure_callees",
limitation: "Callee enumeration is limited to named functions.",
},
],
},
address,
site: "0x1000",
relation: "provider_call",
resolution: "inferred",
})),
unknowns: [{ ...unknown, provider: referenceProvider }],
limitations: [
{
provider: referenceProvider,
operation: "procedure_references",
limitation: "Reference-kind metadata is incomplete.",
},
{
provider: fallbackProvider,
operation: "procedure_callees",
limitation: "Callee enumeration is limited to named functions.",
},
],
outcome: { ok: true, value: undefined },
});
},
);
});
describe("typed native call routes", () => {
describe("partially typed native call routes", () => {
it.each([false, true])(
"keeps known typed calls when an individual kind is unavailable (aggregate=$0)",
async (referenceKindsAvailable) => {
const typedProvider = { ...provider, id: "typed" };
const fallbackProvider = { ...provider, id: "fallback" };
const result = await readNativeCallRoutes(
{
execute: async (operation) =>
operation === "procedure_references"
? ok(
createAnalysisExecution(
{
reference_kinds_available: referenceKindsAvailable,
references: [
callReference("0x1004", "0x2000"),
{
...callReference("0x1008", "0x3000"),
kind: unavailableCallKind,
},
],
unresolved_calls: [],
},
typedProvider,
{ limitations: ["Some reference kinds are unavailable."] },
),
)
: ok(
createAnalysisExecution(
["0x2000", "0x3000", "0x4000"],
fallbackProvider,
{ limitations: ["Callee enumeration is incomplete."] },
),
),
},
"0x1000",
);
expect(result).toEqual({
routes: [
{
provider: typedProvider,
address: "0x2000",
site: "0x1004",
relation: "direct_call",
resolution: "observed",
},
{
provider: fallbackProvider,
address: "0x3000",
site: "0x1000",
relation: "provider_call",
resolution: "inferred",
},
{
provider: fallbackProvider,
address: "0x4000",
site: "0x1000",
relation: "provider_call",
resolution: "inferred",
},
],
unknowns: [],
limitations: [
{
provider: typedProvider,
operation: "procedure_references",
limitation: "Some reference kinds are unavailable.",
},
{
provider: fallbackProvider,
operation: "procedure_callees",
limitation: "Callee enumeration is incomplete.",
},
],
outcome: { ok: true, value: undefined },
});
},
);
it("preserves distinct typed sites when fallback overlaps their target", async () => {
const typedProvider = { ...provider, id: "typed" };
const fallbackProvider = { ...provider, id: "fallback" };
const result = await readNativeCallRoutes(
{
execute: async (operation) =>
operation === "procedure_references"
? ok(
createAnalysisExecution(
{
reference_kinds_available: false,
references: [
callReference("0x1004", "0x2000"),
callReference("0x1008", "0x2000"),
{
...callReference("0x100c", "0x4000"),
kind: unavailableCallKind,
},
],
unresolved_calls: [],
},
typedProvider,
),
)
: ok(
createAnalysisExecution(["0x2000", "0x3000"], fallbackProvider),
),
},
"0x1000",
);
expect(result).toEqual({
routes: [
{
provider: typedProvider,
address: "0x2000",
site: "0x1004",
relation: "direct_call",
resolution: "observed",
},
{
provider: typedProvider,
address: "0x2000",
site: "0x1008",
relation: "direct_call",
resolution: "observed",
},
{
provider: fallbackProvider,
address: "0x3000",
site: "0x1000",
relation: "provider_call",
resolution: "inferred",
},
],
unknowns: [],
limitations: [],
outcome: { ok: true, value: undefined },
});
});
});
describe("native call fallback output failures", () => {
it("retains source limitations when fallback output is invalid", async () => {
const typedProvider = { ...provider, id: "typed" };
const fallbackProvider = { ...provider, id: "fallback" };
const result = await readNativeCallRoutes(
{
execute: async (operation) =>
operation === "procedure_references"
? err(
new AnalysisCapabilityUnavailableError(
typedProvider.id,
"procedure_references",
"Typed references are unavailable.",
),
)
: ok(
createAnalysisExecution(
{ callees: "not-an-address-list" },
fallbackProvider,
{ limitations: ["Fallback output is truncated."] },
),
),
},
"0x1000",
);
expect(result).toMatchObject({
routes: [],
unknowns: [],
limitations: [
{
provider: fallbackProvider,
operation: "procedure_callees",
limitation: "Fallback output is truncated.",
},
],
outcome: {
ok: false,
error: { _tag: "AnalysisOutputError" },
},
});
});
it("retains known typed routes when fallback output is invalid", async () => {
const typedProvider = { ...provider, id: "typed" };
const fallbackProvider = { ...provider, id: "fallback" };
const result = await readNativeCallRoutes(
{
execute: async (operation) =>
operation === "procedure_references"
? ok(
createAnalysisExecution(
{
reference_kinds_available: false,
references: [
callReference("0x1004", "0x2000"),
{
...callReference("0x1008", "0x3000"),
kind: unavailableCallKind,
},
],
unresolved_calls: [],
},
typedProvider,
{ limitations: ["One call kind remains unavailable."] },
),
)
: ok(
createAnalysisExecution(
{ callees: "not-an-address-list" },
fallbackProvider,
{ limitations: ["Fallback output is truncated."] },
),
),
},
"0x1000",
);
expect(result).toMatchObject({
routes: [
{
provider: typedProvider,
address: "0x2000",
site: "0x1004",
relation: "direct_call",
resolution: "observed",
},
],
unknowns: [],
limitations: [
{
provider: typedProvider,
operation: "procedure_references",
limitation: "One call kind remains unavailable.",
},
{
provider: fallbackProvider,
operation: "procedure_callees",
limitation: "Fallback output is truncated.",
},
],
outcome: {
ok: false,
error: { _tag: "AnalysisOutputError" },
},
});
});
});
describe("native call fallback cancellation", () => {
it("keeps known typed routes when fallback is cancelled", async () => {
let calls = 0;
const referenceProvider = { ...provider, id: "references" };
const result = await readNativeCallRoutes(
{
execute: async (operation) => {
calls += 1;
return operation === "procedure_references"
? ok(
createAnalysisExecution(
{
reference_kinds_available: false,
references: [
callReference("0x1004", "0x2000"),
{
...callReference("0x1008", "0x3000"),
kind: unavailableCallKind,
},
],
unresolved_calls: [],
},
referenceProvider,
{ limitations: ["One call kind remains unavailable."] },
),
)
: err(new AnalysisCancelledError("procedure_callees"));
},
},
"0x1000",
);
expect(result).toMatchObject({
routes: [
{
provider: referenceProvider,
address: "0x2000",
site: "0x1004",
relation: "direct_call",
resolution: "observed",
},
],
unknowns: [],
limitations: [
{
provider: referenceProvider,
operation: "procedure_references",
limitation: "One call kind remains unavailable.",
},
],
outcome: {
ok: false,
error: { _tag: "AnalysisCancelledError" },
},
});
expect(calls).toBe(2);
});
});
describe("complete typed native call routes", () => {
it("preserves computed targets, ambiguity and targetless call sites", async () => {
const reference = jsonValueSchema.parse(ghidraReferenceEdge());
if (
@@ -87,13 +426,15 @@ describe("typed native call routes", () => {
Array.isArray(reference)
)
throw new TypeError("Invalid reference fixture");
const calls: string[] = [];
const result = await readNativeCallRoutes(
{
execute: async () =>
ok(
execute: async (operation) => {
calls.push(operation);
return ok(
createAnalysisExecution(
{
reference_kinds_available: true,
reference_kinds_available: false,
references: ["0x2000", "0x3000"].map((address) => ({
...reference,
source_address: "0x1004",
@@ -125,36 +466,41 @@ describe("typed native call routes", () => {
provider,
{ limitations: ["SEH handler targets may be outside the body."] },
),
),
);
},
},
"0x1000",
);
expect(result).toMatchObject({
ok: true,
value: {
routes: [
{
address: "0x2000",
relation: "indirect_call",
resolution: "ambiguous",
},
{
address: "0x3000",
relation: "indirect_call",
resolution: "ambiguous",
},
],
unknowns: [{ address: "0x1008" }],
limitations: [
{
provider,
operation: "procedure_references",
limitation: "SEH handler targets may be outside the body.",
},
],
},
routes: [
{
provider,
address: "0x2000",
relation: "indirect_call",
resolution: "ambiguous",
},
{
provider,
address: "0x3000",
relation: "indirect_call",
resolution: "ambiguous",
},
],
unknowns: [{ address: "0x1008", provider }],
limitations: [
{
provider,
operation: "procedure_references",
limitation: "SEH handler targets may be outside the body.",
},
],
outcome: { ok: true, value: undefined },
});
expect(calls).toEqual(["procedure_references"]);
});
});
describe("malformed typed native call output", () => {
it("does not fall back on cancellation or invalid typed output", async () => {
let calls = 0;
const cancelled = await readNativeCallRoutes(
@@ -167,8 +513,10 @@ describe("typed native call routes", () => {
"0x1000",
);
expect(cancelled).toMatchObject({
ok: false,
error: { _tag: "AnalysisCancelledError" },
routes: [],
unknowns: [],
limitations: [],
outcome: { ok: false, error: { _tag: "AnalysisCancelledError" } },
});
expect(calls).toBe(1);
const malformed = await readNativeCallRoutes(
@@ -183,8 +531,8 @@ describe("typed native call routes", () => {
"0x1000",
);
expect(malformed).toMatchObject({
ok: false,
error: { _tag: "AnalysisOutputError" },
routes: [],
unknowns: [],
limitations: [
{
provider,
@@ -192,8 +540,12 @@ describe("typed native call routes", () => {
limitation: "Malformed output still has source caveats.",
},
],
outcome: { ok: false, error: { _tag: "AnalysisOutputError" } },
});
});
});
describe("missing unresolved-call observations", () => {
it("rejects omitted unresolved-call observations instead of claiming none", async () => {
let calls = 0;
const result = await readNativeCallRoutes(
@@ -215,8 +567,10 @@ describe("typed native call routes", () => {
);
expect(result).toMatchObject({
ok: false,
error: { _tag: "AnalysisOutputError" },
routes: [],
unknowns: [],
limitations: [],
outcome: { ok: false, error: { _tag: "AnalysisOutputError" } },
});
expect(calls).toBe(1);
});
+82 -61
View File
@@ -9,6 +9,8 @@ import {
referenceKindSchema,
} from "../../domain/hopperValues.js";
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { err, ok, type Result } from "../../domain/result.js";
import type { SourceLimitation } from "../SourceLimitations.js";
/** Preferred typed observations and the untyped fallback share one owner. */
@@ -39,9 +41,30 @@ const referencesSchema = z.object({
),
});
interface NativeCallRoute {
readonly address: string;
readonly site: string;
readonly relation: "direct_call" | "indirect_call" | "provider_call";
readonly resolution: "observed" | "resolved" | "ambiguous" | "inferred";
readonly provider: ProviderIdentity;
}
/** Enumeration failure does not invalidate facts already obtained. */
interface NativeCallRouteRead {
readonly routes: readonly NativeCallRoute[];
readonly unknowns: readonly {
address: string;
reason: string;
provider: ProviderIdentity;
}[];
readonly limitations: readonly SourceLimitation[];
readonly outcome: Result<void, AnalysisError>;
}
const typedCallRoutes = (
references: z.infer<typeof referencesSchema>["references"],
) => {
provider: ProviderIdentity,
): NativeCallRoute[] => {
const calls = references.filter(({ kind }) => kind.available && kind.call);
const countBySite = new Map<string, Set<string>>();
for (const call of calls) {
@@ -50,6 +73,7 @@ const typedCallRoutes = (
countBySite.set(call.source_address, targets);
}
return calls.map((call) => ({
provider,
address: call.target_procedure?.address ?? call.target_address,
site: call.source_address,
relation:
@@ -65,13 +89,13 @@ const typedCallRoutes = (
}));
};
/** Prefer typed static call references, preserving computed targets and targetless sites. */
/** Retain typed observations while supplementing incomplete kinds with inferred callees. */
export const readNativeCallRoutes = async (
analysis: AnalysisOperationPort,
procedure: string,
signal?: AbortSignal,
selectorOrigin?: ExecutionOptions["selectorOrigin"],
) => {
): Promise<NativeCallRouteRead> => {
const options = {
...(signal === undefined ? {} : { signal }),
...(selectorOrigin === undefined ? {} : { selectorOrigin }),
@@ -81,88 +105,85 @@ export const readNativeCallRoutes = async (
{ procedure, direction: "outgoing" },
options,
);
let unknowns: Array<{
const routes: NativeCallRoute[] = [];
const unknowns: Array<{
address: string;
reason: string;
provider: ProviderIdentity;
}> = [];
let limitations: SourceLimitation[] = [];
const limitations: SourceLimitation[] = [];
const finish = (
outcome: Result<void, AnalysisError>,
): NativeCallRouteRead => ({
routes,
unknowns,
limitations,
outcome,
});
if (execution.ok) {
limitations = limitationsFrom(
NATIVE_CALL_ROUTE_SOURCES.references,
execution.value.provider,
execution.value.limitations,
limitations.push(
...limitationsFrom(
NATIVE_CALL_ROUTE_SOURCES.references,
execution.value.provider,
execution.value.limitations,
),
);
const parsed = referencesSchema.safeParse(execution.value.result);
if (!parsed.success)
return {
ok: false as const,
error: new AnalysisOutputError(
NATIVE_CALL_ROUTE_SOURCES.references,
"Invalid typed call references",
return finish(
err(
new AnalysisOutputError(
NATIVE_CALL_ROUTE_SOURCES.references,
"Invalid typed call references",
),
),
unknowns,
limitations,
};
);
// Missing detailed reference flags do not invalidate observed targetless
// calls. Keep their producing provider even if another provider supplies
// the fallback callee inventory.
unknowns = parsed.data.unresolved_calls.map((call) => ({
...call,
provider: execution.value.provider,
}));
unknowns.push(
...parsed.data.unresolved_calls.map((call) => ({
...call,
provider: execution.value.provider,
})),
);
const references = parsed.data.references;
routes.push(...typedCallRoutes(references, execution.value.provider));
if (
parsed.data.reference_kinds_available === true ||
(references.length > 0 && references.every(({ kind }) => kind.available))
) {
return {
ok: true as const,
value: {
provider: execution.value.provider,
routes: typedCallRoutes(references),
unknowns,
limitations,
},
};
}
references.every(({ kind }) => kind.available) &&
(parsed.data.reference_kinds_available || references.length > 0)
)
return finish(ok(undefined));
} else if (execution.error._tag !== "AnalysisCapabilityUnavailableError")
return {
ok: false as const,
error: execution.error,
unknowns,
limitations,
};
return finish(err(execution.error));
const callees = await analysis.execute(
NATIVE_CALL_ROUTE_SOURCES.callees,
{ procedure },
options,
);
if (!callees.ok)
return { ok: false as const, error: callees.error, unknowns, limitations };
limitations = [
...limitations,
if (!callees.ok) return finish(err(callees.error));
limitations.push(
...limitationsFrom(
NATIVE_CALL_ROUTE_SOURCES.callees,
callees.value.provider,
callees.value.limitations,
),
];
);
const parsed = parseRelatedAddresses(callees.value.result, "callees");
return parsed.ok
? {
ok: true as const,
value: {
provider: callees.value.provider,
routes: parsed.value.map((address) => ({
address,
site: procedure,
relation: "provider_call" as const,
resolution: "inferred" as const,
})),
unknowns,
limitations,
},
}
: { ok: false as const, error: parsed.error, unknowns, limitations };
if (!parsed.ok) return finish(err(parsed.error));
// Keep every observed call site. A target-only fallback cannot replace one,
// and repeated fallback targets add no new observation.
const targets = new Set(routes.map(({ address }) => address));
for (const address of parsed.value) {
if (targets.has(address)) continue;
targets.add(address);
routes.push({
address,
site: procedure,
relation: "provider_call",
resolution: "inferred",
provider: callees.value.provider,
});
}
return finish(ok(undefined));
};
+34 -38
View File
@@ -202,7 +202,7 @@ export const traceNativeUiAction = async (
max_edges: input.max_edges,
},
});
const expanded = await addDirectCallees({
const expanded = await expandNativeCalls({
analysis,
graph,
initial,
@@ -372,7 +372,7 @@ const resolveUiAction = (
};
};
const addDirectCallees = async (input: {
const expandNativeCalls = async (input: {
readonly analysis: AnalysisOperationPort;
readonly graph: z.infer<typeof nativeInvestigationGraphSchema>;
readonly initial: z.infer<typeof nativeInvestigationTraceSchema>;
@@ -435,14 +435,13 @@ const addDirectCallees = async (input: {
"provider",
);
examined += 1;
sourceLimitations.push(
...(execution.ok ? execution.value.limitations : execution.limitations),
);
if (!execution.ok && execution.error._tag === "AnalysisCancelledError")
return { error: execution.error };
const unknowns = execution.ok
? execution.value.unknowns
: execution.unknowns;
sourceLimitations.push(...execution.limitations);
if (
!execution.outcome.ok &&
execution.outcome.error._tag === "AnalysisCancelledError"
)
return { error: execution.outcome.error };
const unknowns = execution.unknowns;
for (const [index, unknown] of unknowns.entries()) {
if (edges.size >= input.input.max_edges) {
truncated = true;
@@ -470,40 +469,19 @@ const addDirectCallees = async (input: {
limitations: [],
});
}
if (!execution.ok) {
if (edges.size >= input.input.max_edges) {
truncated = true;
omitted += 1;
continue;
}
const id = `unknown-callees:${current.address}`;
edges.set(id, {
id,
from: current.node.id,
to: null,
relation: "direct_call",
resolution: "unresolved",
reason: projectAnalysisError(execution.error).message,
evidence: [],
limitations: [
"The provider could not enumerate resolved direct callees.",
],
});
continue;
}
for (const [index, route] of execution.value.routes.entries()) {
for (const [index, route] of execution.routes.entries()) {
const address = route.address;
if (edges.size >= input.input.max_edges) {
truncated = true;
omitted += execution.value.routes.length - index;
omitted += execution.routes.length - index;
break;
}
const targetId = `native:function:${address}`;
const evidence = [
{
kind: "reference" as const,
provider: execution.value.provider,
description: `${execution.value.provider.name} reported ${route.relation} from ${current.address} to ${address}`,
provider: route.provider,
description: `${route.provider.name} reported ${route.relation} from ${current.address} to ${address}`,
location: { address: route.site, file_offset: null },
artifact_path: null,
artifact_sha256: input.targetSha256,
@@ -513,12 +491,12 @@ const addDirectCallees = async (input: {
if (nodes.size >= input.input.max_nodes) {
truncated = true;
omitted += 1;
const id = `unknown-node-bound:${current.address}:${address}`;
const id = `unknown-node-bound:${current.address}:${route.site}:${address}`;
edges.set(id, {
id,
from: current.node.id,
to: null,
relation: "direct_call",
relation: route.relation,
resolution: "unresolved",
reason: "native_trace_node_bound_reached",
evidence,
@@ -532,7 +510,7 @@ const addDirectCallees = async (input: {
kind: "function" as const,
name,
location: { address, file_offset: null },
attributes: { resolved_by: execution.value.provider.id },
attributes: { resolved_by: route.provider.id },
evidence,
};
nodes.set(targetId, node);
@@ -551,6 +529,24 @@ const addDirectCallees = async (input: {
],
});
}
if (!execution.outcome.ok) {
if (edges.size >= input.input.max_edges) {
truncated = true;
omitted += 1;
continue;
}
const id = `unknown-callees:${current.address}`;
edges.set(id, {
id,
from: current.node.id,
to: null,
relation: "provider_call",
resolution: "unresolved",
reason: projectAnalysisError(execution.outcome.error).message,
evidence: [],
limitations: ["The provider could not complete callee enumeration."],
});
}
}
return {
nodes: [...nodes.values()],
@@ -0,0 +1,427 @@
import { afterEach, expect, it } from "vitest";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { z } from "zod";
import type {
AnalysisExecution,
AnalysisOperationPort,
} from "../../../src/application/AnalysisProvider.js";
import type { AnalysisError } from "../../../src/domain/analysisErrorBase.js";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisOutputError,
} from "../../../src/domain/analysisErrorCore.js";
import { inspectNativeDispatchMetadata } from "../../../src/domain/native/objcSwiftMetadata.js";
import { nativeInvestigationTraceSchema } from "../../../src/domain/native/nativeInvestigationGraph.js";
import { err, ok, type Result } from "../../../src/domain/result.js";
import { createServer } from "../../../src/server/createServer.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
const identity = { id: "typed", name: "Typed provider", version: "1" };
const fallback = { id: "fallback", name: "Fallback provider", version: "1" };
const sha256 = "a".repeat(64);
const subject = {
sha256,
path: "/fixture",
format: "mach-o",
architecture: "arm64",
} as const;
const resources: Array<{ close(): Promise<unknown> }> = [];
afterEach(async () =>
Promise.all(resources.splice(0).map((resource) => resource.close())),
);
function typedKind(indirect = false) {
return {
available: true,
provenance: "fixture reference classification",
type: "call",
flow: true,
call: true,
jump: false,
data: false,
read: false,
write: false,
indirect,
computed: indirect,
conditional: false,
terminal: false,
primary: true,
operand_index: 0,
external: false,
};
}
function ref(site: string, target: string, indirect = false) {
return {
source_address: site,
target_address: target,
target_procedure: { address: target },
kind: typedKind(indirect),
};
}
const mixedReferences = (refs = [ref("0x1004", "0x2000")]) => ({
reference_kinds_available: false,
references: [
...refs,
{
source_address: "0x1008",
target_address: "0x9000",
target_procedure: { address: "0x9000" },
kind: { available: false, reason: "classification unavailable" },
},
],
unresolved_calls: [],
});
const completeReferences = (refs: ReturnType<typeof ref>[]) => ({
reference_kinds_available: true,
references: refs,
unresolved_calls: [],
});
const noClassifiedReferences = {
reference_kinds_available: false,
references: [],
unresolved_calls: [],
};
type Plan = {
readonly references?: ReturnType<typeof mixedReferences>;
readonly callees?: readonly string[];
readonly fallbackProvider?: typeof identity;
readonly fallbackFailure?: "unavailable" | "output" | "cancelled";
};
function publicTrace(plan: Plan, maxNodes = 12) {
const analysis: AnalysisOperationPort = {
execute: async (
operation,
parameters,
): Promise<Result<AnalysisExecution, AnalysisError>> => {
if (operation === "decode_interface_builder")
return err(
new AnalysisCapabilityUnavailableError(
identity.id,
operation,
"No authored UI fixture",
),
);
if (operation === "inspect_native_dispatch_metadata")
return ok(
createAnalysisExecution(
{
target_sha256: sha256,
provider: identity,
analysis_profile_digest: null,
result: inspectNativeDispatchMetadata([]),
},
identity,
{ subject },
),
);
if (operation === "procedure_address")
return ok(createAnalysisExecution("0x1000", identity, { subject }));
if (operation === "procedure_references")
return ok(
createAnalysisExecution(
plan.references ?? mixedReferences(),
identity,
{ subject },
),
);
if (operation === "procedure_callees") {
if (plan.fallbackFailure === "unavailable")
return err(
new AnalysisCapabilityUnavailableError(
identity.id,
operation,
"enumeration unavailable in fixture",
),
);
if (plan.fallbackFailure === "output")
return err(
new AnalysisOutputError(
operation,
"enumeration output failed in fixture",
),
);
if (plan.fallbackFailure === "cancelled")
return err(new AnalysisCancelledError(operation));
return ok(
createAnalysisExecution(
plan.callees ?? ["0x3000"],
plan.fallbackProvider ?? fallback,
{ subject },
),
);
}
throw new Error(
`Unexpected operation ${operation} ${JSON.stringify(parameters)}`,
);
},
};
const server = createServer({ kind: "fixed", analysis });
const client = new Client({ name: "native-ui-partial-routes", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
resources.push({
close: async () => {
await client.close();
await server.close();
},
});
return (async () => {
await server.connect(serverTransport);
await client.connect(clientTransport);
const result = await client.callTool({
name: "trace_native_ui_action",
arguments: { action: "0x1000", max_nodes: maxNodes, max_depth: 1 },
});
return result;
})();
}
const traceFrom = (result: { structuredContent?: unknown }) =>
z
.object({
normalized_result: nativeInvestigationTraceSchema,
})
.parse(result.structuredContent).normalized_result;
it("keeps typed callsite evidence and distinct fallback-provider routes for mixed reference kinds", async () => {
const response = await publicTrace({});
expect(response.isError).not.toBe(true);
const trace = traceFrom(response);
expect(trace.target_sha256).toBe(sha256);
expect(trace.edges).toEqual(
expect.arrayContaining([
expect.objectContaining({
from: "native:function:0x1000",
to: "native:function:0x2000",
relation: "direct_call",
resolution: "observed",
evidence: expect.arrayContaining([
expect.objectContaining({
provider: identity,
location: expect.objectContaining({ address: "0x1004" }),
}),
]),
}),
expect.objectContaining({
from: "native:function:0x1000",
to: "native:function:0x3000",
relation: "provider_call",
resolution: "inferred",
evidence: expect.arrayContaining([
expect.objectContaining({
provider: fallback,
location: expect.objectContaining({ address: "0x1000" }),
}),
]),
}),
]),
);
});
it.each(["unavailable", "output"] as const)(
"retains known observed routes when fallback enumeration has %s failure",
async (failure) => {
const response = await publicTrace({ fallbackFailure: failure });
expect(response.isError).not.toBe(true);
const trace = traceFrom(response);
expect(trace.edges).toContainEqual(
expect.objectContaining({
from: "native:function:0x1000",
to: "native:function:0x2000",
relation: "direct_call",
resolution: "observed",
evidence: expect.arrayContaining([
expect.objectContaining({
provider: identity,
location: expect.objectContaining({ address: "0x1004" }),
}),
]),
}),
);
expect(trace.unresolved).toContainEqual(
expect.objectContaining({
from: "native:function:0x1000",
to: null,
relation: "provider_call",
resolution: "unresolved",
reason:
failure === "unavailable"
? "This analysis is unavailable for the current target. Choose another analysis or target."
: expect.stringContaining("unreadable result"),
limitations: expect.arrayContaining([
"The provider could not complete callee enumeration.",
]),
}),
);
},
);
it("deduplicates fallback overlap without erasing multiple typed callsites", async () => {
const response = await publicTrace({
references: mixedReferences([
ref("0x1004", "0x2000"),
ref("0x100c", "0x2000"),
]),
callees: ["0x2000", "0x3000"],
});
expect(response.isError).not.toBe(true);
const trace = traceFrom(response);
const typedEdges = trace.edges.filter(
(edge) => edge.to === "native:function:0x2000",
);
expect(typedEdges).toHaveLength(2);
expect(
typedEdges
.map((edge) => edge.evidence.flatMap((item) => item.location?.address))
.flat(),
).toEqual(expect.arrayContaining(["0x1004", "0x100c"]));
expect(
trace.edges.filter(
(edge) =>
edge.to === "native:function:0x2000" &&
edge.relation === "provider_call",
),
).toHaveLength(0);
});
it.each([
{
relation: "direct_call",
references: completeReferences([
ref("0x1004", "0x2000"),
ref("0x1008", "0x3000"),
]),
callees: [],
firstRelation: "direct_call",
firstTarget: "native:function:0x2000",
firstResolution: "observed",
firstProvider: identity,
firstSite: "0x1004",
},
{
relation: "indirect_call",
references: completeReferences([
ref("0x1004", "0x2000"),
ref("0x1008", "0x3000", true),
]),
callees: [],
firstRelation: "direct_call",
firstTarget: "native:function:0x2000",
firstResolution: "observed",
firstProvider: identity,
firstSite: "0x1004",
},
{
relation: "provider_call",
references: noClassifiedReferences,
callees: ["0x2000", "0x3000"],
firstRelation: "provider_call",
firstTarget: "native:function:0x2000",
firstResolution: "inferred",
firstProvider: fallback,
firstSite: "0x1000",
},
] as const)(
"preserves $relation when node bounds omit a route",
async ({
relation,
callees,
references,
firstRelation,
firstTarget,
firstResolution,
firstProvider,
firstSite,
}) => {
const response = await publicTrace({ references, callees }, 2);
expect(response.isError).not.toBe(true);
const trace = traceFrom(response);
expect(trace.unresolved).toContainEqual(
expect.objectContaining({
from: "native:function:0x1000",
to: null,
relation,
resolution: "unresolved",
reason: "native_trace_node_bound_reached",
}),
);
expect(trace.edges).toContainEqual(
expect.objectContaining({
from: "native:function:0x1000",
to: firstTarget,
relation: firstRelation,
resolution: firstResolution,
evidence: expect.arrayContaining([
expect.objectContaining({
provider: firstProvider,
location: expect.objectContaining({ address: firstSite }),
}),
]),
}),
);
expect(trace.truncated).toBe(true);
expect(trace.coverage).toContainEqual(
expect.objectContaining({
facet: "static_native_calls",
status: "partial",
}),
);
expect(trace.limitations).toContain(
"Static analysis does not prove that an authored UI action is reachable or executed at runtime.",
);
},
);
it("retains separate omitted callsites when routes share a target", async () => {
const response = await publicTrace(
{
references: completeReferences([
ref("0x1004", "0x2000"),
ref("0x1008", "0x3000", true),
ref("0x100c", "0x3000"),
]),
callees: [],
},
2,
);
expect(response.isError).not.toBe(true);
const trace = traceFrom(response);
expect(trace.unresolved).toEqual(
expect.arrayContaining([
expect.objectContaining({
from: "native:function:0x1000",
to: null,
relation: "indirect_call",
resolution: "unresolved",
evidence: expect.arrayContaining([
expect.objectContaining({
provider: identity,
location: expect.objectContaining({ address: "0x1008" }),
}),
]),
}),
expect.objectContaining({
from: "native:function:0x1000",
to: null,
relation: "direct_call",
resolution: "unresolved",
evidence: expect.arrayContaining([
expect.objectContaining({
provider: identity,
location: expect.objectContaining({ address: "0x100c" }),
}),
]),
}),
]),
);
expect(trace.truncated).toBe(true);
});
it("keeps cancellation terminal when fallback enumeration is cancelled", async () => {
const response = await publicTrace({ fallbackFailure: "cancelled" });
expect(response.isError).toBe(true);
expect(parseMcpToolError(response).error.code).toBe("cancelled");
});