mirror of
https://github.com/morluto/rea
synced 2026-10-30 07:39:58 -04:00
fix(native): preserve partial call reference evidence
Retain provider-reported call sites and raw classification independently of generic reference flags. Keep all endpoint evidence when containing-procedure routes merge, and include relation in edge identity to avoid order-dependent labels. Closes #5130
This commit is contained in:
@@ -216,12 +216,24 @@ 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
|
||||
Incomplete reference kinds do not discard known typed calls or partial
|
||||
provider call references. A `provider_call` with `observed` resolution retains
|
||||
the provider-reported call site and endpoint without claiming generic direct,
|
||||
indirect or Objective-C dispatch semantics. Its `call_reference` evidence
|
||||
preserves the original endpoints, classification, raw provider type and
|
||||
provenance, even when the target node is a containing procedure. Hopper's
|
||||
`none`, `unknown`, `direct` and `objective_c` labels stay provider-scoped;
|
||||
unknown numeric types remain available in the raw type field. Target-only
|
||||
callee fallback uses `provider_call` with `inferred` resolution.
|
||||
|
||||
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.
|
||||
Reports sharing a call site and containing target procedure retain all their
|
||||
original endpoint evidence on the shared graph edge. Different reported
|
||||
relations retain separate edges, independent of enumeration order.
|
||||
|
||||
Session discovery describes two automatic source routes for this trace:
|
||||
`authored_ui_dispatch` needs Interface Builder decoding and dispatch metadata;
|
||||
|
||||
@@ -12,8 +12,12 @@ 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";
|
||||
import {
|
||||
nativeCallClassificationSchema,
|
||||
type NativeCallReference,
|
||||
} from "../../domain/native/nativeCallReference.js";
|
||||
|
||||
/** Preferred typed observations and the untyped fallback share one owner. */
|
||||
/** Detailed or partial observations and the untyped fallback share one owner. */
|
||||
export const NATIVE_CALL_ROUTE_SOURCES = {
|
||||
references: "procedure_references",
|
||||
callees: "procedure_callees",
|
||||
@@ -34,6 +38,7 @@ const referencesSchema = z.object({
|
||||
target_address: z.string(),
|
||||
target_procedure: z.object({ address: z.string() }).nullable(),
|
||||
kind: referenceKindSchema,
|
||||
call: nativeCallClassificationSchema.exactOptional(),
|
||||
}),
|
||||
),
|
||||
unresolved_calls: z.array(
|
||||
@@ -47,6 +52,7 @@ interface NativeCallRoute {
|
||||
readonly relation: "direct_call" | "indirect_call" | "provider_call";
|
||||
readonly resolution: "observed" | "resolved" | "ambiguous" | "inferred";
|
||||
readonly provider: ProviderIdentity;
|
||||
readonly callReference?: NativeCallReference;
|
||||
}
|
||||
|
||||
/** Enumeration failure does not invalidate facts already obtained. */
|
||||
@@ -61,11 +67,13 @@ interface NativeCallRouteRead {
|
||||
readonly outcome: Result<void, AnalysisError>;
|
||||
}
|
||||
|
||||
const typedCallRoutes = (
|
||||
const observedCallRoutes = (
|
||||
references: z.infer<typeof referencesSchema>["references"],
|
||||
provider: ProviderIdentity,
|
||||
): NativeCallRoute[] => {
|
||||
const calls = references.filter(({ kind }) => kind.available && kind.call);
|
||||
const calls = references.filter(({ kind, call }) =>
|
||||
kind.available ? kind.call : call !== undefined,
|
||||
);
|
||||
const countBySite = new Map<string, Set<string>>();
|
||||
for (const call of calls) {
|
||||
const targets = countBySite.get(call.source_address) ?? new Set<string>();
|
||||
@@ -76,20 +84,31 @@ const typedCallRoutes = (
|
||||
provider,
|
||||
address: call.target_procedure?.address ?? call.target_address,
|
||||
site: call.source_address,
|
||||
relation:
|
||||
call.kind.available && (call.kind.computed || call.kind.indirect)
|
||||
...(call.call === undefined
|
||||
? {}
|
||||
: {
|
||||
callReference: {
|
||||
...call.call,
|
||||
source_address: call.source_address,
|
||||
target_address: call.target_address,
|
||||
},
|
||||
}),
|
||||
relation: !call.kind.available
|
||||
? ("provider_call" as const)
|
||||
: call.kind.computed || call.kind.indirect
|
||||
? ("indirect_call" as const)
|
||||
: ("direct_call" as const),
|
||||
resolution:
|
||||
(countBySite.get(call.source_address)?.size ?? 0) > 1
|
||||
resolution: !call.kind.available
|
||||
? ("observed" as const)
|
||||
: (countBySite.get(call.source_address)?.size ?? 0) > 1
|
||||
? ("ambiguous" as const)
|
||||
: call.kind.available && (call.kind.computed || call.kind.indirect)
|
||||
: call.kind.computed || call.kind.indirect
|
||||
? ("resolved" as const)
|
||||
: ("observed" as const),
|
||||
}));
|
||||
};
|
||||
|
||||
/** Retain typed observations while supplementing incomplete kinds with inferred callees. */
|
||||
/** Retain available observations while supplementing incomplete kinds with inferred callees. */
|
||||
export const readNativeCallRoutes = async (
|
||||
analysis: AnalysisOperationPort,
|
||||
procedure: string,
|
||||
@@ -148,7 +167,7 @@ export const readNativeCallRoutes = async (
|
||||
})),
|
||||
);
|
||||
const references = parsed.data.references;
|
||||
routes.push(...typedCallRoutes(references, execution.value.provider));
|
||||
routes.push(...observedCallRoutes(references, execution.value.provider));
|
||||
if (
|
||||
references.every(({ kind }) => kind.available) &&
|
||||
(parsed.data.reference_kinds_available || references.length > 0)
|
||||
|
||||
@@ -471,39 +471,43 @@ const expandNativeCalls = async (input: {
|
||||
}
|
||||
for (const [index, route] of execution.routes.entries()) {
|
||||
const address = route.address;
|
||||
if (edges.size >= input.input.max_edges) {
|
||||
const targetId = `native:function:${address}`;
|
||||
const targetOmitted =
|
||||
!nodes.has(targetId) && nodes.size >= input.input.max_nodes;
|
||||
const edgeId = `${targetOmitted ? "unknown-node-bound" : "call"}:${current.address}:${route.site}:${address}:${route.relation}`;
|
||||
if (!edges.has(edgeId) && edges.size >= input.input.max_edges) {
|
||||
truncated = true;
|
||||
omitted += execution.routes.length - index;
|
||||
break;
|
||||
}
|
||||
const targetId = `native:function:${address}`;
|
||||
const evidence = [
|
||||
{
|
||||
kind: "reference" as const,
|
||||
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,
|
||||
},
|
||||
];
|
||||
const evidence = edges.get(edgeId)?.evidence ?? [];
|
||||
evidence.push({
|
||||
kind: "reference",
|
||||
provider: route.provider,
|
||||
description: `${route.provider.name} reported ${route.relation} from ${current.address} to ${address}`,
|
||||
location: { address: route.site, file_offset: null },
|
||||
...(route.callReference === undefined
|
||||
? {}
|
||||
: { call_reference: route.callReference }),
|
||||
artifact_path: null,
|
||||
artifact_sha256: input.targetSha256,
|
||||
});
|
||||
if (targetOmitted) {
|
||||
truncated = true;
|
||||
omitted += 1;
|
||||
edges.set(edgeId, {
|
||||
id: edgeId,
|
||||
from: current.node.id,
|
||||
to: null,
|
||||
relation: route.relation,
|
||||
resolution: "unresolved",
|
||||
reason: "native_trace_node_bound_reached",
|
||||
evidence,
|
||||
limitations: [],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (!nodes.has(targetId)) {
|
||||
if (nodes.size >= input.input.max_nodes) {
|
||||
truncated = true;
|
||||
omitted += 1;
|
||||
const id = `unknown-node-bound:${current.address}:${route.site}:${address}`;
|
||||
edges.set(id, {
|
||||
id,
|
||||
from: current.node.id,
|
||||
to: null,
|
||||
relation: route.relation,
|
||||
resolution: "unresolved",
|
||||
reason: "native_trace_node_bound_reached",
|
||||
evidence,
|
||||
limitations: [],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const name = addressNames.get(address) ?? address;
|
||||
const node = {
|
||||
id: targetId,
|
||||
@@ -516,7 +520,6 @@ const expandNativeCalls = async (input: {
|
||||
nodes.set(targetId, node);
|
||||
queue.push({ node, address, depth: current.depth + 1 });
|
||||
}
|
||||
const edgeId = `call:${current.address}:${route.site}:${address}`;
|
||||
edges.set(edgeId, {
|
||||
id: edgeId,
|
||||
from: current.node.id,
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { JsonValue } from "./jsonValue.js";
|
||||
import { err, ok, type Result } from "./result.js";
|
||||
import { nativeApiBoundarySchema } from "./native/nativeApiBoundary.js";
|
||||
import { nativeValueFlowSchema } from "./native/nativeValueFlow.js";
|
||||
import { nativeCallClassificationSchema } from "./native/nativeCallReference.js";
|
||||
import { AnalysisOutputError } from "./analysisErrorCore.js";
|
||||
|
||||
export interface AddressedName {
|
||||
@@ -350,13 +351,7 @@ export const referenceEdgeSchema = z
|
||||
source_procedure: procedureIdentitySchema.nullable(),
|
||||
target_procedure: procedureIdentitySchema.nullable(),
|
||||
kind: referenceKindSchema,
|
||||
call: z
|
||||
.strictObject({
|
||||
classification: z.enum(["none", "unknown", "direct", "objective_c"]),
|
||||
provider_type: z.number().int(),
|
||||
provenance: z.string().min(1),
|
||||
})
|
||||
.exactOptional(),
|
||||
call: nativeCallClassificationSchema.exactOptional(),
|
||||
})
|
||||
.strict();
|
||||
const facetAvailabilitySchema = z.discriminatedUnion("available", [
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/** Partial provider classification; it does not establish generic reference flags. */
|
||||
export const nativeCallClassificationSchema = z.strictObject({
|
||||
classification: z.enum(["none", "unknown", "direct", "objective_c"]),
|
||||
provider_type: z.number().int(),
|
||||
provenance: z.string().min(1),
|
||||
});
|
||||
|
||||
/** Original provider endpoints, before resolving a containing procedure. */
|
||||
export const nativeCallReferenceSchema = nativeCallClassificationSchema.extend({
|
||||
source_address: z.string(),
|
||||
target_address: z.string(),
|
||||
});
|
||||
export type NativeCallReference = z.infer<typeof nativeCallReferenceSchema>;
|
||||
@@ -2,6 +2,7 @@ import { z } from "zod";
|
||||
import { isMachOFormat, type BinaryTarget } from "../binaryTargetTypes.js";
|
||||
import { providerSchema } from "../evidence.js";
|
||||
import { isSwiftMangledSymbolName } from "../swiftSymbolClassification.js";
|
||||
import { nativeCallReferenceSchema } from "./nativeCallReference.js";
|
||||
|
||||
import {
|
||||
compareUnicodeCodePoints,
|
||||
@@ -126,6 +127,7 @@ export const nativeMetadataEvidenceSchema = z.strictObject({
|
||||
location: nativeMetadataLocationSchema,
|
||||
/** Preserve a provider's source value when it does not establish a location. */
|
||||
reported_address: z.string().optional(),
|
||||
call_reference: nativeCallReferenceSchema.exactOptional(),
|
||||
artifact_path: z.string().min(1).nullable().default(null),
|
||||
artifact_sha256: z
|
||||
.string()
|
||||
|
||||
@@ -52,14 +52,40 @@ function typedKind(indirect = false) {
|
||||
external: false,
|
||||
};
|
||||
}
|
||||
function ref(site: string, target: string, indirect = false) {
|
||||
type CallReference = {
|
||||
readonly classification: "none" | "unknown" | "direct" | "objective_c";
|
||||
readonly provider_type: number;
|
||||
readonly provenance: string;
|
||||
};
|
||||
function ref(
|
||||
site: string,
|
||||
target: string,
|
||||
indirect = false,
|
||||
callReference?: CallReference,
|
||||
targetProcedure = target,
|
||||
) {
|
||||
return {
|
||||
source_address: site,
|
||||
target_address: target,
|
||||
target_procedure: { address: target },
|
||||
kind: typedKind(indirect),
|
||||
target_procedure: { address: targetProcedure },
|
||||
kind:
|
||||
callReference === undefined
|
||||
? typedKind(indirect)
|
||||
: {
|
||||
available: false as const,
|
||||
reason: "Detailed reference-kind flags unavailable",
|
||||
},
|
||||
...(callReference === undefined ? {} : { call: callReference }),
|
||||
};
|
||||
}
|
||||
const partialCall = (
|
||||
classification: CallReference["classification"],
|
||||
provider_type: number,
|
||||
): CallReference => ({
|
||||
classification,
|
||||
provider_type,
|
||||
provenance: "hopper-public-python-api:CallReference",
|
||||
});
|
||||
const mixedReferences = (refs = [ref("0x1004", "0x2000")]) => ({
|
||||
reference_kinds_available: false,
|
||||
references: [
|
||||
@@ -85,12 +111,12 @@ const noClassifiedReferences = {
|
||||
};
|
||||
|
||||
type Plan = {
|
||||
readonly references?: ReturnType<typeof mixedReferences>;
|
||||
readonly references?: unknown;
|
||||
readonly callees?: readonly string[];
|
||||
readonly fallbackProvider?: typeof identity;
|
||||
readonly fallbackFailure?: "unavailable" | "output" | "cancelled";
|
||||
};
|
||||
function publicTrace(plan: Plan, maxNodes = 12) {
|
||||
function publicTrace(plan: Plan, maxNodes = 12, maxEdges = 500) {
|
||||
const analysis: AnalysisOperationPort = {
|
||||
execute: async (
|
||||
operation,
|
||||
@@ -173,7 +199,12 @@ function publicTrace(plan: Plan, maxNodes = 12) {
|
||||
await client.connect(clientTransport);
|
||||
const result = await client.callTool({
|
||||
name: "trace_native_ui_action",
|
||||
arguments: { action: "0x1000", max_nodes: maxNodes, max_depth: 1 },
|
||||
arguments: {
|
||||
action: "0x1000",
|
||||
max_nodes: maxNodes,
|
||||
max_edges: maxEdges,
|
||||
max_depth: 1,
|
||||
},
|
||||
});
|
||||
return result;
|
||||
})();
|
||||
@@ -421,7 +452,267 @@ it("retains separate omitted callsites when routes share a target", async () =>
|
||||
});
|
||||
|
||||
it("keeps cancellation terminal when fallback enumeration is cancelled", async () => {
|
||||
const response = await publicTrace({ fallbackFailure: "cancelled" });
|
||||
const response = await publicTrace({
|
||||
references: completeReferences([
|
||||
ref("0x1004", "0x2000", false, partialCall("direct", 2)),
|
||||
]),
|
||||
fallbackFailure: "cancelled",
|
||||
});
|
||||
expect(response.isError).toBe(true);
|
||||
expect(parseMcpToolError(response).error.code).toBe("cancelled");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ classification: "direct", provider_type: 2 },
|
||||
{ classification: "objective_c", provider_type: 3 },
|
||||
{ classification: "none", provider_type: 0 },
|
||||
{ classification: "unknown", provider_type: 1 },
|
||||
{ classification: "unknown", provider_type: 99 },
|
||||
] as const)(
|
||||
"retains raw $classification CallReference facts without inventing generic flags",
|
||||
async ({ classification, provider_type }) => {
|
||||
const call = partialCall(classification, provider_type);
|
||||
const response = await publicTrace({
|
||||
references: completeReferences([
|
||||
ref("0x1004", "0x2000", false, call, "0x2fff"),
|
||||
]),
|
||||
callees: [],
|
||||
});
|
||||
expect(response.isError).not.toBe(true);
|
||||
const trace = traceFrom(response);
|
||||
expect(trace.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
from: "native:function:0x1000",
|
||||
to: "native:function:0x2fff",
|
||||
relation: "provider_call",
|
||||
resolution: "observed",
|
||||
evidence: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
provider: identity,
|
||||
location: expect.objectContaining({ address: "0x1004" }),
|
||||
call_reference: {
|
||||
...call,
|
||||
source_address: "0x1004",
|
||||
target_address: "0x2000",
|
||||
},
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
expect(trace.edges.some((edge) => edge.relation === "objc_dispatch")).toBe(
|
||||
false,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("deduplicates fallback overlap while preserving CallReference site and provider", async () => {
|
||||
const call = partialCall("direct", 2);
|
||||
const response = await publicTrace({
|
||||
references: completeReferences([ref("0x1004", "0x2000", false, call)]),
|
||||
callees: ["0x2000", "0x3000"],
|
||||
});
|
||||
expect(response.isError).not.toBe(true);
|
||||
const trace = traceFrom(response);
|
||||
const targetEdges = trace.edges.filter(
|
||||
(edge) => edge.to === "native:function:0x2000",
|
||||
);
|
||||
expect(targetEdges).toHaveLength(1);
|
||||
expect(targetEdges[0]).toMatchObject({
|
||||
relation: "provider_call",
|
||||
resolution: "observed",
|
||||
evidence: [
|
||||
expect.objectContaining({
|
||||
provider: identity,
|
||||
location: expect.objectContaining({ address: "0x1004" }),
|
||||
call_reference: expect.objectContaining({
|
||||
classification: "direct",
|
||||
provider_type: 2,
|
||||
provenance: "hopper-public-python-api:CallReference",
|
||||
source_address: "0x1004",
|
||||
target_address: "0x2000",
|
||||
}),
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["unavailable", "output"] as const)(
|
||||
"preserves partial CallReference evidence when fallback has %s failure",
|
||||
async (fallbackFailure) => {
|
||||
const call = partialCall("direct", 2);
|
||||
const response = await publicTrace({
|
||||
references: completeReferences([
|
||||
ref("0x1004", "0x2000", false, call, "0x2fff"),
|
||||
]),
|
||||
fallbackFailure,
|
||||
});
|
||||
expect(response.isError).not.toBe(true);
|
||||
const trace = traceFrom(response);
|
||||
expect(trace.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
to: "native:function:0x2fff",
|
||||
relation: "provider_call",
|
||||
resolution: "observed",
|
||||
evidence: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
provider: identity,
|
||||
location: expect.objectContaining({ address: "0x1004" }),
|
||||
call_reference: expect.objectContaining({
|
||||
...call,
|
||||
source_address: "0x1004",
|
||||
target_address: "0x2000",
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
expect(trace.unresolved).toContainEqual(
|
||||
expect.objectContaining({
|
||||
to: null,
|
||||
relation: "provider_call",
|
||||
resolution: "unresolved",
|
||||
limitations: expect.arrayContaining([
|
||||
"The provider could not complete callee enumeration.",
|
||||
]),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps partial CallReference evidence on a node-bound unresolved edge", async () => {
|
||||
const response = await publicTrace(
|
||||
{
|
||||
references: completeReferences([
|
||||
ref("0x1004", "0x2000", false, partialCall("direct", 2)),
|
||||
ref("0x1008", "0x3000", false, partialCall("unknown", 99)),
|
||||
]),
|
||||
callees: [],
|
||||
},
|
||||
2,
|
||||
);
|
||||
expect(response.isError).not.toBe(true);
|
||||
const trace = traceFrom(response);
|
||||
expect(trace.unresolved).toContainEqual(
|
||||
expect.objectContaining({
|
||||
relation: "provider_call",
|
||||
resolution: "unresolved",
|
||||
evidence: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
provider: identity,
|
||||
location: expect.objectContaining({ address: "0x1008" }),
|
||||
call_reference: expect.objectContaining({
|
||||
classification: "unknown",
|
||||
provider_type: 99,
|
||||
source_address: "0x1008",
|
||||
target_address: "0x3000",
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("reports malformed partial CallReference as unreadable route evidence", async () => {
|
||||
const response = await publicTrace({
|
||||
references: {
|
||||
reference_kinds_available: false,
|
||||
references: [
|
||||
{
|
||||
...ref("0x1004", "0x2000"),
|
||||
kind: { available: false, reason: "Detailed flags unavailable" },
|
||||
call: {
|
||||
classification: "objectivec",
|
||||
provider_type: 3,
|
||||
provenance: "hopper-public-python-api:CallReference",
|
||||
},
|
||||
},
|
||||
],
|
||||
unresolved_calls: [],
|
||||
},
|
||||
fallbackFailure: "unavailable",
|
||||
});
|
||||
expect(response.isError).not.toBe(true);
|
||||
const trace = traceFrom(response);
|
||||
expect(trace.edges).toEqual([]);
|
||||
expect(trace.unresolved).toContainEqual(
|
||||
expect.objectContaining({
|
||||
relation: "provider_call",
|
||||
resolution: "unresolved",
|
||||
reason: expect.stringContaining("unreadable result"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each([2, 12])(
|
||||
"retains original endpoints on a shared edge at the edge bound (maxNodes=%s)",
|
||||
async (maxNodes) => {
|
||||
const response = await publicTrace(
|
||||
{
|
||||
references: completeReferences(
|
||||
[
|
||||
ref("0x1004", "0x2004", false, partialCall("direct", 2), "0x2000"),
|
||||
ref(
|
||||
"0x1004",
|
||||
"0x2008",
|
||||
false,
|
||||
partialCall("unknown", 99),
|
||||
"0x2000",
|
||||
),
|
||||
ref("0x1008", "0x3000", false, partialCall("direct", 2)),
|
||||
].reverse(),
|
||||
),
|
||||
callees: [],
|
||||
},
|
||||
maxNodes,
|
||||
2,
|
||||
);
|
||||
expect(response.isError).not.toBe(true);
|
||||
const trace = traceFrom(response);
|
||||
const evidence = [...trace.edges, ...trace.unresolved].flatMap(
|
||||
(edge) => edge.evidence,
|
||||
);
|
||||
expect(evidence.map((item) => item.call_reference)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ target_address: "0x2004", provider_type: 2 }),
|
||||
expect.objectContaining({
|
||||
target_address: "0x2008",
|
||||
provider_type: 99,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ maxNodes: 2, reverse: false },
|
||||
{ maxNodes: 2, reverse: true },
|
||||
{ maxNodes: 12, reverse: false },
|
||||
{ maxNodes: 12, reverse: true },
|
||||
])(
|
||||
"preserves typed and partial relations sharing a site and target ($maxNodes/$reverse)",
|
||||
async ({ maxNodes, reverse }) => {
|
||||
const references = [
|
||||
ref("0x1004", "0x2000"),
|
||||
ref("0x1004", "0x2008", false, partialCall("unknown", 99), "0x2000"),
|
||||
];
|
||||
const response = await publicTrace(
|
||||
{
|
||||
references: completeReferences([
|
||||
ref("0x1008", "0x3000"),
|
||||
...(reverse ? references.reverse() : references),
|
||||
]),
|
||||
callees: [],
|
||||
},
|
||||
maxNodes,
|
||||
);
|
||||
expect(response.isError).not.toBe(true);
|
||||
const trace = traceFrom(response);
|
||||
const routes = [...trace.edges, ...trace.unresolved].filter((edge) =>
|
||||
edge.evidence.some((item) => item.location.address === "0x1004"),
|
||||
);
|
||||
expect(routes.map((edge) => edge.relation).sort()).toEqual([
|
||||
"direct_call",
|
||||
"provider_call",
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user