mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
626c7b422a | ||
|
|
678cd82042 |
No files matched your search
@@ -799,6 +799,7 @@ jobs:
|
||||
tests/boundary/filesystem/installScript.test.ts
|
||||
tests/boundary/filesystem/nibNativeHierarchy.test.ts
|
||||
tests/boundary/filesystem/interfaceBuilderBundle.test.ts
|
||||
tests/acceptance/nativeUiHandlerLocation.test.ts
|
||||
tests/boundary/cli/nativePlistCli.test.ts
|
||||
tests/boundary/cli/nativePlistPrototypeKeyCli.test.ts
|
||||
tests/boundary/cli/nativePlistSelectionFailureCli.test.ts
|
||||
|
||||
@@ -378,7 +378,9 @@ bounded typed call references. Direct references, resolved indirect references,
|
||||
ambiguous candidates, inferred untyped provider callees and targetless call
|
||||
sites remain distinct. A static route does not establish runtime reachability.
|
||||
Swift/closure dispatch requires resolved static references; unavailable ABI
|
||||
forms remain unknown.
|
||||
forms remain unknown. A handler node's `location` pairs the implementation
|
||||
address with its `implementation_file_offset`, or an unknown offset; the method
|
||||
record's own location remains in the node's Evidence.
|
||||
|
||||
Incomplete reference kinds do not discard known typed calls or partial
|
||||
provider call references. A `provider_call` with `observed` resolution retains
|
||||
@@ -835,6 +837,11 @@ complete observation.
|
||||
non-generic, non-resilient class vtable descriptors. Records include exact virtual addresses/file offsets and
|
||||
artifact evidence. Vtable indexes are metadata word offsets; witness indexes
|
||||
start after the conformance header. Names unavailable in metadata remain null.
|
||||
An Objective-C implementation's `location` is its method-list entry; the
|
||||
reader maps `implementation_address` separately into
|
||||
`implementation_file_offset`, which uses the same file coordinates as
|
||||
`location.file_offset`, including a universal slice's container offset.
|
||||
Unmapped implementations and symbol-only records keep it null.
|
||||
The selected Mach-O header must match the requested CPU family for both thin
|
||||
files and universal slices before metadata decoding. CPU subtypes and
|
||||
capability bits remain source facts; an `arm64` request may select an
|
||||
|
||||
@@ -152,6 +152,11 @@ const PATH_SCOPES = {
|
||||
"tests/boundary/filesystem/installScript.test.ts",
|
||||
"tests/boundary/filesystem/nibNativeHierarchy.test.ts",
|
||||
"tests/boundary/filesystem/interfaceBuilderBundle.test.ts",
|
||||
// Compiled UI handler joins: metadata reader, graph and trace workflow.
|
||||
"src/domain/native/objcSwiftMetadata.ts",
|
||||
"src/domain/native/nativeInvestigationGraph.ts",
|
||||
"src/application/native/NativeUiActionTrace.ts",
|
||||
"tests/acceptance/nativeUiHandlerLocation.test.ts",
|
||||
"tests/boundary/cli/nativePlistCli.test.ts",
|
||||
"tests/boundary/cli/nativePlistPrototypeKeyCli.test.ts",
|
||||
"tests/boundary/cli/nativePlistSelectionFailureCli.test.ts",
|
||||
|
||||
@@ -257,6 +257,90 @@ describe("Interface Builder dispatch joins", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("Interface Builder handler locations", () => {
|
||||
const record = { address: "0x100000798", file_offset: 34712 };
|
||||
const handlerFor = (implementationFileOffset?: number | null) => {
|
||||
const ui = buildInterfaceBuilderAnalysis({
|
||||
targetSha256: "b".repeat(64),
|
||||
toolVersion: "test",
|
||||
documents: [
|
||||
{
|
||||
relativePath: "Contents/Resources/Main.nib",
|
||||
archiveSha256: "c".repeat(64),
|
||||
documentKind: "nib",
|
||||
raw: {
|
||||
"com.apple.ibtool.document.objects": {
|
||||
controller: { customClass: "AuditController" },
|
||||
button: { class: "NSButton", title: "Run" },
|
||||
},
|
||||
"com.apple.ibtool.document.connections": {
|
||||
button: [
|
||||
{
|
||||
type: "action",
|
||||
label: "runAction:",
|
||||
destinationId: "controller",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
limits: interfaceBuilderLimitsSchema.parse({}),
|
||||
});
|
||||
const metadata = objcSwiftMetadataSchema.parse({
|
||||
objc_dispatch_implementations: [
|
||||
{
|
||||
class_name: "AuditController",
|
||||
selector: "runAction:",
|
||||
method_type: "instance",
|
||||
implementation_address: "0x10000077c",
|
||||
...(implementationFileOffset === undefined
|
||||
? {}
|
||||
: { implementation_file_offset: implementationFileOffset }),
|
||||
location: record,
|
||||
decode: { status: "decoded", reason: null },
|
||||
evidence: [
|
||||
{
|
||||
kind: "binary_metadata",
|
||||
description: "Encoded relative Objective-C method entry",
|
||||
location: record,
|
||||
artifact_path: "/fixture/Audit.app/Contents/MacOS/Audit",
|
||||
artifact_sha256: "b".repeat(64),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
db_save_result: null,
|
||||
});
|
||||
return joinInterfaceBuilderDispatch(ui.graph, metadata).nodes.find(
|
||||
({ id }) => id === "native:function:0x10000077c",
|
||||
);
|
||||
};
|
||||
|
||||
it.each([
|
||||
{ label: "mapped selected-slice", offset: 34684, expected: 34684 },
|
||||
{ label: "unmapped", offset: null, expected: null },
|
||||
{
|
||||
label: "older Evidence without a mapped",
|
||||
offset: undefined,
|
||||
expected: null,
|
||||
},
|
||||
])(
|
||||
"pairs the handler address only with its $label implementation offset",
|
||||
({ offset, expected }) => {
|
||||
const handler = handlerFor(offset);
|
||||
expect(handler?.location).toEqual({
|
||||
address: "0x10000077c",
|
||||
file_offset: expected,
|
||||
});
|
||||
// The method record keeps its own coordinates as Evidence.
|
||||
expect(handler?.evidence.map(({ location }) => location)).toEqual([
|
||||
record,
|
||||
]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("Objective-C category ownership joins", () => {
|
||||
it.each([
|
||||
{
|
||||
|
||||
@@ -389,9 +389,11 @@ export const joinInterfaceBuilderDispatch = (
|
||||
id: functionId,
|
||||
kind: "function",
|
||||
name: `${implementation.class_name} ${selector}`,
|
||||
// `implementation.location` is the method record, which stays in its
|
||||
// Evidence. Only the reader's mapping of the code address locates it.
|
||||
location: {
|
||||
address: implementation.implementation_address,
|
||||
file_offset: implementation.location.file_offset,
|
||||
file_offset: implementation.implementation_file_offset ?? null,
|
||||
},
|
||||
attributes: {
|
||||
class_name: implementation.class_name,
|
||||
|
||||
@@ -139,10 +139,12 @@ describe("symbol-only Objective-C implementation addresses", () => {
|
||||
1,
|
||||
);
|
||||
expect(result.objc_classes[0]?.methods[0]?.address).toBe(numeric);
|
||||
// A provider symbol names an address; it does not map it into the file.
|
||||
expect(result.objc_dispatch_implementations[0]).toMatchObject({
|
||||
implementation_address: canonical,
|
||||
location: { address: canonical },
|
||||
evidence: [{ location: { address: source } }],
|
||||
implementation_file_offset: null,
|
||||
location: { address: canonical, file_offset: null },
|
||||
evidence: [{ location: { address: source, file_offset: null } }],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -184,6 +184,12 @@ export const objcDispatchImplementationSchema = z.strictObject({
|
||||
selector: z.string().min(1),
|
||||
method_type: z.enum(["instance", "class"]),
|
||||
implementation_address: z.string().nullable(),
|
||||
/**
|
||||
* File offset of `implementation_address`, in the same artifact coordinates
|
||||
* as `location.file_offset`. Null when no reader mapped the implementation;
|
||||
* absent in older Evidence. `location` is the method record, not the code.
|
||||
*/
|
||||
implementation_file_offset: z.int().nonnegative().nullable().optional(),
|
||||
/** Category that adds this method, when it is not defined by the class itself. */
|
||||
category: z.string().nullable().optional(),
|
||||
location: nativeMetadataLocationSchema,
|
||||
@@ -598,6 +604,7 @@ export const inspectNativeDispatchMetadata = (
|
||||
selector,
|
||||
method_type: methodType,
|
||||
implementation_address: address,
|
||||
implementation_file_offset: null,
|
||||
location: { address, file_offset: null },
|
||||
decode: { status: "partial", reason: "symbol_name_only" },
|
||||
evidence,
|
||||
|
||||
@@ -1212,11 +1212,13 @@ describe("Apple dispatch binary metadata", () => {
|
||||
},
|
||||
{ name: "Fixture", is_meta_class: true },
|
||||
]);
|
||||
// The method entry and its code are distinct locations in one image.
|
||||
expect(result.objc_dispatch_implementations).toMatchObject([
|
||||
{
|
||||
selector: "performAction:",
|
||||
implementation_address: "0x100000700",
|
||||
location: { file_offset: 0x488 },
|
||||
implementation_file_offset: 0x700,
|
||||
location: { address: "0x100000488", file_offset: 0x488 },
|
||||
decode: { status: "decoded" },
|
||||
},
|
||||
]);
|
||||
@@ -1315,6 +1317,8 @@ describe("Apple dispatch coverage ownership", () => {
|
||||
const result = decodeAppleDispatchMetadata(bytes, 100, provenance);
|
||||
expect(result.objc_dispatch_implementations[0]).toMatchObject({
|
||||
implementation_address: null,
|
||||
implementation_file_offset: null,
|
||||
location: { address: "0x100000488", file_offset: 0x488 },
|
||||
decode: { status: "partial" },
|
||||
});
|
||||
expect(
|
||||
@@ -1387,6 +1391,8 @@ describe("Apple dispatch coverage ownership", () => {
|
||||
category: "FixtureCategory",
|
||||
class_name: "(FixtureCategory)",
|
||||
implementation_address: "0x100000700",
|
||||
implementation_file_offset: 0x700,
|
||||
location: { address: "0x100000b88", file_offset: 0xb88 },
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
@@ -1951,6 +1957,7 @@ describe("universal Apple dispatch metadata", () => {
|
||||
});
|
||||
expect(result.objc_dispatch_implementations[0]).toMatchObject({
|
||||
implementation_address: "0x100000700",
|
||||
implementation_file_offset: offset + 0x700,
|
||||
location: { file_offset: offset + 0x488 },
|
||||
});
|
||||
});
|
||||
@@ -2035,6 +2042,7 @@ describe("byte-swapped universal Apple dispatch metadata", () => {
|
||||
});
|
||||
expect(result.objc_dispatch_implementations[0]).toMatchObject({
|
||||
implementation_address: "0x100000700",
|
||||
implementation_file_offset: 256 + 0x700,
|
||||
location: { file_offset: 256 + 0x488 },
|
||||
});
|
||||
},
|
||||
|
||||
@@ -188,6 +188,7 @@ export const decodeObjcDispatchFacets = (input: {
|
||||
method_type: methodType,
|
||||
...(category === undefined ? {} : { category }),
|
||||
implementation_address: implementationLocation?.address ?? null,
|
||||
implementation_file_offset: implementationLocation?.file_offset ?? null,
|
||||
location: read.location(entry),
|
||||
decode:
|
||||
implementationLocation === null
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { buildBinary } from "plist";
|
||||
import { describe, expect } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { nativeInvestigationTraceSchema } from "../../src/domain/native/nativeInvestigationGraph.js";
|
||||
import { nativeDispatchMetadataResultSchema } from "../../src/domain/native/objcSwiftMetadata.js";
|
||||
import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../support/cli/cliFixture.js";
|
||||
import type { TestProcesses } from "../support/process/processFixture.js";
|
||||
|
||||
const HANDLER = "-[AuditController runAction:]";
|
||||
const SOURCE = `#import <Cocoa/Cocoa.h>
|
||||
__attribute__((noinline)) void audit_tail_sink(id value) { __asm__ volatile("" : : "r"(value) : "memory"); }
|
||||
@interface AuditController : NSViewController
|
||||
- (IBAction)runAction:(id)sender;
|
||||
@end
|
||||
@implementation AuditController
|
||||
- (IBAction)runAction:(id)sender { audit_tail_sink(sender); }
|
||||
@end
|
||||
int main(void) { return 0; }
|
||||
`;
|
||||
/** Keep provider selection on REA's own Interface Builder and Mach-O readers. */
|
||||
const LOCAL_READERS = {
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_ANALYSIS_PROVIDER: "auto",
|
||||
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
|
||||
GHIDRA_INSTALL_DIR: "/rea-unconfigured-deep-provider/ghidra",
|
||||
JAVA_HOME: "/rea-unconfigured-deep-provider/java",
|
||||
REA_IDA_MCP_CONFIG: "/rea-unconfigured-deep-provider/ida-mcp.json",
|
||||
REA_KUNA_COMMAND: "/rea-unconfigured-deep-provider/kuna",
|
||||
REA_KUNA_CACHE_DIR: "/rea-unconfigured-deep-provider/kuna-cache",
|
||||
};
|
||||
const evidenceOf = <Schema extends z.ZodType>(schema: Schema) =>
|
||||
z.object({
|
||||
subject: z.object({ architecture: z.string() }),
|
||||
normalized_result: schema,
|
||||
});
|
||||
const traceEvidence = evidenceOf(nativeInvestigationTraceSchema);
|
||||
const metadataEvidence = evidenceOf(nativeDispatchMetadataResultSchema);
|
||||
|
||||
/** Bundle a compiled handler with a keyed-archive NIB action wired to it. */
|
||||
const writeApp = async (root: string, executable: string) => {
|
||||
const app = join(root, "Audit.app");
|
||||
const contents = join(app, "Contents");
|
||||
await mkdir(join(contents, "MacOS"), { recursive: true });
|
||||
await mkdir(join(contents, "Resources"));
|
||||
await copyFile(executable, join(contents, "MacOS", "Audit"));
|
||||
await writeFile(
|
||||
join(contents, "Info.plist"),
|
||||
"<plist><dict><key>CFBundleExecutable</key><string>Audit</string></dict></plist>",
|
||||
);
|
||||
await writeFile(
|
||||
join(contents, "Resources", "Main.nib"),
|
||||
buildBinary({
|
||||
$archiver: "NSKeyedArchiver",
|
||||
$version: 100000,
|
||||
$objects: [
|
||||
"$null",
|
||||
{ $class: { UID: 4 } },
|
||||
{ $class: { UID: 5 }, NSClassName: "AuditController" },
|
||||
{
|
||||
$class: { UID: 6 },
|
||||
NSSource: { UID: 1 },
|
||||
NSDestination: { UID: 2 },
|
||||
NSLabel: "runAction:",
|
||||
},
|
||||
{
|
||||
$classname: "NSButton",
|
||||
$classes: ["NSButton", "NSControl", "NSView", "NSObject"],
|
||||
},
|
||||
{
|
||||
$classname: "NSCustomObject",
|
||||
$classes: ["NSCustomObject", "NSObject"],
|
||||
},
|
||||
{
|
||||
$classname: "NSNibControlConnector",
|
||||
$classes: ["NSNibControlConnector", "NSNibConnector", "NSObject"],
|
||||
},
|
||||
],
|
||||
$top: { root: { UID: 1 } },
|
||||
}),
|
||||
);
|
||||
return { app, executable: join(contents, "MacOS", "Audit") };
|
||||
};
|
||||
|
||||
/** Map the handler symbol through Apple's own symbol, load-command and FAT readers. */
|
||||
const independentHandlerLocation = async (
|
||||
processes: TestProcesses,
|
||||
executable: string,
|
||||
architecture: string,
|
||||
) => {
|
||||
const tool = async (...arguments_: string[]) => {
|
||||
const result = await processes.run("/usr/bin/xcrun", arguments_, {
|
||||
timeoutMs: 30_000,
|
||||
});
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
return result.stdout;
|
||||
};
|
||||
const symbol = (await tool("nm", "-arch", architecture, executable))
|
||||
.split("\n")
|
||||
.find((line) => line.endsWith(` ${HANDLER}`));
|
||||
expect(symbol, `nm lacks ${HANDLER}`).toBeDefined();
|
||||
const address = BigInt(`0x${symbol!.trim().split(/\s+/u)[0]!}`);
|
||||
const segment = [
|
||||
...(await tool("otool", "-arch", architecture, "-l", executable)).matchAll(
|
||||
/cmd LC_SEGMENT_64\n\s+cmdsize \d+\n\s+segname \S+\n\s+vmaddr (0x[0-9a-f]+)\n\s+vmsize 0x[0-9a-f]+\n\s+fileoff (\d+)\n\s+filesize (\d+)/gu,
|
||||
),
|
||||
]
|
||||
.map(([, vmaddr, fileoff, filesize]) => ({
|
||||
address: BigInt(vmaddr!),
|
||||
offset: BigInt(fileoff!),
|
||||
size: BigInt(filesize!),
|
||||
}))
|
||||
.find(
|
||||
(candidate) =>
|
||||
candidate.address <= address &&
|
||||
address < candidate.address + candidate.size,
|
||||
);
|
||||
expect(segment, `no file-backed segment maps ${HANDLER}`).toBeDefined();
|
||||
const fat = await tool("otool", "-f", "-v", executable);
|
||||
const slice = !fat.startsWith("Fat headers")
|
||||
? "0"
|
||||
: /^\s+offset (\d+)$/mu.exec(
|
||||
fat
|
||||
.split(/^architecture /mu)
|
||||
.find((block) => block.startsWith(`${architecture}\n`)) ?? "",
|
||||
)?.[1];
|
||||
expect(slice, `otool lacks the ${architecture} slice`).toBeDefined();
|
||||
return {
|
||||
address: `0x${address.toString(16)}`,
|
||||
file_offset: Number(
|
||||
BigInt(slice!) + segment!.offset + address - segment!.address,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
describe.skipIf(process.platform !== "darwin")(
|
||||
"compiled Objective-C UI handler locations",
|
||||
() => {
|
||||
cliTest(
|
||||
"locates the handler's code, not its method record, in thin and universal images",
|
||||
async ({ cli, processes }) => {
|
||||
const root = await createTestTempDirectory("rea-ui-handler-location-");
|
||||
const source = join(root, "handler.m");
|
||||
await writeFile(source, SOURCE);
|
||||
const { call } = await connectLocalToolsMcp(LOCAL_READERS);
|
||||
const good = async (name: string, arguments_: object) => {
|
||||
const result = await call(name, { ...arguments_ });
|
||||
expect(result.isError, JSON.stringify(result.content)).not.toBe(true);
|
||||
return result.structuredContent;
|
||||
};
|
||||
for (const variant of [
|
||||
// arm64 always uses relative method lists.
|
||||
{ name: "arm64", flags: ["-arch", "arm64"], encoding: "relative" },
|
||||
// A pre-macOS 11 x86_64 image keeps absolute method lists.
|
||||
{
|
||||
name: "x86_64",
|
||||
flags: ["-arch", "x86_64", "-mmacosx-version-min=10.15"],
|
||||
encoding: "absolute",
|
||||
},
|
||||
// The selected slice's offsets are relative to the container.
|
||||
{
|
||||
name: "universal",
|
||||
flags: [
|
||||
"-arch",
|
||||
"x86_64",
|
||||
"-arch",
|
||||
"arm64",
|
||||
"-mmacosx-version-min=10.15",
|
||||
],
|
||||
encoding: undefined,
|
||||
},
|
||||
]) {
|
||||
const directory = join(root, variant.name);
|
||||
await mkdir(directory);
|
||||
const compiled = await processes.run(
|
||||
"/usr/bin/xcrun",
|
||||
[
|
||||
"clang",
|
||||
...variant.flags,
|
||||
"-O2",
|
||||
"-framework",
|
||||
"Cocoa",
|
||||
source,
|
||||
"-o",
|
||||
join(directory, "Audit"),
|
||||
],
|
||||
{ timeoutMs: 60_000 },
|
||||
);
|
||||
expect(compiled.exitCode, compiled.stderr).toBe(0);
|
||||
const { app, executable } = await writeApp(
|
||||
directory,
|
||||
join(directory, "Audit"),
|
||||
);
|
||||
const before = await readFile(executable);
|
||||
|
||||
await good("open_binary", { path: app });
|
||||
const traced = traceEvidence.parse(
|
||||
await good("trace_native_ui_action", { action: "runAction:" }),
|
||||
);
|
||||
const metadata = metadataEvidence.parse(
|
||||
await good("inspect_native_dispatch_metadata", {}),
|
||||
);
|
||||
await good("close_binary", {});
|
||||
|
||||
const expected = await independentHandlerLocation(
|
||||
processes,
|
||||
executable,
|
||||
traced.subject.architecture,
|
||||
);
|
||||
const functionId = `native:function:${expected.address}`;
|
||||
const trace = traced.normalized_result;
|
||||
expect(trace.edges, variant.name).toContainEqual(
|
||||
expect.objectContaining({
|
||||
relation: "objc_dispatch",
|
||||
resolution: "resolved",
|
||||
to: functionId,
|
||||
}),
|
||||
);
|
||||
const handler = trace.nodes.find(({ id }) => id === functionId);
|
||||
expect(handler?.location, variant.name).toEqual(expected);
|
||||
|
||||
const record =
|
||||
metadata.normalized_result.result.objc_dispatch_implementations.find(
|
||||
({ class_name, selector }) =>
|
||||
class_name === "AuditController" && selector === "runAction:",
|
||||
);
|
||||
expect(record, variant.name).toMatchObject({
|
||||
implementation_address: expected.address,
|
||||
implementation_file_offset: expected.file_offset,
|
||||
});
|
||||
// The fixture is discriminating only while the record lies elsewhere.
|
||||
expect(record?.location.file_offset).not.toBe(expected.file_offset);
|
||||
expect(handler?.evidence, variant.name).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: "binary_metadata",
|
||||
description:
|
||||
variant.encoding === undefined
|
||||
? expect.any(String)
|
||||
: `Encoded ${variant.encoding} Objective-C method entry`,
|
||||
location: record?.location,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (variant.name === "universal") {
|
||||
const output = await cli.run({
|
||||
arguments: [
|
||||
"trace-native-ui-action",
|
||||
app,
|
||||
"runAction:",
|
||||
"--json",
|
||||
],
|
||||
environment: LOCAL_READERS,
|
||||
});
|
||||
expect(output.exitCode, output.stderr).toBe(0);
|
||||
const cliTrace = traceEvidence.parse(output.json).normalized_result;
|
||||
expect(cliTrace.nodes.find(({ id }) => id === functionId)).toEqual(
|
||||
handler,
|
||||
);
|
||||
}
|
||||
expect(Buffer.compare(await readFile(executable), before)).toBe(0);
|
||||
}
|
||||
},
|
||||
120_000,
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -533,6 +533,11 @@ const assertSuccessfulRoute = (
|
||||
to: `native:function:${handlerAddress}`,
|
||||
}),
|
||||
);
|
||||
// A provider symbol names the handler address but maps no file bytes.
|
||||
expect(
|
||||
trace.nodes.find(({ id }) => id === `native:function:${handlerAddress}`)
|
||||
?.location,
|
||||
).toEqual({ address: handlerAddress, file_offset: null });
|
||||
expect(calls).toContain("decode_interface_builder");
|
||||
expect(calls).toContain(
|
||||
scenario.name === "authored-ui-direct"
|
||||
|
||||
Reference in new issue
Block a user