fix(native): retain completed LLDB facts across output-close cancellation

This commit is contained in:
morluto committed 2026-10-24 03:16:30 +08:00
1 parent f3df653d9b
commit ef64631f07
2 files changed
+108 -1

No files matched your search

+3 -1
View File
@@ -1453,7 +1453,9 @@ const runLldb = async (
const snapshot = supervisor.snapshot();
const interruption = deadline.interruption;
return await finish(
interruption === undefined
// Closed output with an observed exit owns a physical result. Read it
// before the outer success-only cancellation projection retains facts.
interruption === undefined || supervisor.observedExit() !== undefined
? {
kind: "exited",
exitCode: snapshot.exitCode ?? null,
@@ -606,6 +606,111 @@ describe("LLDB process exit while output is still open", () => {
});
describe("LLDB process exit at output-close loop completion", () => {
for (const abort of [false, true])
it.skipIf(process.platform === "win32")(
`retains completed trace facts after output closure (cancelled=${String(abort)})`,
async () => {
const fixtureRoot = await mkdtemp(
join(tmpdir(), "rea-lldb-complete-output-"),
);
const fakeLldb = join(fixtureRoot, "fake-lldb");
const completed = {
...successfulTracerOutput,
target_output: {
...successfulTracerOutput.target_output,
stdout_bytes: 2,
},
};
await writeFile(
fakeLldb,
`#!${process.execPath}
const fs = require("node:fs");
const { spawnSync } = require("node:child_process");
const command = process.argv.at(-1);
if (!command?.startsWith("rea_trace ")) process.exit(91);
const config = JSON.parse(fs.readFileSync(command.slice("rea_trace ".length), "utf8"));
const child = spawnSync(process.execPath, ["-e", ""]);
const result = ${JSON.stringify(completed)};
result.pid = child.pid;
fs.writeFileSync(config.pid_path, String(child.pid));
fs.writeFileSync(config.stdout_capture_path, "ok");
fs.writeFileSync(config.stderr_capture_path, "");
fs.writeFileSync(config.observation_path, JSON.stringify({ kind: "event", event: result.events[0] }) + "\\n");
fs.writeFileSync(config.result_path, JSON.stringify(result));
process.exit(0);
`,
{ mode: 0o700 },
);
const controller = new AbortController();
const reason = { requested: "stop after completed output closure" };
let interrupted = false;
const originalWait =
ProviderProcessSupervisor.prototype.waitForOutputClose;
const waitSpy = vi
.spyOn(ProviderProcessSupervisor.prototype, "waitForOutputClose")
.mockImplementation(async function (
this: ProviderProcessSupervisor,
timeoutMs?: number,
signal?: AbortSignal,
): Promise<boolean> {
const closed = await originalWait.call(this, timeoutMs, signal);
if (
abort &&
!interrupted &&
closed &&
this.observedExit()?.code === 0
) {
interrupted = true;
controller.abort(reason);
}
return closed;
});
const tracer = new LldbCallTracer({}, undefined, async () =>
ok({ path: fakeLldb, sha256: "e".repeat(64) }),
);
try {
const result = await tracer.trace(request, controller.signal);
expect(interrupted).toBe(abort);
if (result.ok) {
expect(abort).toBe(false);
expect(result.value.run).toMatchObject(completed);
expect(result.value.stdout).toMatchObject({
text: "ok",
bytes: 2,
complete: true,
});
} else {
expect(abort).toBe(true);
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
expect(result.error.cleanupIncomplete).toBe(false);
expect(
nativeCallPartialObservationSchema.parse(
result.error.partialObservation,
),
).toMatchObject({
coverage: { status: "partial", reason: "cancelled" },
debugger: { version: completed.version },
process: {
outcome: "exited",
exit_status: 0,
terminated: true,
elapsed_ms: 1,
stdout: { text: "ok", bytes: 2, complete: true },
},
target: { launch_identity: completed.target_identity },
breakpoints: [{ index: 0, location_count: 1 }],
events: [EVENT],
});
}
} finally {
waitSpy.mockRestore();
expect((await tracer.close()).ok).toBe(true);
await rm(fixtureRoot, { recursive: true, force: true });
}
},
);
it.skipIf(process.platform === "win32")(
"keeps a nonzero exit when output closure and cancellation meet at loop exit",
async () => {