fix(native): keep bounded trace edges and depth consistent (#2758)

This commit is contained in:
Iven Koo 2026-10-13 20:06:50 +08:00 committed by GitHub
parent 68ecdcd7a1
commit f90382756d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 63 additions and 4 deletions

View File

@ -83,6 +83,61 @@ const graph = nativeInvestigationGraphSchema.parse({
});
describe("native investigation graph traces", () => {
it.each(["forward", "backward"] as const)(
"keeps both endpoints of retained edges within the %s node budget",
(direction) => {
const trace = traceNativeInvestigationGraph(graph, {
start: direction === "forward" ? "a" : "c",
direction,
limits: { max_nodes: 1 },
});
expect(trace.nodes).toHaveLength(1);
expect(trace.edges).toEqual([]);
expect(trace).toMatchObject({
truncated: true,
reason: "max_nodes_reached",
reached_depth: 0,
});
},
);
it.each([{ max_nodes: 2 }, { max_edges: 1 }])(
"reports retained node depth when a sibling hits the budget %j",
(limits) => {
const branching = {
...graph,
edges: graph.edges
.filter((edge) => edge.resolution !== "unresolved")
.map((edge) => ({ ...edge, from: "a" })),
};
const trace = traceNativeInvestigationGraph(branching, {
start: "a",
limits,
});
expect(trace.nodes.map(({ id }) => id)).toEqual(["a", "b"]);
expect(trace.edges.map(({ id }) => id)).toEqual(["1"]);
expect(trace.reached_depth).toBe(1);
expect(trace.truncated).toBe(true);
},
);
it("retains cycle edges to admitted nodes when the node budget is full", () => {
const cycle = {
...graph,
nodes: graph.nodes.slice(0, 2),
edges: [
graph.edges[0],
{ ...graph.edges[0], id: "back", from: "b", to: "a" },
],
};
const trace = traceNativeInvestigationGraph(
nativeInvestigationGraphSchema.parse(cycle),
{ start: "a", limits: { max_nodes: 2 } },
);
expect(trace.edges.map(({ id }) => id)).toEqual(["1", "back"]);
expect(trace).toMatchObject({ reached_depth: 1, truncated: false });
});
it("retains observed and inferred route edges plus unresolved dispatch", () => {
const trace = traceNativeInvestigationGraph(graph, { start: "a" });
expect(trace.nodes.map(({ id }) => id)).toEqual(["a", "b", "c"]);

View File

@ -207,7 +207,6 @@ export const traceNativeInvestigationGraph = (
while (queue.length > 0) {
const current = queue.shift();
if (current === undefined) break;
reachedDepth = Math.max(reachedDepth, current.depth);
const adjacent = adjacency.get(current.id) ?? [];
for (const edge of adjacent) {
@ -227,19 +226,24 @@ export const traceNativeInvestigationGraph = (
reason ??= "max_depth_reached";
continue;
}
selectedEdges.set(edge.id, edge);
if (alreadyVisited) continue;
if (visited.size >= limits.max_nodes) {
// Check destination capacity before retaining its edge. Otherwise the node
// bound leaves a resolved edge pointing outside the returned trace.
if (!alreadyVisited && visited.size >= limits.max_nodes) {
truncated = true;
reason = "max_nodes_reached";
break;
}
selectedEdges.set(edge.id, edge);
if (alreadyVisited) continue;
if (!nodeById.has(nextId)) {
truncated = true;
reason ??= "edge_target_missing_from_graph";
continue;
}
visited.add(nextId);
// A retained node has already been reached even if a later sibling
// exhausts a budget before this node is dequeued for expansion.
reachedDepth = Math.max(reachedDepth, current.depth + 1);
queue.push({ id: nextId, depth: current.depth + 1 });
}
if (reason === "max_edges_reached" || reason === "max_nodes_reached") break;