import { expect, it } from "vitest"; import { createHash } from "node:crypto"; import { matchNativeFunctions } from "./NativeFunctionMatchingService.js"; import { nativeFunctionMatchingSchema } from "../../domain/native/functionMatching.js"; import { createEvidence } from "../../domain/evidence.js"; import { createAnalysisProfile } from "../../domain/analysisProfile.js"; import { functionDossierSchema } from "../../domain/hopperValues.js"; import { procedureInfoSchema } from "../../domain/native/procedureInfo.js"; import { nativeMatchingInventory, nativeMatchingDetails, nativeMatchingTarget, nativeMatchingProvider, } from "../../../tests/fixtures/nativeFunctionMatching.js"; const compare = async (input: unknown) => { const result = await matchNativeFunctions(input); if (!result.ok) throw result.error; return { ...result.value, report: nativeFunctionMatchingSchema.parse( result.value.evidence.normalized_result, ), }; }; it("retains transitive provenance without treating dependencies as native observations and fails closed for a missing ancestor", async () => { const ancestor = createEvidence(undefined, nativeMatchingProvider, { operation: "fixture_provenance", parameters: {}, result: { provenance: "Synthetic ancestor" }, }); const dependency = createEvidence(undefined, nativeMatchingProvider, { operation: "fixture_dependency", parameters: {}, result: { provenance: "Synthetic dependency" }, evidenceLinks: [ancestor.evidence_id], }); const left = nativeMatchingInventory( "1", [{ address: "0x10", value: "main" }], [dependency.evidence_id], ); const right = nativeMatchingInventory( "2", [{ address: "0x20", value: "main" }], [dependency.evidence_id], ); const dependencies = new Map( [ancestor, dependency].map((record) => [record.evidence_id, record]), ); const result = await matchNativeFunctions({ left, right }, undefined, (id) => dependencies.get(id), ); if (!result.ok) throw result.error; expect( new Set(result.value.sources.map((record) => record.evidence_id)), ).toEqual( new Set( [left, right, dependency, ancestor].map((record) => record.evidence_id), ), ); expect(new Set(result.value.evidence.evidence_links)).toEqual( new Set([left.evidence_id, right.evidence_id]), ); expect( nativeFunctionMatchingSchema.parse(result.value.evidence.normalized_result) .summary.matched, ).toBe(1); const missing = await matchNativeFunctions( { left, right }, undefined, (id) => (id === dependency.evidence_id ? dependency : undefined), ); expect(missing.ok).toBe(false); if (missing.ok) throw new Error("Missing ancestor accepted"); expect(missing.error).toMatchObject({ evidenceId: ancestor.evidence_id, reason: "missing", }); }); it("keeps symbol origin unknown when supplied metadata describes a different same-address alias", async () => { const left = nativeMatchingInventory("1", [ { address: "0x10", value: "reported_name" }, ]); const right = nativeMatchingInventory("2", [ { address: "0x20", value: "reported_name" }, ]); const alias = createEvidence( nativeMatchingTarget("1"), nativeMatchingProvider, { operation: "list_names", parameters: {}, result: [ { address: "0x10", value: "other_alias", symbol: { primary: false, dynamic: false, external: false, type: "function", source: "user_defined", }, }, ], }, ); const { report } = await compare({ left, right, left_observations: [alias] }); expect(report.summary.matched).toBe(1); expect(report.matches[0]?.left[0]?.limitations).toContain( "Symbol origin unavailable; exact-name matching uses the reported name and the existing automatic-name exclusion rule.", ); expect(report.matches[0]?.left[0]?.evidence_links).toContain( alias.evidence_id, ); }); it("preserves contradictory symbol origins from inventory/name facts or multiple name observations, independently of byte correspondence", async () => { const observe = (operation: string, generated: boolean) => createEvidence(nativeMatchingTarget("1"), nativeMatchingProvider, { operation, parameters: {}, result: [ { address: "0x10", value: "same_name", symbol: { primary: true, dynamic: generated, external: false, type: "function", source: generated ? "default" : "user_defined", }, }, ], }); const right = nativeMatchingInventory("2", [ { address: "0x20", value: "same_name" }, ]); for (const [left, names] of [ [observe("list_procedures", false), [observe("list_names", true)]], [observe("list_procedures", true), [observe("list_names", false)]], [ nativeMatchingInventory("1", [{ address: "0x10", value: "same_name" }]), [observe("list_names", false), observe("list_names", true)], ], [ nativeMatchingInventory("1", [{ address: "0x10", value: "same_name" }]), [observe("list_names", true), observe("list_names", false)], ], ] as const) { for (const withBytes of [false, true]) { const { report } = await compare({ left, right, left_observations: [ ...names, ...(withBytes ? nativeMatchingDetails("1", "0x10", "c3") : []), ], right_observations: withBytes ? nativeMatchingDetails("2", "0x20", "c3") : [], }); expect(report.summary.matched).toBe(withBytes ? 1 : 0); expect(report.matches[0]!.basis).toBe( withBytes ? "identical-bytes" : null, ); const member = report.matches[0]!.left[0]!; expect(member.limitations).toContain( "Conflicting generated and named symbol-origin observations; exact-name matching is unavailable. Independent complete body bytes remain eligible.", ); expect(member.limitations.join(" ")).not.toContain( "Symbol origin unavailable", ); for (const source of names) expect(member.evidence_links).toContain(source.evidence_id); } } }); it("keeps informative CFG/call/string fingerprints as unresolved candidates and refuses incomplete structural observations", async () => { const profile = createAnalysisProfile(nativeMatchingProvider, { fixture: "structural-candidates-v1", }); const observe = ( side: "1" | "2", operation: string, result: Parameters[2]["result"], complete = true, ) => createEvidence(nativeMatchingTarget(side), nativeMatchingProvider, { operation, parameters: {}, result, analysisProfile: profile, relationshipCoverage: complete ? { status: "complete" } : { status: "unknown", reason: "Missing direct call coverage" }, }); const left = observe("1", "list_procedures", [ { address: "0x10", value: "old_behavior" }, ]); const right = observe("2", "list_procedures", [ { address: "0x20", value: "new_behavior" }, ]); const dossier = (side: "1" | "2", address: string) => { const value = functionDossierSchema.parse( nativeMatchingDetails(side, address, "0102", true)[0]!.normalized_result, ); value.basic_blocks = [ { start: address, end: `0x${(BigInt(address) + 2n).toString(16)}`, successors: [], }, ]; value.callees = [ { address: "0x500", name: "puts", classification: null, body: { available: false, reason: "Synthetic external target" }, }, ]; value.unresolved_calls = []; value.referenced_strings = [ { address: "0x900", value: "marker", source_address: address }, ]; return value; }; const l = dossier("1", "0x10"), r = dossier("2", "0x20"); const { report } = await compare({ left, right, left_observations: [observe("1", "analyze_function", l)], right_observations: [observe("2", "analyze_function", r)], }); expect(report.summary).toEqual({ matched: 0, only_left: 0, only_right: 0, unresolved: 1, }); expect(report.matches[0]?.left[0]?.structural_candidate_sha256).toMatch( /^[a-f0-9]{64}$/u, ); expect(report.matches[0]?.left[0]?.structural_candidate_sha256).toBe( report.matches[0]?.right[0]?.structural_candidate_sha256, ); expect(report.matches[0]?.compare_functions_input).toBeNull(); const incomplete = await compare({ left, right, left_observations: [observe("1", "analyze_function", l, false)], right_observations: [observe("2", "analyze_function", r)], }); expect(incomplete.report.summary.unresolved).toBe(0); expect(incomplete.report.source_coverage.left_structural_candidates).toBe(0); const legacy = { ...l }; delete legacy.facet_availability; const missingFacets = await compare({ left, right, left_observations: [observe("1", "analyze_function", legacy)], right_observations: [observe("2", "analyze_function", r)], }); expect(missingFacets.report.source_coverage.left_structural_candidates).toBe( 0, ); expect(missingFacets.report.summary.unresolved).toBe(0); l.callees = []; l.referenced_strings = []; r.callees = []; r.referenced_strings = []; const uninformative = await compare({ left, right, left_observations: [observe("1", "analyze_function", l)], right_observations: [observe("2", "analyze_function", r)], }); expect(uninformative.report.source_coverage.left_structural_candidates).toBe( 0, ); expect(uninformative.report.summary.unresolved).toBe(0); }); it("matches symbols before bytes, retains exact renamed bytes, changed pairs, unpaired observations and duplicate candidates", async () => { const left = nativeMatchingInventory("1", [ { address: "0x1000", value: "stable" }, { address: "0x1010", value: "changed" }, { address: "0x1020", value: "old_name" }, { address: "0x1030", value: "removed_observation" }, { address: "0x1040", value: "duplicate" }, { address: "0x1050", value: "duplicate" }, ]); const right = nativeMatchingInventory("2", [ { address: "0x2000", value: "stable" }, { address: "0x2010", value: "changed" }, { address: "0x2020", value: "renamed" }, { address: "0x2030", value: "added_observation" }, { address: "0x2040", value: "duplicate" }, ]); const result = await compare({ left, right, left_observations: [ ...nativeMatchingDetails("1", "0x1010", "0102"), ...nativeMatchingDetails("1", "0x1020", "030405", true), ], right_observations: [ ...nativeMatchingDetails("2", "0x2010", "aabb"), ...nativeMatchingDetails("2", "0x2020", "030405", true), ], }); expect(result.report.summary).toEqual({ matched: 3, only_left: 1, only_right: 1, unresolved: 1, }); const renamed = result.report.matches.find( (row) => row.basis === "identical-bytes", )!; expect(renamed.left[0]).toMatchObject({ name: "old_name", body_bytes: 3, body_sha256: createHash("sha256") .update(Buffer.from("030405", "hex")) .digest("hex"), }); expect(renamed.right[0]?.name).toBe("renamed"); expect(renamed.compare_functions_input?.left.evidence_id).toBe( renamed.left[0]?.dossier_evidence_id, ); expect( result.report.matches.find((row) => row.left[0]?.name === "changed"), ).toMatchObject({ state: "matched", basis: "exact-symbol" }); expect( result.report.matches.find((row) => row.state === "unresolved"), ).toMatchObject({ left: [expect.anything(), expect.anything()], right: [expect.anything()], }); expect(new Set(result.evidence.evidence_links)).toEqual( new Set(result.sources.map((x) => x.evidence_id)), ); expect(result.report.limitations.join(" ")).toContain("never prove absence"); }); it("does not force repeated identical-byte candidates after an exact name consumes an alias", async () => { const left = nativeMatchingInventory("1", [ { address: "0x10", value: "known" }, { address: "0x20", value: "FUN_20" }, ]); const right = nativeMatchingInventory("2", [ { address: "0x30", value: "known" }, { address: "0x40", value: "FUN_40" }, ]); const { report } = await compare({ left, right, left_observations: [ ...nativeMatchingDetails("1", "0x10", "c3"), ...nativeMatchingDetails("1", "0x20", "c3"), ], right_observations: [ ...nativeMatchingDetails("2", "0x30", "c3"), ...nativeMatchingDetails("2", "0x40", "c3"), ], }); expect(report.summary).toEqual({ matched: 1, unresolved: 1, only_left: 0, only_right: 0, }); expect( report.matches.find((x) => x.state === "unresolved")?.left[0]?.name, ).toBe("FUN_20"); }); it("reconstructs discontiguous bodies from adjacent byte reads without next-procedure guesses and keeps address-space/64-bit identity", async () => { const address = "ram:0xfffffffffffffff0"; const left = nativeMatchingInventory("1", [{ address, value: "FUN_f0" }]); const right = nativeMatchingInventory("2", [ { address: "0x20", value: "FUN_20" }, ]); const body = { available: true, provenance: "synthetic noncontiguous body", ranges: [ { start: address, end: "ram:0xfffffffffffffff1" }, { start: "ram:0xfffffffffffffff8", end: "ram:0xfffffffffffffff8" }, ], total_bytes: 3, span_bytes: 9, non_contiguous: true, contains_entry: true, }; const info = createEvidence( nativeMatchingTarget("1"), nativeMatchingProvider, { operation: "procedure_info", parameters: {}, result: { name: "a", entrypoint: address, basicblock_count: 1, length: 3, signature: null, locals: [], classification: null, body, }, }, ); const read = (address: string, hex: string) => createEvidence(nativeMatchingTarget("1"), nativeMatchingProvider, { operation: "read_bytes", parameters: {}, result: { address, requested_bytes: hex.length / 2, returned_bytes: hex.length / 2, bytes_hex: hex, complete: true, }, }); const rightDetails = nativeMatchingDetails("2", "0x20", "010203"); const { report } = await compare({ left, right, left_observations: [ info, read(address, "01"), read("ram:0xfffffffffffffff1", "02"), read("ram:0xfffffffffffffff8", "03"), ], right_observations: rightDetails, }); // Same concatenated bytes with a different body layout do not become a pair. expect(report.summary.matched).toBe(0); expect(report.matches[0]?.left[0]).toMatchObject({ address, body_bytes: 3, body_sha256: createHash("sha256") .update(Buffer.from("010203", "hex")) .digest("hex"), }); }); it("preserves byte correspondence when consistent dossiers differ only in request identity, without choosing an arbitrary dossier", async () => { const left = nativeMatchingInventory("1", [ { address: "0x10", value: "old_name" }, ]); const right = nativeMatchingInventory("2", [ { address: "0x20", value: "new_name" }, ]); const leftDetails = nativeMatchingDetails("1", "0x10", "0102", true); const another = createEvidence( nativeMatchingTarget("1"), nativeMatchingProvider, { operation: "analyze_function", parameters: { procedure: "old_name" }, result: leftDetails[0]!.normalized_result, relationshipCoverage: { status: "complete" }, }, ); for (const observations of [ [...leftDetails, another], [another, ...leftDetails], ]) { const { report } = await compare({ left, right, left_observations: observations, right_observations: nativeMatchingDetails("2", "0x20", "0102", true), }); expect(report.summary.matched).toBe(1); expect(report.matches[0]).toMatchObject({ basis: "identical-bytes", compare_functions_input: null, left: [ { dossier_evidence_id: null, body_bytes: 2, body_sha256: createHash("sha256") .update(Buffer.from("0102", "hex")) .digest("hex"), }, ], }); expect(report.matches[0]!.left[0]!.evidence_links).toContain( another.evidence_id, ); } const conflicting = nativeMatchingDetails("1", "0x10", "010203", true)[0]!; const { report } = await compare({ left, right, left_observations: [...leftDetails, conflicting], right_observations: nativeMatchingDetails("2", "0x20", "0102", true), }); expect(report.summary.matched).toBe(0); expect(report.matches[0]!.left[0]!.body_sha256).toBeNull(); }); it("rejects repeated per-occurrence source-link expansion within the shared representation budget", async () => { const base = nativeMatchingDetails("1", "0x10", "c3")[0]!; const observations = Array.from({ length: 250 }, (_, observation) => createEvidence(nativeMatchingTarget("1"), nativeMatchingProvider, { operation: "procedure_info", parameters: { procedure: "0x10", observation }, result: base.normalized_result, }), ); const result = await matchNativeFunctions({ left: nativeMatchingInventory( "1", Array.from({ length: 1000 }, (_, i) => ({ address: "0x10", value: `alias_${i}`, })), ), right: nativeMatchingInventory("2", []), left_observations: observations, }); expect(result.ok).toBe(false); if (!result.ok) expect(result.error._tag).toBe("AnalysisResourceConstraintError"); }); it("retains missing bytes and conflicting overlapping observations as unknown instead of exact matches", async () => { const left = nativeMatchingInventory("1", [ { address: "0x10", value: "FUN_10" }, ]); const right = nativeMatchingInventory("2", [ { address: "0x20", value: "FUN_20" }, ]); const observations = [ ...nativeMatchingDetails("1", "0x10", "0102"), nativeMatchingDetails("1", "0x10", "ffff")[1]!, ]; const { report } = await compare({ left, right, left_observations: observations, right_observations: nativeMatchingDetails("2", "0x20", "0102"), }); expect(report.summary.matched).toBe(0); expect(report.matches[0]?.left[0]?.body_sha256).toBeNull(); }); it("composes consistent overlapping and partial byte reads without losing complete body coverage", async () => { const left = nativeMatchingInventory("1", [ { address: "0x10", value: "FUN_10" }, ]); const right = nativeMatchingInventory("2", [ { address: "0x20", value: "FUN_20" }, ]); const info = nativeMatchingDetails("1", "0x10", "01020304")[0]!; const read = (address: string, hex: string, requested = hex.length / 2) => createEvidence(nativeMatchingTarget("1"), nativeMatchingProvider, { operation: "read_bytes", parameters: { address, length: requested }, result: { address, requested_bytes: requested, returned_bytes: hex.length / 2, bytes_hex: hex, complete: requested === hex.length / 2, }, }); const { report } = await compare({ left, right, left_observations: [ info, read("0x10", "010203", 6), read("0x11", "020304"), ], right_observations: nativeMatchingDetails("2", "0x20", "01020304"), }); expect(report.summary.matched).toBe(1); expect(report.source_coverage).toMatchObject({ status: "complete-within-inputs", left_byte_fingerprints: 1, right_byte_fingerprints: 1, left_inventory_completeness: "unknown", }); }); it("does not use generated symbols and preserves literal named FUN_ symbols when producer metadata proves their origin", async () => { const entries = [ { address: "0x10", value: "FUN_10" }, { address: "0x20", value: "apparently_named" }, ]; const names = (side: "1" | "2") => createEvidence(nativeMatchingTarget(side), nativeMatchingProvider, { operation: "list_names", parameters: {}, result: entries.map((entry, i) => ({ ...entry, symbol: { primary: true, dynamic: i === 1, external: false, type: "function", source: i === 0 ? "user_defined" : "default", }, })), }); const { report } = await compare({ left: nativeMatchingInventory("1", entries), right: nativeMatchingInventory("2", entries), left_observations: [names("1")], right_observations: [names("2")], }); expect(report.summary.matched).toBe(1); expect(report.matches[0]?.left[0]?.name).toBe("FUN_10"); expect(report.matches[0]?.left[0]?.evidence_links).toContain( names("1").evidence_id, ); }); it("assigns source authentication, role selection, context, and lookup failures to their owning boundary", async () => { const left = nativeMatchingInventory("1", [ { address: "0x10", value: "main" }, ]); const cases = [ { input: { left: { ...left, normalized_result: [] }, right: left }, tag: "EvidenceIntegrityError", }, { input: { left: nativeMatchingDetails("1", "0x10", "c3")[0], right: left }, tag: "AnalysisInputError", path: ["left"], }, { input: { left, right: left, left_observations: nativeMatchingDetails("2", "0x10", "c3"), }, tag: "EvidenceIntegrityError", }, { input: { left: nativeMatchingInventory("1", [ { address: "not-an-address", value: "main" }, ]), right: left, }, tag: "EvidenceIntegrityError", }, { input: { left, right: left, left_observations: [left] }, tag: "AnalysisInputError", path: ["left_observations", 0], }, { input: { left, right: left, left_observations: [ createEvidence(nativeMatchingTarget("1"), nativeMatchingProvider, { operation: "procedure_info", parameters: { procedure: "0x10" }, result: { ...procedureInfoSchema.parse( nativeMatchingDetails("1", "0x10", "c3")[0]!.normalized_result, ), entrypoint: "not-an-address", }, }), ], }, tag: "EvidenceIntegrityError", }, { input: { left, right: left, left_observations: [ { kind: "retained-evidence", evidence_id: `ev_${"a".repeat(64)}` }, ], }, tag: "EvidenceIntegrityError", evidenceId: `ev_${"a".repeat(64)}`, reason: "missing", }, ] as const; for (const { input, tag, ...expected } of cases) { const result = await matchNativeFunctions(input); expect(result.ok).toBe(false); if (!result.ok) { expect(result.error._tag).toBe(tag); if ("path" in expected) { expect(result.error).toMatchObject({ issues: expect.arrayContaining([ expect.objectContaining({ path: expected.path }), ]), }); } if ("evidenceId" in expected) expect(result.error).toMatchObject({ evidenceId: expected.evidenceId, reason: expected.reason, }); } } }); it("cooperatively preserves the exact cancellation reason and rejects expansion before cloning oversized input", async () => { const controller = new AbortController(), reason = new Error("stop matching"); const left = nativeMatchingInventory( "1", Array.from({ length: 2000 }, (_, i) => ({ address: `0x${i.toString(16)}`, value: `item${i}`, })), ); const pending = matchNativeFunctions( { left, right: left }, { signal: controller.signal }, ); setImmediate(() => controller.abort(reason)); const cancelled = await pending; expect(cancelled.ok).toBe(false); if (!cancelled.ok) { expect(cancelled.error._tag).toBe("AnalysisCancelledError"); expect(cancelled.error.cause).toBe(reason); } const large = await matchNativeFunctions({ left: { padding: "x".repeat(26 * 1024 * 1024) }, right: left, }); expect(large.ok).toBe(false); if (!large.ok) expect(large.error._tag).toBe("AnalysisResourceConstraintError"); }); it("keeps duplicate candidate storage linear and emits every input occurrence exactly once", async () => { const entries = Array.from({ length: 1000 }, (_, i) => ({ address: `0x${i.toString(16)}`, value: "same", })); const { report } = await compare({ left: nativeMatchingInventory("1", entries), right: nativeMatchingInventory("2", entries), }); expect(report.matches).toHaveLength(1); expect(report.matches[0]?.left).toHaveLength(1000); expect(report.matches[0]?.right).toHaveLength(1000); expect(new Set(report.matches[0]?.left.map((x) => x.function_id)).size).toBe( 1000, ); }); it("normalizes accepted body range address spaces before matching byte coverage", async () => { const body = { available: true as const, provenance: "synthetic mixed-space body", ranges: [ { start: "ram:0x10", end: "ram:0x10" }, { start: "other%ab:0x20", end: "other%ab:0x20" }, ], total_bytes: 2, span_bytes: null, non_contiguous: true, contains_entry: true, }; const info = createEvidence( nativeMatchingTarget("1"), nativeMatchingProvider, { operation: "procedure_info", parameters: { procedure: "ram:0x10" }, result: { name: "main", entrypoint: "ram:0x10", basicblock_count: 1, length: 2, signature: null, locals: [], classification: null, body, }, }, ); const read = (address: string, value: string) => createEvidence(nativeMatchingTarget("1"), nativeMatchingProvider, { operation: "read_bytes", parameters: {}, result: { address, requested_bytes: 1, returned_bytes: 1, bytes_hex: value, complete: true, }, }); const { report } = await compare({ left: nativeMatchingInventory("1", [ { address: "ram:0x10", value: "main" }, ]), right: nativeMatchingInventory("2", [{ address: "0x30", value: "main" }]), left_observations: [ info, read("ram:0x10", "01"), read("other%AB:0x20", "02"), ], }); expect(report.matches[0]?.left[0]?.body_sha256).toBe( createHash("sha256").update(Buffer.from("0102", "hex")).digest("hex"), ); }); it("compares repeated bodies and entry membership after address-space normalization", async () => { const procedure = (entrypoint: string, rangeAddress: string) => createEvidence(nativeMatchingTarget("1"), nativeMatchingProvider, { operation: "procedure_info", parameters: { procedure: entrypoint }, result: { name: "main", entrypoint, basicblock_count: 1, length: 2, signature: null, locals: [], classification: null, body: { available: true, provenance: "synthetic escaped-space body", ranges: [ { start: `${rangeAddress}:0x10`, end: `${rangeAddress}:0x11`, }, ], total_bytes: 2, span_bytes: 2, non_contiguous: false, contains_entry: true, }, }, }); const first = procedure("ram%AB:0x10", "ram%ab"); const equivalent = procedure("ram%ab:0x10", "ram%AB"); const bytes = createEvidence( nativeMatchingTarget("1"), nativeMatchingProvider, { operation: "read_bytes", parameters: { address: "ram%AB:0x10", length: 2 }, result: { address: "ram%AB:0x10", requested_bytes: 2, returned_bytes: 2, bytes_hex: "0102", complete: true, }, }, ); const { report } = await compare({ left: nativeMatchingInventory("1", [ { address: "ram%AB:0x10", value: "main" }, ]), right: nativeMatchingInventory("2", [{ address: "0x20", value: "main" }]), left_observations: [first, equivalent, bytes], }); const member = report.matches[0]?.left[0]; expect(member?.body_sha256).toBe( createHash("sha256").update(Buffer.from("0102", "hex")).digest("hex"), ); expect(member?.limitations).not.toContain( "Conflicting procedure observations; body and dossier remain unknown.", ); expect(member?.evidence_links).toEqual( expect.arrayContaining([first.evidence_id, equivalent.evidence_id]), ); }); it("retains zero-byte reads with unavailable addresses when no request coordinate exists", async () => { const info = createEvidence( nativeMatchingTarget("1"), nativeMatchingProvider, { operation: "procedure_info", parameters: { procedure: "0x10" }, result: { name: "main", entrypoint: "0x10", basicblock_count: 1, length: 1, signature: null, locals: [], classification: null, body: { available: true, provenance: "synthetic body", ranges: [{ start: "0x10", end: "0x10" }], total_bytes: 1, span_bytes: 1, non_contiguous: false, contains_entry: true, }, }, }, ); const emptyRead = createEvidence( nativeMatchingTarget("1"), nativeMatchingProvider, { operation: "read_bytes", parameters: {}, result: { address: "unavailable", requested_bytes: 1, returned_bytes: 0, bytes_hex: "", complete: false, }, }, ); const left = nativeMatchingInventory("1", [ { address: "0x10", value: "main" }, ]); const right = nativeMatchingInventory("2", [ { address: "0x20", value: "main" }, ]); const result = await matchNativeFunctions({ left, right, left_observations: [info, emptyRead], }); if (!result.ok) throw result.error; const report = nativeFunctionMatchingSchema.parse( result.value.evidence.normalized_result, ); expect(report.matches[0]?.left[0]).toMatchObject({ body_bytes: 1, body_sha256: null, }); expect(result.value.evidence.evidence_links).toContain(emptyRead.evidence_id); expect(result.value.sources.map((source) => source.evidence_id)).toContain( emptyRead.evidence_id, ); });