Files
rea/tests/boundary/providers/native/nativeCallObservation.test.ts
T
3e8a5f76d0 feat(native): retain LLDB source coordinates and verify Pascal calls (#2800)
* feat(native): retain LLDB source coordinates and verify Pascal call entries

Preserve partial line-table attribution for native locations and historical
evidence compatibility. Verify owned Free Pascal call entries through CLI/MCP.

Refs #2382, #2372, #2397

Co-Authored-By: Codex <[email protected]>

* docs: namespace native specifications by domain and feature

Use stable descriptive paths instead of numeric prefixes to avoid parallel contribution collisions.

Co-Authored-By: Codex <[email protected]>

* fix(native): bound Pascal verification and compare caller coordinates

Address review findings at their producing boundaries and preserve directly affected consumers. Source inspection and git diff --check completed. Tests and automated checks were not run at the user’s explicit request.

* fix(native): bound compiler shutdown and source-coordinate fixtures

* docs: clarify historical native source verification

* fix(native): preserve source attribution and compiler ownership

* docs(native): qualify retained source attribution

* fix(ci): clean up native Pascal verifier processes

* test(native): type the Pascal verifier helper import after merging main

Main now typechecks this boundary test, which imports the untyped
canonical-temporary-directory helper and reads an unknown catch value.
Add the helper's declaration file and narrow the caught error.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test(native): verify partial source attribution caveat

* fix(native): retain verifier compiler cleanup ownership

* fix(native): forward verifier cancellation and measure path bytes

CLI and MCP artifact runs observe the verifier abort signal, and a
failed Pascal verifier test stops the compiler group before deleting
its directory. Repeated source locations grow by measured UTF-8 JSON
bytes; workflow tokens, CPU and peak memory stay unmeasured.

---------

Co-authored-by: Matheus Breguêz <[email protected]>
Co-authored-by: Codex <[email protected]>
Co-authored-by: Kaoru0822 <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Cursor Agent <[email protected]>
2026-11-07 23:25:20 +08:00

615 lines
20 KiB
TypeScript

import { createHash } from "node:crypto";
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
AnalysisCapabilityUnavailableError,
AnalysisCancelledError,
AnalysisInputError,
} from "../../../../src/domain/analysisErrorCore.js";
import type { AnalysisError } from "../../../../src/domain/analysisErrorBase.js";
import { EvidenceIntegrityError } from "../../../../src/domain/evidenceErrors.js";
import { nativeCallPartialObservationSchema } from "../../../../src/domain/native/nativeCallPartialObservation.js";
import { nativeCallObservationResultSchema } from "../../../../src/domain/native/nativeCallObservation.js";
import {
NATIVE_CALL_BREAKPOINT_LOCATION_BUDGET_LIMITATION,
NATIVE_CALL_BREAKPOINT_LOCATION_LIST_INCOMPLETE_LIMITATION,
} from "../../../../src/native/NativeCallObservation.js";
import { err, ok, type Result } from "../../../../src/domain/result.js";
import type {
NativeCallTrace,
NativeCallTracer,
} from "../../../../src/native/LldbCallTracer.js";
import { NativeMacOSProvider } from "../../../../src/native/NativeMacOSProvider.js";
import {
NativeFixtureRunner,
nativeMachoTarget,
} from "../../../fixtures/nativeCommands.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
const LOCATION = {
load_address: "0x100000928",
file_address: "0x100000928",
module: "Fixture",
module_path: "/tmp/Fixture",
symbol: "-[Greeter greet:times:]",
source_location: {
file_path: "/not-acquired/é space/fixture.pas",
line: 23,
column: null,
},
};
const run = (
overrides: Partial<NativeCallTrace["run"]> = {},
): NativeCallTrace["run"] => ({
status: "traced",
version: "lldb-2100.0.17.203",
pid: 4242,
outcome: "exited",
resource_limit_reached: false,
target_output: {
stdout_bytes: 0,
stderr_bytes: 0,
stdout_truncated: false,
stderr_truncated: false,
stdout_complete: true,
stderr_complete: true,
},
exit_status: 0,
exit_description: null,
killed: false,
terminated: true,
target_identity: {
loaded_image_sha256: null,
selected_file_sha256: "a".repeat(64),
file_device: "1",
file_inode: "2",
module_path: "/tmp/Fixture",
module_uuid: "fixture-uuid",
stable: true,
},
elapsed_ms: 12.5,
breakpoints: [
{ index: 0, location_count: 1, locations: [LOCATION] },
{ index: 1, location_count: 1, locations: [LOCATION] },
],
events: [
{
sequence: 0,
elapsed_ms: 3,
thread_id: 7,
breakpoint_index: 0,
...LOCATION,
receiver_class: "Greeter",
selector: "greet:times:",
registers: [
{ name: "x0", value: "0x6000" },
{ name: "x1", value: "0x7000" },
],
backtrace: [{ ...LOCATION, source_location: null }],
},
],
other_stops: [],
...overrides,
});
/** Records the request and returns a canned run or failure. */
class FixtureTracer implements NativeCallTracer {
readonly requests: Parameters<NativeCallTracer["trace"]>[0][] = [];
constructor(
private readonly outcome: Result<NativeCallTrace, AnalysisError> = ok({
debugger: {
path: "/usr/bin/lldb",
sha256: "a".repeat(64),
version: "lldb-2100.0.17.203",
},
run: run(),
stdout: { text: "hello\n", bytes: 6, truncated: false, complete: true },
stderr: { text: "", bytes: 0, truncated: false, complete: true },
terminated: true,
}),
) {}
trace(request: Parameters<NativeCallTracer["trace"]>[0]) {
this.requests.push(request);
return Promise.resolve(this.outcome);
}
close() {
return Promise.resolve(ok(null));
}
}
const BREAKPOINTS = [
{ kind: "objc-method", class_name: "Greeter", selector: "greet:times:" },
{ kind: "function", name: "rea_add" },
];
const fixtureTarget = async () => {
const directory = await createTestTempDirectory("rea-native-calls-");
const path = join(directory, "Fixture");
await writeFile(path, "fixture executable");
return {
...nativeMachoTarget(path),
sha256: createHash("sha256").update("fixture executable").digest("hex"),
};
};
const observe = async (
tracer: NativeCallTracer,
parameters: Record<string, unknown>,
target?: Awaited<ReturnType<typeof fixtureTarget>>,
signal?: AbortSignal,
) =>
new NativeMacOSProvider({}, new NativeFixtureRunner(), "darwin", () => tracer)
.createClient(target ?? (await fixtureTarget()))
.execute(
"observe_native_calls",
// The provider parses JSON-like parameters itself.
JSON.parse(JSON.stringify(parameters)),
signal === undefined ? undefined : { signal },
);
describe("observe_native_calls projection: projects completed observations", () => {
it("launches the active Mach-O through the tracer and projects its run", async () => {
const tracer = new FixtureTracer();
const target = await fixtureTarget();
const observed = await observe(
tracer,
{
breakpoints: BREAKPOINTS,
arguments: ["--flag"],
environment: { REA_CASE: "selected" },
},
target,
);
if (!observed.ok) throw observed.error;
expect(tracer.requests[0]).toMatchObject({
executable: target.path,
architecture: "arm64",
input: {
arguments: ["--flag"],
environment: { REA_CASE: "selected" },
duration_ms: 10_000,
max_events: 1_000,
argument_registers: 4,
backtrace_frames: 0,
},
});
const result = nativeCallObservationResultSchema.parse(
observed.value.result,
);
expect(result.target.environment).toEqual({ REA_CASE: "selected" });
expect(result.breakpoints.map(({ request }) => request)).toEqual([
{
kind: "objc-method",
class_name: "Greeter",
selector: "greet:times:",
method_type: "any",
},
{ kind: "function", name: "rea_add", module: null },
]);
expect(result.process).toMatchObject({
pid: 4242,
outcome: "exited",
exit_status: 0,
terminated: true,
stdout: { text: "hello\n" },
});
expect(result.target.launch_identity).toEqual({
file_device: "1",
file_inode: "2",
loaded_image_sha256: null,
selected_file_sha256: "a".repeat(64),
module_path: "/tmp/Fixture",
module_uuid: "fixture-uuid",
stable: true,
});
expect(result.events[0]).toMatchObject({
receiver_class: "Greeter",
selector: "greet:times:",
source_location: LOCATION.source_location,
backtrace: [{ ...LOCATION, source_location: null }],
});
expect(result.breakpoints[0]?.locations[0]?.source_location).toEqual(
LOCATION.source_location,
);
expect(result.coverage).toEqual({
status: "complete",
event_limit_reached: false,
resource_limit_reached: false,
unresolved_breakpoints: [],
});
expect(observed.value.locations).toEqual([
{ kind: "artifact-path", path: target.path },
]);
});
});
describe("observe_native_calls projection: keeps locations within the metadata budget", () => {
it("projects every breakpoint location when the aggregate metadata budget allows it", async () => {
const locations = Array.from({ length: 65 }, (_, index) => ({
...LOCATION,
load_address: `0x${(0x100000928 + index * 4).toString(16)}`,
file_address: `0x${(0x100000928 + index * 4).toString(16)}`,
}));
const observed = await observe(
new FixtureTracer(
ok({
debugger: {
path: "/usr/bin/lldb",
sha256: "a".repeat(64),
version: null,
},
run: run({
breakpoints: [
{ index: 0, location_count: locations.length, locations },
{ index: 1, location_count: 1, locations: [LOCATION] },
],
}),
stdout: { text: "", bytes: 0, truncated: false, complete: true },
stderr: { text: "", bytes: 0, truncated: false, complete: true },
terminated: true,
}),
),
{ breakpoints: BREAKPOINTS },
);
if (!observed.ok) throw observed.error;
const result = nativeCallObservationResultSchema.parse(
observed.value.result,
);
expect(result.breakpoints[0]?.location_count).toBe(65);
expect(result.breakpoints[0]?.locations).toHaveLength(65);
expect(result.coverage).toEqual({
status: "complete",
event_limit_reached: false,
resource_limit_reached: false,
unresolved_breakpoints: [],
});
});
it("uses a generic warning when locations are missing without a budget signal", async () => {
const observed = await observe(
new FixtureTracer(
ok({
debugger: {
path: "/usr/bin/lldb",
sha256: "a".repeat(64),
version: null,
},
run: run({
breakpoints: [
{ index: 0, location_count: 2, locations: [LOCATION] },
{ index: 1, location_count: 1, locations: [LOCATION] },
],
}),
stdout: { text: "", bytes: 0, truncated: false, complete: true },
stderr: { text: "", bytes: 0, truncated: false, complete: true },
terminated: true,
}),
),
{ breakpoints: BREAKPOINTS },
);
if (!observed.ok) throw observed.error;
const result = nativeCallObservationResultSchema.parse(
observed.value.result,
);
expect(result.limitations).toContain(
NATIVE_CALL_BREAKPOINT_LOCATION_LIST_INCOMPLETE_LIMITATION,
);
});
});
describe("observe_native_calls projection: reports partial observation coverage", () => {
it("retains normal exit and location counts when breakpoint metadata is bounded", async () => {
const observed = await observe(
new FixtureTracer(
ok({
debugger: {
path: "/usr/bin/lldb",
sha256: "a".repeat(64),
version: null,
},
run: run({
breakpoint_locations_truncated: true,
breakpoints: [
{ index: 0, location_count: 64, locations: [LOCATION] },
{ index: 1, location_count: 64, locations: [] },
],
}),
stdout: { text: "", bytes: 0, truncated: false, complete: true },
stderr: { text: "", bytes: 0, truncated: false, complete: true },
terminated: true,
}),
),
{ breakpoints: BREAKPOINTS },
);
if (!observed.ok) throw observed.error;
const result = nativeCallObservationResultSchema.parse(
observed.value.result,
);
expect(result.process).toMatchObject({
outcome: "exited",
exit_status: 0,
terminated: true,
});
expect(
result.breakpoints.map(({ location_count }) => location_count),
).toEqual([64, 64]);
expect(result.coverage).toMatchObject({
status: "partial",
breakpoint_locations_truncated: true,
resource_limit_reached: false,
unresolved_breakpoints: [],
});
expect(result.limitations).toContain(
NATIVE_CALL_BREAKPOINT_LOCATION_BUDGET_LIMITATION,
);
expect(result.limitations).not.toContain(
NATIVE_CALL_BREAKPOINT_LOCATION_LIST_INCOMPLETE_LIMITATION,
);
expect(result.limitations.join(" ")).not.toContain("process was killed");
});
it("marks event limits, unresolved breakpoints and unconfirmed exits", async () => {
const tracer = new FixtureTracer(
ok({
debugger: {
path: "/usr/bin/lldb",
sha256: "a".repeat(64),
version: null,
},
run: run({
outcome: "event-limit",
exit_status: null,
killed: true,
terminated: false,
breakpoints: [
{ index: 0, location_count: 1, locations: [LOCATION] },
{ index: 1, location_count: 0, locations: [] },
],
}),
stdout: { text: "", bytes: 0, truncated: false, complete: false },
stderr: {
text: "x".repeat(16),
bytes: 2_000_000,
truncated: true,
complete: true,
},
terminated: true,
}),
);
const observed = await observe(tracer, {
breakpoints: BREAKPOINTS,
max_events: 1,
});
if (!observed.ok) throw observed.error;
const result = nativeCallObservationResultSchema.parse(
observed.value.result,
);
expect(result.coverage).toEqual({
status: "partial",
event_limit_reached: true,
resource_limit_reached: false,
unresolved_breakpoints: [1],
});
expect(result.process.terminated).toBe(false);
expect(result.limitations.join(" ")).not.toContain("was killed");
expect(result.limitations).toContain(
"Target output draining did not complete; reported byte counts are observed lower bounds, not stream totals.",
);
expect(result.limitations).toEqual(
expect.arrayContaining([
"Observation stopped at max_events (1); later calls were not recorded and termination was requested but could not be confirmed.",
"Breakpoints 1 matched no code in any image loaded while observing; their calls could not be observed.",
"Target output beyond 1048576 bytes (1 MiB) per stream is counted but not kept. This release has no runtime override.",
"REA could not confirm that process 4242 exited; check for it before relying on host state.",
]),
);
});
it("marks duration and aggregate resource limits as partial coverage", async () => {
for (const outcome of ["duration-elapsed", "resource-limit"] as const) {
const tracer = new FixtureTracer(
ok({
debugger: {
path: "/usr/bin/lldb",
sha256: "a".repeat(64),
version: null,
},
run: run({
outcome,
resource_limit_reached: outcome === "resource-limit",
}),
stdout: { text: "", bytes: 0, truncated: false, complete: true },
stderr: { text: "", bytes: 0, truncated: false, complete: true },
terminated: true,
}),
);
const observed = await observe(tracer, { breakpoints: BREAKPOINTS });
if (!observed.ok) throw observed.error;
const result = nativeCallObservationResultSchema.parse(
observed.value.result,
);
expect(result.coverage).toMatchObject({
status: "partial",
resource_limit_reached: outcome === "resource-limit",
});
}
});
});
describe("observe_native_calls projection: preserves unknown launch identity", () => {
it("preserves unknown launch identity without claiming digest binding", async () => {
const tracer = new FixtureTracer(
ok({
debugger: {
path: "/usr/bin/lldb",
sha256: "a".repeat(64),
version: null,
},
run: run({
target_identity: {
loaded_image_sha256: null,
selected_file_sha256: null,
file_device: null,
file_inode: null,
module_path: null,
module_uuid: null,
stable: false,
},
}),
stdout: { text: "", bytes: 0, truncated: false, complete: true },
stderr: { text: "", bytes: 0, truncated: false, complete: true },
terminated: true,
}),
);
const observed = await observe(tracer, { breakpoints: BREAKPOINTS });
if (!observed.ok) throw observed.error;
const result = nativeCallObservationResultSchema.parse(
observed.value.result,
);
expect(result.target.launch_identity).toEqual({
file_device: null,
file_inode: null,
loaded_image_sha256: null,
selected_file_sha256: null,
module_path: null,
module_uuid: null,
stable: false,
});
expect(result.limitations).toContain(
"LLDB did not provide enough module identity evidence to confirm pathname and module continuity around launch.",
);
});
});
describe("observe_native_calls failures", () => {
it("rejects malformed input before launching anything", async () => {
for (const parameters of [
{ breakpoints: [] },
{ breakpoints: [{ kind: "objc-method", selector: "-[A b]" }] },
{ breakpoints: BREAKPOINTS, environment: { "A=B": "x" } },
{ breakpoints: BREAKPOINTS, argument_registers: 9 },
]) {
const tracer = new FixtureTracer();
const observed = await observe(tracer, parameters);
expect(observed.ok).toBe(false);
if (!observed.ok)
expect(observed.error).toBeInstanceOf(AnalysisInputError);
expect(tracer.requests).toEqual([]);
}
});
it("refuses a changed or non-Mach-O target", async () => {
const tracer = new FixtureTracer();
const target = await fixtureTarget();
const changed = await observe(
tracer,
{ breakpoints: BREAKPOINTS },
{ ...target, sha256: "f".repeat(64) },
);
expect(!changed.ok && changed.error).toBeInstanceOf(EvidenceIntegrityError);
const database = await new NativeMacOSProvider(
{},
new NativeFixtureRunner(),
"darwin",
() => tracer,
)
.createClient({
path: target.path,
sha256: target.sha256,
kind: "database",
format: "analysis-database",
})
.execute("observe_native_calls", { breakpoints: [] });
expect(!database.ok && database.error).toBeInstanceOf(
AnalysisCapabilityUnavailableError,
);
expect(tracer.requests).toEqual([]);
});
it("keeps the tracer's failure reason and caller cancellation", async () => {
const denied = new AnalysisCapabilityUnavailableError(
"native-macos",
"observe_native_calls",
"debugger-attach-denied: attach failed",
);
const failed = await observe(new FixtureTracer(err(denied)), {
breakpoints: BREAKPOINTS,
});
expect(!failed.ok && failed.error).toBe(denied);
const controller = new AbortController();
controller.abort();
const cancelled = await observe(
new FixtureTracer(),
{ breakpoints: BREAKPOINTS },
undefined,
controller.signal,
);
expect(!cancelled.ok && cancelled.error).toBeInstanceOf(
AnalysisCancelledError,
);
});
});
it("retains completed call facts when cancellation arrives with the tracer result", async () => {
const controller = new AbortController();
const reason = { caller: "completed-trace-consumption" };
const recorded = new FixtureTracer();
const result = await observe(
{
trace: async (request) => {
const result = await recorded.trace(request);
controller.abort(reason);
return result;
},
close: () => recorded.close(),
},
{ breakpoints: BREAKPOINTS },
undefined,
controller.signal,
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected completion cancellation");
expect(result.error).toMatchObject({
_tag: "AnalysisCancelledError",
cause: reason,
});
const partial = nativeCallPartialObservationSchema.parse(
result.error.partialObservation,
);
expect(partial.events).toHaveLength(1);
expect(partial.process.stdout?.text).toBe("hello\n");
expect(partial.process.terminated).toBe(true);
expect(partial.coverage).toMatchObject({
status: "partial",
reason: "cancelled",
});
});
it("grows repeated source-location JSON by the location object, not by a token estimate", () => {
const path = "/not-acquired/é space/fixture.pas";
const located = {
...LOCATION,
source_location: { file_path: path, line: 23, column: null },
};
const absent = { ...located, source_location: null };
const copies = 3;
const withPath = Buffer.byteLength(
JSON.stringify(Array.from({ length: copies }, () => located)),
);
const withoutPath = Buffer.byteLength(
JSON.stringify(Array.from({ length: copies }, () => absent)),
);
const perCopy =
Buffer.byteLength(JSON.stringify(located.source_location)) -
Buffer.byteLength("null");
expect(withPath - withoutPath).toBe(copies * perCopy);
expect(perCopy).toBeGreaterThan(Buffer.byteLength(path));
});