fix(tracing): retain caller reasons at traversal checkpoints

This commit is contained in:
morluto
2026-10-24 03:04:36 +08:00
parent 7685deecf6
commit 48af28bf71
2 changed files with 66 additions and 4 deletions
+54
View File
@@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest";
import { traceCallPath } from "./CallPathTracing.js";
import { createAnalysisExecution } from "./AnalysisProvider.js";
import {
AnalysisCancelledError,
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
import { err, ok } from "../domain/result.js";
const input = { start: "entry", direction: "forward" as const };
const provider = { id: "fixture", name: "Fixture", version: "1" };
describe("call-path traversal cancellation ownership", () => {
for (const phase of ["selection", "relationship"] as const)
for (const reason of [null, { requested: "stop after provider reply" }])
it(`preserves the exact reason at the ${phase} continuation (${reason === null ? "null" : "object"})`, async () => {
const controller = new AbortController();
const result = await traceCallPath(
async () => {
if (phase === "selection") controller.abort(reason);
return ok("0x1000");
},
async () => {
controller.abort(reason);
return ok(createAnalysisExecution([], provider));
},
input,
controller.signal,
);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected cancellation");
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
expect(result.error.cause).toBe(reason);
});
it("preserves a selected provider failure when cancellation arrives with its reply", async () => {
const controller = new AbortController();
const failure = new AnalysisOutputError(
"procedure_callees",
"invalid fixture reply",
);
const result = await traceCallPath(
async () => ok("0x1000"),
async () => {
controller.abort({ requested: "stop" });
return err(failure);
},
input,
controller.signal,
);
expect(result).toEqual(err(failure));
});
});
+12 -4
View File
@@ -98,14 +98,18 @@ export const traceCallPath = async (
direction: input.direction,
};
if (isAborted(signal))
return err(new AnalysisCancelledError("trace_call_path"));
return err(
new AnalysisCancelledError("trace_call_path", { cause: signal?.reason }),
);
const state = createTraceState(resolvedInput);
while (
state.queueIndex < state.queue.length &&
state.traversalPath.length === 0
) {
if (isAborted(signal))
return err(new AnalysisCancelledError("trace_call_path"));
return err(
new AnalysisCancelledError("trace_call_path", { cause: signal?.reason }),
);
const address = state.queue[state.queueIndex++];
if (address === undefined) break;
const current = state.visited.get(address);
@@ -121,7 +125,9 @@ export const traceCallPath = async (
if (expanded.value) break;
}
if (isAborted(signal))
return err(new AnalysisCancelledError("trace_call_path"));
return err(
new AnalysisCancelledError("trace_call_path", { cause: signal?.reason }),
);
return ok(projectTrace(resolvedInput, state));
};
@@ -155,7 +161,9 @@ const expandAddress = async (
return result;
}
if (isAborted(signal))
return err(new AnalysisCancelledError("trace_call_path"));
return err(
new AnalysisCancelledError("trace_call_path", { cause: signal?.reason }),
);
if (!result.ok) {
state.failures.push({
address,