fix(session): describe native UI trace source routes

Share source operations with execution owners and derive availability and missing prerequisites from one evaluation. Distinguish authored UI dispatch from native-seed call tracing without inventing a default request mode. Preserve target gates, typed failures and partial results.

Fixes #5071
This commit is contained in:
morluto committed 2026-10-19 03:39:02 +08:00
1 parent 29910d16b9
commit 32f0e51209
5 files changed
+815 -81

No files matched your search

+11
View File
@@ -216,6 +216,17 @@ sites remain distinct. A static route does not establish runtime reachability.
Swift/closure dispatch requires resolved static references; unavailable ABI
forms remain unknown.
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
a call reader. Metadata can come from direct inspection or `list_names`;
the call reader can supply typed `procedure_references` or inferred
`procedure_callees`. An authored UI-to-handler edge remains useful without
call expansion, and native-seed tracing can proceed without UI decoding.
These routes add no request option or default mode, so discovery omits
`default_mode_available`. Availability describes source prerequisites;
the result reports the query's actual matches, coverage and failures.
`trace_native_values` / `rea trace-native-values <binary> <procedure> --provider
ghidra` composes high-p-code def-use graphs across statically resolved calls.
Logical CALL arguments bind to recovered parameter ordinals; recovered RETURN
+93 -71
View File
@@ -1,6 +1,8 @@
import { DEFAULT_CALL_RELATIONSHIP_DIRECTION } from "../contracts/enhancedInputs.js";
import { CALL_RELATIONSHIP_TRAVERSALS } from "./CallRelationshipTraversal.js";
import { NATIVE_DISPATCH_METADATA_SOURCES } from "./native/NativeDispatchMetadataInspection.js";
import { NATIVE_UI_ACTION_OPERATIONS } from "./native/NativeUiActionTrace.js";
import { NATIVE_CALL_ROUTE_SOURCES } from "./native/NativeCallRoutes.js";
import { z } from "zod";
import { sqliteDatabaseCapability } from "./sqlite/SqliteDatabaseCapability.js";
import type {
@@ -117,10 +119,10 @@ type CompositionRequirements = readonly (
)[];
type AvailabilityMode = {
readonly name: string;
readonly requirements: readonly AnalysisOperation[];
readonly requirements: CompositionRequirements;
};
type ModeComposition = {
readonly defaultMode: string;
readonly defaultMode?: string;
readonly modes: readonly AvailabilityMode[];
};
type ProviderComposition = CompositionRequirements | ModeComposition;
@@ -135,16 +137,32 @@ const CALL_RELATIONSHIP_COMPOSITION: ModeComposition = {
),
};
const NATIVE_UI_ACTION_COMPOSITION: ModeComposition = {
modes: [
{
name: "authored_ui_dispatch",
requirements: [
NATIVE_UI_ACTION_OPERATIONS.interfaceBuilder,
Object.values(NATIVE_DISPATCH_METADATA_SOURCES),
],
},
{
name: "native_seed_call_trace",
requirements: [
Object.values(NATIVE_DISPATCH_METADATA_SOURCES),
NATIVE_UI_ACTION_OPERATIONS.nativeSeed,
Object.values(NATIVE_CALL_ROUTE_SOURCES),
],
},
],
};
// Complete coverage is checked against canonical enhanced contracts, rather than
// allowing a newly registered workflow to silently look like a missing provider.
const ENHANCED_REQUIREMENTS: ReadonlyMap<string, ProviderComposition> = new Map(
Object.entries({
trace_native_values: ["analyze_function", "resolve_native_call_targets"],
trace_native_ui_action: [
"decode_interface_builder",
"list_names",
"procedure_callees",
],
trace_native_ui_action: NATIVE_UI_ACTION_COMPOSITION,
inspect_native_dispatch_metadata: [
Object.values(NATIVE_DISPATCH_METADATA_SOURCES),
],
@@ -739,76 +757,79 @@ const composedAvailability = (
requirements.defaultMode,
descriptors,
)
: composedAvailabilityFor(requirements, descriptors);
: composedAvailabilityFor(requirements, descriptors).availability;
};
/** Evaluate source groups once so discovery and missing-operation facts agree. */
const composedAvailabilityFor = (
requirements: readonly (string | readonly string[])[],
requirements: CompositionRequirements,
descriptors: ReadonlyMap<string, ProviderDescriptor>,
): Availability => {
for (const requirement of requirements) {
if (typeof requirement === "string") {
if (!descriptors.has(requirement))
return {
reason: "provider_missing",
remediation: `Configure a provider for required operation ${requirement}.`,
};
} else {
const declared = requirement.some((operation) =>
descriptors.has(operation),
): {
readonly availability: Availability;
readonly missingOperations: readonly AnalysisOperation[];
} => {
const unmet = requirements
.map((requirement) => {
const operations =
typeof requirement === "string" ? [requirement] : requirement;
const observations = operations.map((operation) =>
descriptors.get(operation),
);
if (!declared)
return {
reason: "provider_missing",
remediation: `Configure a provider for one of: ${requirement.join(", ")}.`,
};
}
}
for (const requirement of requirements) {
if (typeof requirement === "string") {
const descriptor = descriptors.get(requirement);
if (descriptor?.available === false)
return {
reason:
descriptor.availability_code === "unsupported_host"
? "unsupported_host"
: "provider_unavailable",
remediation:
descriptor.reason ??
"Choose another target or configured provider.",
};
} else {
const anyAvailable = requirement.some(
(operation) => descriptors.get(operation)?.available === true,
);
if (!anyAvailable) {
const unavailable = requirement
.map((operation) => descriptors.get(operation))
.find((descriptor) => descriptor?.available === false);
if (unavailable !== undefined)
return {
reason:
unavailable.availability_code === "unsupported_host"
? "unsupported_host"
: "provider_unavailable",
remediation:
unavailable.reason ??
"Choose another target or configured provider.",
};
}
}
}
return { reason: "available", remediation: null };
return {
requirement,
operations,
available: observations.some(
(descriptor) => descriptor?.available === true,
),
unavailable: observations.find(
(descriptor) => descriptor?.available === false,
),
};
})
.filter((group) => !group.available);
const missingOperations = [
...new Set(unmet.flatMap((group) => group.operations)),
];
// Preserve the existing admission rule: report an undeclared prerequisite
// before an unhealthy declaration, in requirement order within each class.
const undeclared = unmet.find((group) => group.unavailable === undefined);
if (undeclared !== undefined)
return {
missingOperations,
availability: {
reason: "provider_missing",
remediation:
typeof undeclared.requirement === "string"
? `Configure a provider for required operation ${undeclared.requirement}.`
: `Configure a provider for one of: ${undeclared.operations.join(", ")}.`,
},
};
const unavailable = unmet[0]?.unavailable;
if (unavailable !== undefined)
return {
missingOperations,
availability: {
reason:
unavailable.availability_code === "unsupported_host"
? "unsupported_host"
: "provider_unavailable",
remediation:
unavailable.reason ?? "Choose another target or configured provider.",
},
};
return {
availability: { reason: "available", remediation: null },
missingOperations,
};
};
const availabilityForModes = (
declarations: readonly AvailabilityMode[],
defaultMode: string,
defaultMode: string | undefined,
descriptors: ReadonlyMap<string, ProviderDescriptor>,
): Availability => {
const modeResults: AvailabilityModeResult[] = declarations.map((mode) => {
const availability = composedAvailabilityFor(
const { availability, missingOperations } = composedAvailabilityFor(
mode.requirements,
descriptors,
);
@@ -824,10 +845,7 @@ const availabilityForModes = (
name: mode.name,
available: false,
reason: availability.reason,
missing_operations: mode.requirements.filter((operation) => {
const descriptor = descriptors.get(operation);
return descriptor === undefined || descriptor.available === false;
}),
missing_operations: [...missingOperations],
remediation: availability.remediation,
};
});
@@ -858,8 +876,12 @@ const availabilityForModes = (
const specificFailureRemediation =
failure?.reason === "provider_missing" ? undefined : failure?.remediation;
const facts = {
defaultModeAvailable:
modes.find((mode) => mode.name === defaultMode)?.available ?? false,
...(defaultMode === undefined
? {}
: {
defaultModeAvailable:
modes.find((mode) => mode.name === defaultMode)?.available ?? false,
}),
modes,
};
if (availableMode !== undefined)
+11 -5
View File
@@ -11,6 +11,12 @@ import {
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
import type { SourceLimitation } from "../SourceLimitations.js";
/** Preferred typed observations and the untyped fallback share one owner. */
export const NATIVE_CALL_ROUTE_SOURCES = {
references: "procedure_references",
callees: "procedure_callees",
} as const;
const limitationsFrom = (
operation: SourceLimitation["operation"],
provider: ProviderIdentity,
@@ -71,7 +77,7 @@ export const readNativeCallRoutes = async (
...(selectorOrigin === undefined ? {} : { selectorOrigin }),
};
const execution = await analysis.execute(
"procedure_references",
NATIVE_CALL_ROUTE_SOURCES.references,
{ procedure, direction: "outgoing" },
options,
);
@@ -83,7 +89,7 @@ export const readNativeCallRoutes = async (
let limitations: SourceLimitation[] = [];
if (execution.ok) {
limitations = limitationsFrom(
"procedure_references",
NATIVE_CALL_ROUTE_SOURCES.references,
execution.value.provider,
execution.value.limitations,
);
@@ -92,7 +98,7 @@ export const readNativeCallRoutes = async (
return {
ok: false as const,
error: new AnalysisOutputError(
"procedure_references",
NATIVE_CALL_ROUTE_SOURCES.references,
"Invalid typed call references",
),
unknowns,
@@ -128,7 +134,7 @@ export const readNativeCallRoutes = async (
limitations,
};
const callees = await analysis.execute(
"procedure_callees",
NATIVE_CALL_ROUTE_SOURCES.callees,
{ procedure },
options,
);
@@ -137,7 +143,7 @@ export const readNativeCallRoutes = async (
limitations = [
...limitations,
...limitationsFrom(
"procedure_callees",
NATIVE_CALL_ROUTE_SOURCES.callees,
callees.value.provider,
callees.value.limitations,
),
+12 -5
View File
@@ -23,6 +23,13 @@ import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
type Input = z.output<typeof enhancedInputSchemas.trace_native_ui_action>;
/** Source roles used by execution and route availability. */
export const NATIVE_UI_ACTION_OPERATIONS = {
interfaceBuilder: "decode_interface_builder",
nativeSeed: "procedure_address",
} as const;
/** Build a UI-to-code trace from the active app and selected native provider. */
export const traceNativeUiAction = async (
analysis: AnalysisOperationPort,
@@ -31,7 +38,7 @@ export const traceNativeUiAction = async (
): EnhancedResult => {
const [uiExecution, metadataExecution] = await Promise.all([
analysis.execute(
"decode_interface_builder",
NATIVE_UI_ACTION_OPERATIONS.interfaceBuilder,
{
max_documents: 64,
max_objects: input.max_nodes,
@@ -65,14 +72,14 @@ export const traceNativeUiAction = async (
if (!decoded.success)
return err(
new AnalysisOutputError(
"decode_interface_builder",
NATIVE_UI_ACTION_OPERATIONS.interfaceBuilder,
"artifact provider returned an invalid interface graph",
),
);
if (decoded.data.target_sha256 !== targetSha256)
return err(
new AnalysisOutputError(
"decode_interface_builder",
NATIVE_UI_ACTION_OPERATIONS.interfaceBuilder,
"UI and native provider results refer to different target SHA-256 values",
),
);
@@ -123,7 +130,7 @@ export const traceNativeUiAction = async (
let selected = resolveUiAction(graph, input.action);
if (selected.nodeId === null && selected.candidates.length === 0) {
const native = await analysis.execute(
"procedure_address",
NATIVE_UI_ACTION_OPERATIONS.nativeSeed,
{ procedure: input.action },
signal === undefined ? {} : { signal },
);
@@ -139,7 +146,7 @@ export const traceNativeUiAction = async (
if (!address.success || native.value.subject?.sha256 !== targetSha256)
return err(
new AnalysisOutputError(
"procedure_address",
NATIVE_UI_ACTION_OPERATIONS.nativeSeed,
"Native seed lacks a validated address or matching target identity",
),
);
@@ -0,0 +1,688 @@
import { rm } from "node:fs/promises";
import type { Client } from "@modelcontextprotocol/client";
import type { CallToolResult } from "@modelcontextprotocol/server";
import { afterEach, describe, expect, it } from "vitest";
import { z } from "zod";
import type {
AnalysisExecution,
AnalysisClient,
AnalysisProvider,
CapabilityDescriptor,
} from "../../../src/application/AnalysisProvider.js";
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
import type { AnalysisError } from "../../../src/domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisOutputError,
} from "../../../src/domain/analysisErrorCore.js";
import {
buildInterfaceBuilderAnalysis,
interfaceBuilderLimitsSchema,
} from "../../../src/domain/apple/interfaceBuilderGraph.js";
import { inspectNativeDispatchMetadata } from "../../../src/domain/native/objcSwiftMetadata.js";
import { nativeInvestigationTraceSchema } from "../../../src/domain/native/nativeInvestigationGraph.js";
import type { JsonValue } from "../../../src/domain/jsonValue.js";
import { err, ok, type Result } from "../../../src/domain/result.js";
import type { AnalysisOperation } from "../../../src/application/AnalysisProvider.js";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { expandToolAvailability } from "../../../src/contracts/toolOutputSchemaPrimitives.js";
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { createSessionMcpHarness } from "./sessionMcpHarness.js";
const resources: Array<{ close(): Promise<unknown> }> = [];
let directory: string | undefined;
afterEach(async () => {
await Promise.all(resources.splice(0).map((resource) => resource.close()));
if (directory !== undefined)
await rm(directory, { recursive: true, force: true });
directory = undefined;
});
const identity = {
id: "fixture",
name: "Fixture analysis provider",
version: "1",
};
const controllerName = "BuildViewController";
const selector = "buildTapped:";
const handlerAddress = "0x1000";
const calleeAddress = "0x2000";
const dispatchMetadata = inspectNativeDispatchMetadata([
{ address: handlerAddress, name: `-[${controllerName} ${selector}]` },
]);
const symbolInventory = [
{ address: handlerAddress, name: `-[${controllerName} ${selector}]` },
];
const interfaceBuilderFor = (targetSha256: string) =>
buildInterfaceBuilderAnalysis({
targetSha256,
toolVersion: "fixture",
documents: [
{
relativePath: "Main.storyboardc/scene.nib/objects.nib",
archiveSha256: "b".repeat(64),
documentKind: "storyboard_scene",
raw: {
"com.apple.ibtool.document.objects": {
controller: { customClass: controllerName },
button: { class: "UIButton", title: "Build" },
},
"com.apple.ibtool.document.connections": {
button: [
{ type: "action", label: selector, destinationId: "controller" },
],
},
},
},
],
limits: interfaceBuilderLimitsSchema.parse({}),
});
type ScenarioName =
| "authored-ui-direct"
| "authored-ui-symbols"
| "native-seed-typed"
| "native-seed-callees"
| "metadata-only"
| "missing-metadata"
| "native-route-unavailable"
| "metadata-runtime-failure"
| "decode-cancelled";
type ProviderOperation = Extract<
AnalysisOperation,
| "decode_interface_builder"
| "list_names"
| "inspect_native_dispatch_metadata"
| "procedure_address"
| "procedure_references"
| "procedure_callees"
>;
type Scenario = {
readonly name: ScenarioName;
readonly capabilities: readonly {
readonly operation: ProviderOperation;
readonly available: true;
}[];
readonly unavailable?: readonly {
readonly operation: ProviderOperation;
readonly reason: string;
}[];
readonly failure?: "metadata-runtime" | "decode-cancelled";
};
type RouteScenario = Scenario & {
readonly route: "authored_ui_dispatch" | "native_seed_call_trace";
};
const authoredUiDirectScenario: RouteScenario = {
name: "authored-ui-direct",
route: "authored_ui_dispatch",
capabilities: [
{ operation: "decode_interface_builder", available: true },
{ operation: "inspect_native_dispatch_metadata", available: true },
],
};
const authoredUiSymbolsScenario: RouteScenario = {
name: "authored-ui-symbols",
route: "authored_ui_dispatch",
capabilities: [
{ operation: "decode_interface_builder", available: true },
{ operation: "list_names", available: true },
],
};
const nativeSeedTypedScenario: RouteScenario = {
name: "native-seed-typed",
route: "native_seed_call_trace",
capabilities: [
{ operation: "inspect_native_dispatch_metadata", available: true },
{ operation: "procedure_address", available: true },
{ operation: "procedure_references", available: true },
],
};
const nativeSeedCalleesScenario: RouteScenario = {
name: "native-seed-callees",
route: "native_seed_call_trace",
capabilities: [
{ operation: "inspect_native_dispatch_metadata", available: true },
{ operation: "procedure_address", available: true },
{ operation: "procedure_callees", available: true },
],
};
const routeScenarios = [
authoredUiDirectScenario,
authoredUiSymbolsScenario,
nativeSeedTypedScenario,
nativeSeedCalleesScenario,
] as const;
const metadataOnlyScenario: Scenario = {
name: "metadata-only",
capabilities: [
{ operation: "inspect_native_dispatch_metadata", available: true },
],
};
const missingMetadataScenario: Scenario = {
name: "missing-metadata",
capabilities: [{ operation: "decode_interface_builder", available: true }],
};
const unavailableNativeRouteScenario: Scenario = {
name: "native-route-unavailable",
capabilities: [
{ operation: "inspect_native_dispatch_metadata", available: true },
{ operation: "procedure_address", available: true },
],
unavailable: [
{
operation: "procedure_references",
reason: "Typed references are unavailable in this fixture.",
},
],
};
const metadataRuntimeFailureScenario: Scenario = {
name: "metadata-runtime-failure",
capabilities: [
{ operation: "decode_interface_builder", available: true },
{ operation: "inspect_native_dispatch_metadata", available: true },
{ operation: "list_names", available: true },
],
failure: "metadata-runtime",
};
const decodeCancelledScenario: Scenario = {
name: "decode-cancelled",
capabilities: [
{ operation: "decode_interface_builder", available: true },
{ operation: "inspect_native_dispatch_metadata", available: true },
{ operation: "list_names", available: true },
],
failure: "decode-cancelled",
};
describe("native UI action availability through available routes", () => {
it.each(routeScenarios)(
"reports the $name route available when its observed path succeeds",
async (scenario) => {
directory = await createTestTempDirectory("rea-native-ui-availability-");
const calls: ProviderOperation[] = [];
const { mcp, first } = await createSessionMcpHarness(
directory,
() => provider(scenario, calls),
resources,
);
const opened = await mcp.callTool({
name: "open_binary",
arguments: { path: first },
});
expect(opened.isError).not.toBe(true);
try {
const inventory = await readInventory(mcp);
const result = await mcp.callTool({
name: "trace_native_ui_action",
arguments: {
action:
scenario.route === "authored_ui_dispatch"
? selector
: `-[${controllerName} ${selector}]`,
},
});
assertSuccessfulRoute(scenario, result, calls);
expect(dispatchAvailability(inventory)).toMatchObject({
available: true,
reason: "available",
});
const entry = dispatchAvailability(inventory);
expect(entry?.modes).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: scenario.route,
available: true,
}),
]),
);
expect(entry).not.toHaveProperty("default_mode_available");
} finally {
const closed = await mcp.callTool({
name: "close_binary",
arguments: {},
});
expect(closed.isError).not.toBe(true);
}
},
);
});
describe("native UI action availability for unmet routes", () => {
it.each([
metadataOnlyScenario,
missingMetadataScenario,
unavailableNativeRouteScenario,
])(
"reports unmet route sources for $name without hiding partial results",
async (scenario) => {
directory = await createTestTempDirectory("rea-native-ui-no-metadata-");
const calls: ProviderOperation[] = [];
const { mcp, first } = await createSessionMcpHarness(
directory,
() => provider(scenario, calls),
resources,
);
const opened = await mcp.callTool({
name: "open_binary",
arguments: { path: first },
});
expect(opened.isError).not.toBe(true);
try {
const inventory = await readInventory(mcp);
const result = await mcp.callTool({
name: "trace_native_ui_action",
arguments: {
action:
scenario.name === "metadata-only"
? "missingAction:"
: scenario.name === "native-route-unavailable"
? `-[${controllerName} ${selector}]`
: selector,
},
});
if (scenario.name === "metadata-only") {
expect(result.isError).not.toBe(true);
const trace = z
.object({ normalized_result: nativeInvestigationTraceSchema })
.parse(result.structuredContent).normalized_result;
expect(trace.edges).toEqual([]);
expect(trace.nodes).toEqual([]);
expect(trace.reason).toBe("ui_action_or_object_not_found");
expect(trace.coverage).toContainEqual(
expect.objectContaining({
facet: "interface_builder_archives",
status: "unsupported",
}),
);
expect(calls).toEqual(["inspect_native_dispatch_metadata"]);
} else if (scenario.name === "missing-metadata") {
expect(result.isError).toBe(true);
expect(parseMcpToolError(result)).toMatchObject({
error: { code: "capability_unavailable" },
});
expect(calls).toContain("decode_interface_builder");
expect(calls).not.toContain("inspect_native_dispatch_metadata");
} else {
expect(result.isError).not.toBe(true);
expect(calls).toContain("procedure_address");
expect(calls).not.toContain("procedure_references");
}
const entry = dispatchAvailability(inventory);
expect(entry).toMatchObject({
available: false,
reason:
scenario.name === "native-route-unavailable"
? "unsupported_host"
: "provider_missing",
});
expect(entry?.modes).toEqual(
expect.arrayContaining([
expect.objectContaining({
name: "authored_ui_dispatch",
available: false,
}),
expect.objectContaining({
name: "native_seed_call_trace",
available: false,
}),
]),
);
if (scenario.name === "metadata-only") {
const authored = entry?.modes?.find(
(mode) => mode.name === "authored_ui_dispatch",
);
expect(authored?.missing_operations).toEqual([
"decode_interface_builder",
]);
}
if (scenario.name === "missing-metadata") {
const authored = entry?.modes?.find(
(mode) => mode.name === "authored_ui_dispatch",
);
expect(authored?.missing_operations).toEqual([
"inspect_native_dispatch_metadata",
"list_names",
]);
}
if (scenario.name === "native-route-unavailable") {
const native = entry?.modes?.find(
(mode) => mode.name === "native_seed_call_trace",
);
expect(native?.missing_operations).toEqual([
"procedure_references",
"procedure_callees",
]);
}
} finally {
const closed = await mcp.callTool({
name: "close_binary",
arguments: {},
});
expect(closed.isError).not.toBe(true);
}
},
);
});
describe("native UI action availability for failures and target gates", () => {
it.each([metadataRuntimeFailureScenario, decodeCancelledScenario])(
"preserves runtime failures for $name without trying another declared source",
async (scenario) => {
directory = await createTestTempDirectory(
"rea-native-ui-source-failure-",
);
const calls: ProviderOperation[] = [];
const { mcp, first } = await createSessionMcpHarness(
directory,
() => provider(scenario, calls),
resources,
);
expect(
(
await mcp.callTool({
name: "open_binary",
arguments: { path: first },
})
).isError,
).not.toBe(true);
try {
const inventory = await readInventory(mcp);
const result = await mcp.callTool({
name: "trace_native_ui_action",
arguments: { action: selector },
});
expect(result.isError).toBe(true);
expect(parseMcpToolError(result).error.code).toBe(
scenario.name === "decode-cancelled"
? "cancelled"
: "unreadable_output",
);
if (scenario.name === "metadata-runtime-failure") {
expect(calls).toContain("inspect_native_dispatch_metadata");
expect(calls).not.toContain("list_names");
} else {
expect(calls).toContain("decode_interface_builder");
expect(calls).not.toContain("list_names");
}
expect(dispatchAvailability(inventory)).toMatchObject({
available: true,
});
} finally {
expect(
(await mcp.callTool({ name: "close_binary", arguments: {} })).isError,
).not.toBe(true);
}
},
);
it("reports target_required before querying provider operations", async () => {
directory = await createTestTempDirectory("rea-native-ui-no-target-");
const calls: ProviderOperation[] = [];
const { mcp } = await createSessionMcpHarness(
directory,
() => provider(authoredUiDirectScenario, calls),
resources,
);
const inventory = await readInventory(mcp);
expect(inventory.result.open).toBe(false);
expect(dispatchAvailability(inventory)).toMatchObject({
available: false,
reason: "target_required",
});
expect(calls).toEqual([]);
});
});
const readInventory = async (mcp: Client) => {
const result = await mcp.callTool({ name: "binary_session", arguments: {} });
return toolContract("binary_session").outputSchema.parse(
result.structuredContent,
);
};
const dispatchAvailability = (
inventory: Awaited<ReturnType<typeof readInventory>>,
) =>
expandToolAvailability(inventory.result.tool_availability).find(
(entry) => entry.name === "trace_native_ui_action",
);
const assertSuccessfulRoute = (
scenario: RouteScenario,
result: CallToolResult,
calls: readonly ProviderOperation[],
): void => {
expect(result.isError).not.toBe(true);
const trace = z
.object({ normalized_result: nativeInvestigationTraceSchema })
.parse(result.structuredContent).normalized_result;
expect(trace.target_sha256).toMatch(/^[a-f0-9]{64}$/u);
if (scenario.route === "authored_ui_dispatch") {
expect(trace.edges).toContainEqual(
expect.objectContaining({
relation: "objc_dispatch",
to: `native:function:${handlerAddress}`,
}),
);
expect(calls).toContain("decode_interface_builder");
expect(calls).toContain(
scenario.name === "authored-ui-direct"
? "inspect_native_dispatch_metadata"
: "list_names",
);
expect(calls).not.toContain(
scenario.name === "authored-ui-direct"
? "list_names"
: "inspect_native_dispatch_metadata",
);
expect(calls).not.toContain("procedure_address");
return;
}
expect(trace.start).toBe(`native:function:${handlerAddress}`);
expect(trace.edges).toContainEqual(
expect.objectContaining({
relation:
scenario.name === "native-seed-typed" ? "direct_call" : "provider_call",
from: `native:function:${handlerAddress}`,
to: `native:function:${calleeAddress}`,
}),
);
expect(calls).not.toContain("decode_interface_builder");
expect(calls).toContain("inspect_native_dispatch_metadata");
expect(calls).toContain("procedure_address");
expect(calls).toContain(
scenario.name === "native-seed-typed"
? "procedure_references"
: "procedure_callees",
);
};
const capabilityEffects = {
mutatesArtifact: false,
launchesProcess: false,
mayShowUi: false,
mayAccessNetwork: false,
mayWriteFilesystem: false,
changesPermissions: false,
requiresRoot: false,
};
const providerOperations = new Set<AnalysisOperation>([
"decode_interface_builder",
"list_names",
"inspect_native_dispatch_metadata",
"procedure_address",
"procedure_references",
"procedure_callees",
]);
const isProviderOperation = (
operation: AnalysisOperation,
): operation is ProviderOperation => providerOperations.has(operation);
type ProviderFixtureContext = {
readonly scenario: Scenario;
readonly calls: ProviderOperation[];
readonly target: Parameters<AnalysisProvider["createClient"]>[0];
readonly profile: Parameters<AnalysisProvider["createClient"]>[1];
};
const provider = (
scenario: Scenario,
calls: ProviderOperation[],
): AnalysisProvider => {
const capabilities: CapabilityDescriptor[] = [
...scenario.capabilities.map((capability) => {
return {
provider: identity,
operation: capability.operation,
effects: capabilityEffects,
limitations: [],
available: true as const,
reason: null,
};
}),
...(scenario.unavailable ?? []).map((capability) => {
return {
provider: identity,
operation: capability.operation,
effects: capabilityEffects,
limitations: [],
available: false as const,
availabilityCode: "unsupported_host" as const,
reason: capability.reason,
};
}),
];
return {
identity: () => identity,
capabilities: () => capabilities,
createClient: (target, profile): AnalysisClient => ({
execute: (operation, parameters) =>
executeProviderFixture(
{ scenario, calls, target, profile },
operation,
parameters,
),
close: async () => ok(null),
}),
};
};
const executeProviderFixture = async (
context: ProviderFixtureContext,
operation: AnalysisOperation,
parameters: Readonly<Record<string, JsonValue>>,
): Promise<Result<AnalysisExecution, AnalysisError>> => {
if (operation === "health")
return ok(createAnalysisExecution(null, identity));
if (!isProviderOperation(operation))
throw new Error(`Unexpected provider operation: ${operation}`);
context.calls.push(operation);
const failure = failureFor(context.scenario, operation);
if (failure !== undefined) return err(failure);
return ok(
createAnalysisExecution(
resultFor(operation, parameters, context.target.sha256, context.profile),
identity,
{
subject: context.target,
...(context.profile === undefined
? {}
: { analysisProfile: context.profile }),
},
),
);
};
const failureFor = (
scenario: Scenario,
operation: ProviderOperation,
): AnalysisError | undefined => {
if (
operation === "inspect_native_dispatch_metadata" &&
scenario.failure === "metadata-runtime"
)
return new AnalysisOutputError(
"inspect_native_dispatch_metadata",
"fixture failure",
);
if (
operation === "decode_interface_builder" &&
scenario.failure === "decode-cancelled"
)
return new AnalysisCancelledError("decode_interface_builder");
return undefined;
};
const resultFor = (
operation: ProviderOperation,
parameters: Readonly<Record<string, JsonValue>>,
targetSha256: string,
profile: Parameters<AnalysisProvider["createClient"]>[1],
): unknown => {
switch (operation) {
case "decode_interface_builder":
return interfaceBuilderFor(targetSha256);
case "inspect_native_dispatch_metadata":
return {
target_sha256: targetSha256,
provider: identity,
analysis_profile_digest: profile?.digest ?? null,
result: dispatchMetadata,
};
case "procedure_address":
return handlerAddress;
case "list_names":
return symbolInventory;
case "procedure_callees":
return parameters.procedure === handlerAddress ? [calleeAddress] : [];
case "procedure_references":
return typedCallReferences(
typeof parameters.procedure === "string" ? parameters.procedure : "",
);
}
};
const typedCallReferences = (procedure: string) => ({
reference_kinds_available: true,
references:
procedure === handlerAddress
? [
{
source_address: "0x1008",
target_address: calleeAddress,
target_procedure: { address: calleeAddress },
kind: {
available: true,
provenance: "fixture typed reference",
type: "call",
flow: true,
call: true,
jump: false,
data: false,
read: false,
write: false,
indirect: false,
computed: false,
conditional: false,
terminal: false,
primary: true,
operand_index: 0,
external: false,
},
},
]
: [],
unresolved_calls: [],
});