mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 01:45:32 +00:00
fix(native): preserve bridge failure types during cleanup
This commit is contained in:
1 parent
7c323870c2
commit
1d0baa352f
4 files changed
+165
-21
No files matched your search
@@ -127,7 +127,8 @@ export const nativeCallEventSchema = z.strictObject({
|
||||
backtrace: z.array(nativeCodeLocationSchema),
|
||||
});
|
||||
|
||||
const capturedOutputSchema = z.strictObject({
|
||||
/** Retained native output with explicit stream completeness and byte counts. */
|
||||
export const nativeCapturedOutputSchema = z.strictObject({
|
||||
text: z.string(),
|
||||
/** Bytes observed by the bridge, not a total when complete is false. */
|
||||
bytes: z.number().int().nonnegative(),
|
||||
@@ -177,8 +178,8 @@ export const nativeCallObservationResultSchema = z.strictObject({
|
||||
/** REA confirmed the process is gone when observation ended. */
|
||||
terminated: z.boolean(),
|
||||
elapsed_ms: z.number().nonnegative(),
|
||||
stdout: capturedOutputSchema,
|
||||
stderr: capturedOutputSchema,
|
||||
stdout: nativeCapturedOutputSchema,
|
||||
stderr: nativeCapturedOutputSchema,
|
||||
/** Signal and exception stops LLDB reported; the process continued past each. */
|
||||
other_stops: z.array(z.string()),
|
||||
}),
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import { nativeCallEventSchema } from "./nativeCallObservation.js";
|
||||
|
||||
const capturedOutputSchema = z.strictObject({
|
||||
text: z.string(),
|
||||
/** Bytes observed so far; the final stream size may be unknown. */
|
||||
bytes: z.number().int().nonnegative(),
|
||||
truncated: z.boolean(),
|
||||
complete: z.boolean(),
|
||||
});
|
||||
import {
|
||||
nativeCallEventSchema,
|
||||
nativeCapturedOutputSchema,
|
||||
} from "./nativeCallObservation.js";
|
||||
|
||||
/** Native call evidence retained when observation fails before a full result. */
|
||||
export const nativeCallPartialObservationSchema = z.strictObject({
|
||||
@@ -22,8 +17,8 @@ export const nativeCallPartialObservationSchema = z.strictObject({
|
||||
}),
|
||||
process: z.strictObject({
|
||||
pid: z.number().int().positive().nullable(),
|
||||
stdout: capturedOutputSchema.nullable(),
|
||||
stderr: capturedOutputSchema.nullable(),
|
||||
stdout: nativeCapturedOutputSchema.nullable(),
|
||||
stderr: nativeCapturedOutputSchema.nullable(),
|
||||
other_stops: z.array(z.string()),
|
||||
}),
|
||||
debugger: z.strictObject({ version: z.string().nullable() }),
|
||||
|
||||
@@ -163,11 +163,16 @@ const ATTACH_REMEDIATION =
|
||||
|
||||
/** Production tracer: `lldb --batch` running the REA LLDB bridge. */
|
||||
export class LldbCallTracer implements NativeCallTracer {
|
||||
constructor(
|
||||
private readonly launch: typeof runLldb = runLldb,
|
||||
private readonly resolveTool: typeof resolveXcrunTool = resolveXcrunTool,
|
||||
) {}
|
||||
|
||||
async trace(
|
||||
request: Parameters<NativeCallTracer["trace"]>[0],
|
||||
signal?: AbortSignal,
|
||||
): Promise<Result<NativeCallTrace, AnalysisError>> {
|
||||
const lldb = await resolveXcrunTool("lldb", signal);
|
||||
const lldb = await this.resolveTool("lldb", signal);
|
||||
if (!lldb.ok)
|
||||
return err(
|
||||
lldb.error.reason === "cancelled"
|
||||
@@ -235,7 +240,7 @@ export class LldbCallTracer implements NativeCallTracer {
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
await prepareProcessOwnershipInspection(signal);
|
||||
const exited = await runLldb(
|
||||
const exited = await this.launch(
|
||||
lldb.path,
|
||||
[
|
||||
"--batch",
|
||||
@@ -333,6 +338,10 @@ export class LldbCallTracer implements NativeCallTracer {
|
||||
},
|
||||
),
|
||||
);
|
||||
if (output !== undefined && output.status !== "traced")
|
||||
return err(
|
||||
tracerFailure(output, lifecycleCleanup, await getPartialObservation()),
|
||||
);
|
||||
if (exited.cleanupFailure !== undefined)
|
||||
return err(
|
||||
new ProviderAdapterError(PROVIDER, OPERATION, {
|
||||
@@ -367,11 +376,6 @@ export class LldbCallTracer implements NativeCallTracer {
|
||||
: { cleanup: lifecycleCleanup }),
|
||||
}),
|
||||
);
|
||||
if (output.status !== "traced") {
|
||||
return err(
|
||||
tracerFailure(output, lifecycleCleanup, await getPartialObservation()),
|
||||
);
|
||||
}
|
||||
const stdout = await capturedOutput(
|
||||
paths.stdoutCapture,
|
||||
output.target_output.stdout_bytes,
|
||||
@@ -417,6 +421,7 @@ export class LldbCallTracer implements NativeCallTracer {
|
||||
const quote = (value: string): string =>
|
||||
`"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
||||
|
||||
/** Preserve a decoded bridge failure's type independently of cleanup outcome. */
|
||||
const tracerFailure = (
|
||||
output: {
|
||||
readonly status:
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { access, readFile, writeFile } from "node:fs/promises";
|
||||
import { dirname } from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { AnalysisCancelledError } from "../../../../src/domain/analysisErrorCore.js";
|
||||
import { EvidenceIntegrityError } from "../../../../src/domain/evidenceErrors.js";
|
||||
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
|
||||
import { nativeCallObservationInputSchema } from "../../../../src/domain/native/nativeCallObservation.js";
|
||||
import type { NativeCallTracer } from "../../../../src/native/LldbCallTracer.js";
|
||||
import { LldbCallTracer } from "../../../../src/native/LldbCallTracer.js";
|
||||
import { ok } from "../../../../src/domain/result.js";
|
||||
|
||||
const EVENT = {
|
||||
sequence: 0,
|
||||
elapsed_ms: 1,
|
||||
thread_id: 2,
|
||||
breakpoint_index: 0,
|
||||
load_address: "0x1000",
|
||||
file_address: "0x1000",
|
||||
module: "fixture",
|
||||
module_path: "/tmp/fixture",
|
||||
symbol: "main",
|
||||
receiver_class: null,
|
||||
selector: null,
|
||||
registers: [],
|
||||
backtrace: [],
|
||||
};
|
||||
|
||||
const request: Parameters<NativeCallTracer["trace"]>[0] = {
|
||||
executable: "/tmp/fixture",
|
||||
architecture: "arm64",
|
||||
expectedSha256: "a".repeat(64),
|
||||
input: nativeCallObservationInputSchema.parse({
|
||||
breakpoints: [{ kind: "function", name: "main" }],
|
||||
arguments: ["--fixture"],
|
||||
environment: { CASE: "retained" },
|
||||
}),
|
||||
};
|
||||
|
||||
const fakeTool = { path: "/usr/bin/lldb", sha256: "b".repeat(64) };
|
||||
const configSchema = z.object({
|
||||
result_path: z.string(),
|
||||
observation_path: z.string(),
|
||||
stdout_capture_path: z.string(),
|
||||
});
|
||||
|
||||
const configFromArguments = async (arguments_: readonly string[]) => {
|
||||
const command = arguments_.at(-1);
|
||||
const match =
|
||||
command === undefined ? null : /^rea_trace (.+)$/u.exec(command);
|
||||
if (match?.[1] === undefined)
|
||||
throw new Error("fake LLDB launcher did not receive a tracer config path");
|
||||
const raw: unknown = JSON.parse(await readFile(match[1], "utf8"));
|
||||
return { config: configSchema.parse(raw), configPath: match[1] };
|
||||
};
|
||||
|
||||
const fixtureTracer = (
|
||||
outcome: "cancelled" | "integrity-error-cleanup-failure",
|
||||
) => {
|
||||
let configPath: string | undefined;
|
||||
const tracer = new LldbCallTracer(
|
||||
async (_executable, arguments_) => {
|
||||
const resolved = await configFromArguments(arguments_);
|
||||
const { config } = resolved;
|
||||
configPath = resolved.configPath;
|
||||
await writeFile(
|
||||
config.observation_path,
|
||||
`${JSON.stringify({ kind: "event", event: EVENT })}\n`,
|
||||
);
|
||||
await writeFile(config.stdout_capture_path, "captured prefix");
|
||||
if (outcome === "integrity-error-cleanup-failure")
|
||||
await writeFile(
|
||||
config.result_path,
|
||||
JSON.stringify({
|
||||
status: "target-integrity-error",
|
||||
error: "launched target digest changed",
|
||||
}),
|
||||
);
|
||||
return outcome === "cancelled"
|
||||
? {
|
||||
kind: "cancelled",
|
||||
targetIdentity: undefined,
|
||||
cleanupFailure: undefined,
|
||||
}
|
||||
: {
|
||||
kind: "exited",
|
||||
exitCode: 1,
|
||||
output: "bridge reported target integrity failure",
|
||||
targetIdentity: undefined,
|
||||
cleanupFailure: "LLDB process-group cleanup could not be verified",
|
||||
};
|
||||
},
|
||||
async () => ok(fakeTool),
|
||||
);
|
||||
return {
|
||||
tracer,
|
||||
runtimeRoot: async () => {
|
||||
if (configPath === undefined)
|
||||
throw new Error("fake LLDB launcher was not called");
|
||||
const root = dirname(configPath);
|
||||
await expect(access(root)).rejects.toThrow();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe("LLDB failure retention through the production tracer", () => {
|
||||
it("preserves cancellation and journal/output evidence after runtime cleanup", async () => {
|
||||
const fixture = fixtureTracer("cancelled");
|
||||
const result = await fixture.tracer.trace(request);
|
||||
await fixture.runtimeRoot();
|
||||
if (result.ok) throw new Error("expected cancellation");
|
||||
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(result.error.partialObservation).toMatchObject({
|
||||
coverage: { status: "partial", reason: "cancelled" },
|
||||
events: [EVENT],
|
||||
process: { stdout: { text: "captured prefix", complete: false } },
|
||||
target: {
|
||||
arguments: ["--fixture"],
|
||||
environment: { CASE: "retained" },
|
||||
working_directory: null,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves bridge integrity failure type when process-group cleanup also fails", async () => {
|
||||
const fixture = fixtureTracer("integrity-error-cleanup-failure");
|
||||
const result = await fixture.tracer.trace(request);
|
||||
await fixture.runtimeRoot();
|
||||
if (result.ok) throw new Error("expected integrity failure");
|
||||
expect(result.error).toBeInstanceOf(EvidenceIntegrityError);
|
||||
expect(result.error.partialObservation).toMatchObject({
|
||||
coverage: { status: "partial", reason: "cleanup-failure" },
|
||||
events: [EVENT],
|
||||
process: { stdout: { text: "captured prefix", complete: false } },
|
||||
});
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "cleanup_incomplete",
|
||||
details: { execution_failure: "evidence_integrity_mismatch" },
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user