diff --git a/src/application/native/EnhancedNativeInvestigation.test.ts b/src/application/native/EnhancedNativeInvestigation.test.ts index 093e8d4825..2d86caedc9 100644 --- a/src/application/native/EnhancedNativeInvestigation.test.ts +++ b/src/application/native/EnhancedNativeInvestigation.test.ts @@ -308,6 +308,9 @@ describe("native UI action trace", () => { }), ); }); +}); + +describe("native UI action seed resolution", () => { it("accepts an explicit native seed without an authored UI connection", async () => { const result = await new EnhancedTools( testAnalysis({ @@ -324,6 +327,31 @@ describe("native UI action trace", () => { }, }); }); + it.each(["", null, 0, { address: "0x1000" }])( + "rejects a malformed provider canonical seed address: %j", + async (address) => { + let traversed = false; + const result = await new EnhancedTools( + testAnalysis({ + procedure_address: () => execution("procedure_address", address), + procedure_callees: () => { + traversed = true; + return execution("procedure_callees", []); + }, + }), + ).execute("trace_native_ui_action", { action: "missing-selector" }); + + expect(result).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisOutputError", + operation: "procedure_address", + reason: "provider returned no canonical procedure address", + }, + }); + expect(traversed).toBe(false); + }, + ); it("builds the evidence path from provider names with declared symbol facts", async () => { const tools = new EnhancedTools( testAnalysis({ diff --git a/src/application/native/NativeUiActionTrace.ts b/src/application/native/NativeUiActionTrace.ts index 350c2cd199..50e3d8e9f0 100644 --- a/src/application/native/NativeUiActionTrace.ts +++ b/src/application/native/NativeUiActionTrace.ts @@ -14,6 +14,7 @@ import { } from "../../domain/native/nativeInvestigationGraph.js"; import { nativeDispatchMetadataResultSchema } from "../../domain/native/objcSwiftMetadata.js"; import { inspectNativeDispatch } from "./NativeDispatchMetadataInspection.js"; +import { parseProcedureAddress } from "../ProcedureAddressResolution.js"; import { jsonValueSchema } from "../../domain/jsonValue.js"; import { readNativeCallRoutes } from "./NativeCallRoutes.js"; import { @@ -163,26 +164,27 @@ export const traceNativeUiAction = async ( if (!native.ok && !permitsCapabilityFallback(native.error)) return err(native.error); if (native.ok) { - const address = z.string().safeParse(native.value.result); - if (!address.success || native.value.subject?.sha256 !== targetSha256) + const address = parseProcedureAddress(native.value.result); + if (!address.ok) return address; + if (native.value.subject?.sha256 !== targetSha256) return err( new AnalysisOutputError( NATIVE_UI_ACTION_OPERATIONS.nativeSeed, - "Native seed lacks a validated address or matching target identity", + "Native seed lacks matching target identity", ), ); - const id = `native:function:${address.data}`; + const id = `native:function:${address.value}`; graph.nodes.push({ id, kind: "function", name: input.action, - location: { address: address.data, file_offset: null }, + location: { address: address.value, file_offset: null }, attributes: { resolved_by: native.value.provider.id }, evidence: [ { kind: "reference", description: `Provider resolved the explicit native seed ${input.action}`, - location: { address: address.data, file_offset: null }, + location: { address: address.value, file_offset: null }, artifact_path: null, artifact_sha256: targetSha256, },