Skip to content

Commit 4fdadd5

Browse files
HumanBean17claude
andcommitted
fix: address review feedback on PR-TRACE-1a
Must-fix issues: - parent_edge_id now stores edge IDs (format: from_id:to_id:edge_type:hop) instead of node IDs, enabling proper tree reconstruction - Budget counting now only counts newly discovered nodes; moved increment after visited check Suggestions addressed: - test_trace_direction_required: now validates pydantic ValidationError - test_trace_inbound_callers_depth_2: removed vacuous >= 0 assertion - test_trace_parent_edge_id_chain: now verifies parent_edge_id format and validates it references an edge that reaches the current node - Tautological hop check: simplified to assert 0 in hops and hops <= {0, 1} - Removed hardcoded row_kind from _edge_attrs_for_row - Documented include_unresolved as 1a no-op (like prune_roles) Tests: 22 passed, 1 skipped Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent a9e6724 commit 4fdadd5

3 files changed

Lines changed: 2212 additions & 28 deletions

File tree

mcp_trace.py

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ def _edge_attrs_for_row(row: dict[str, Any]) -> dict[str, Any]:
108108
if k not in {"source_id", "other_id", "edge_type", "stored_edge_type"}
109109
and v not in (None, "")
110110
}
111-
attrs.setdefault("row_kind", "resolved")
112111
return attrs
113112

114113

@@ -372,8 +371,9 @@ def trace_v2(
372371
) -> TraceOutput:
373372
"""Multi-hop BFS traversal with pruning.
374373
375-
This is PR-TRACE-1a (core BFS engine). Pruning features are wired but
376-
treated as no-ops until PR-TRACE-1b implementation.
374+
This is PR-TRACE-1a (core BFS engine). Pruning features (prune_roles, fan_out_cap,
375+
collapse_trivial, include_unresolved) are wired but treated as no-ops until
376+
PR-TRACE-1b implementation.
377377
"""
378378
# Validate required parameters.
379379
if not direction:
@@ -479,8 +479,8 @@ def trace_v2(
479479
actual_depth = 0
480480
budget_hit = False
481481

482-
# Track incoming edge for each node (for parent_edge_id).
483-
node_to_incoming_edge: dict[str, TraceEdge] = {}
482+
# Track incoming edge ID for each node (for parent_edge_id).
483+
node_to_incoming_edge_id: dict[str, str] = {}
484484

485485
# For seed nodes, record them in nodes dict (always include seeds, filter doesn't apply).
486486
for sid in seed_ids:
@@ -524,24 +524,23 @@ def trace_v2(
524524
edges_by_parent: dict[str, list[TraceEdge]] = defaultdict(list)
525525

526526
for src_id, src_rows in by_source.items():
527-
parent_edge = node_to_incoming_edge.get(src_id)
528-
parent_edge_id = parent_edge.to_id if parent_edge else None
527+
parent_edge_id = node_to_incoming_edge_id.get(src_id)
529528

530529
for row in src_rows:
531530
other_id = str(row.get("other_id") or "")
532531
if not other_id:
533532
continue
534533

535-
# Check budget BEFORE counting.
534+
if other_id in visited:
535+
continue
536+
537+
# Check budget BEFORE counting (only counts newly discovered nodes).
536538
if total_discovered >= max_nodes_discovered:
537539
budget_hit = True
538540
break
539541

540542
total_discovered += 1
541543

542-
if other_id in visited:
543-
continue
544-
545544
# Load target node.
546545
try:
547546
other_kind = _resolve_node_kind(g, other_id)
@@ -561,6 +560,7 @@ def trace_v2(
561560

562561
# Record edge.
563562
edge_type = str(row.get("edge_type") or "")
563+
edge_id = f"{src_id}:{other_id}:{edge_type}:{hop}"
564564
edge = TraceEdge(
565565
from_id=src_id,
566566
to_id=other_id,
@@ -570,13 +570,12 @@ def trace_v2(
570570
attrs=_edge_attrs_for_row(row),
571571
)
572572
edges.append(edge)
573-
edge_id = f"{src_id}:{other_id}:{edge_type}:{hop}"
574573
edge_id_map[edge_id] = edge
575574
edges_by_parent[src_id].append(edge)
576575

577-
# Track incoming edge for this node.
578-
if other_id not in node_to_incoming_edge:
579-
node_to_incoming_edge[other_id] = edge
576+
# Track incoming edge ID for this node (for parent_edge_id of children).
577+
if other_id not in node_to_incoming_edge_id:
578+
node_to_incoming_edge_id[other_id] = edge_id
580579

581580
# PR-TRACE-1b: prune_roles, fan_out_cap, cross_service are no-ops here.
582581
# For now, add all discovered nodes to frontier.

tests/test_mcp_trace.py

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,9 @@ def test_trace_outbound_calls_depth_2(kuzu_graph: KuzuGraph) -> None:
6565
assert out.seed_ids == [seed_id]
6666
assert out.direction == "out"
6767
assert out.edge_types == ["CALLS"]
68-
# Check that we have edges at hop 0 and hop 1.
68+
# Check that we have edges at hop 0 and possibly hop 1.
6969
hops = {e.hop for e in out.edges}
70-
assert hops == {0, 1} or hops == {0} or hops == {0, 1}
70+
assert 0 in hops and hops <= {0, 1}
7171

7272

7373
def test_trace_inbound_callers_depth_2(kuzu_graph: KuzuGraph) -> None:
@@ -85,7 +85,6 @@ def test_trace_inbound_callers_depth_2(kuzu_graph: KuzuGraph) -> None:
8585
assert out.success is True
8686
assert out.seed_ids == [seed_id]
8787
assert out.direction == "in"
88-
assert len(out.edges) >= 0
8988

9089

9190
def test_trace_max_paths_cap(kuzu_graph: KuzuGraph) -> None:
@@ -249,10 +248,20 @@ def test_trace_invalid_edge_type(kuzu_graph: KuzuGraph) -> None:
249248

250249

251250
def test_trace_direction_required(kuzu_graph: KuzuGraph) -> None:
252-
"""Missing direction returns success=False."""
253-
# direction is required with Field(...) so this test validates the contract.
254-
# We can't actually call without direction due to pydantic validation.
255-
pass
251+
"""Missing direction is caught by pydantic validation (literal error)."""
252+
from pydantic import ValidationError
253+
254+
seed_id = _find_method_with_outbound_calls(kuzu_graph)
255+
if seed_id is None:
256+
pytest.skip("No method with outbound calls in fixture")
257+
# Pydantic validation rejects empty string before our code runs.
258+
with pytest.raises(ValidationError, match="direction"):
259+
trace_v2(
260+
ids=seed_id,
261+
direction="",
262+
edge_types=["CALLS"],
263+
graph=kuzu_graph,
264+
)
256265

257266

258267
def test_trace_edge_types_required(kuzu_graph: KuzuGraph) -> None:
@@ -573,10 +582,20 @@ def test_trace_parent_edge_id_chain(kuzu_graph: KuzuGraph) -> None:
573582

574583
for e in out.edges:
575584
if e.hop > 0:
576-
# parent_edge_id should point to a valid edge.
577-
# We check that the parent edge's to_id == e.from_id.
585+
# parent_edge_id should be a valid edge identifier that matches an edge in the result.
578586
if e.parent_edge_id:
579-
# Find the parent edge.
580-
parent_edges = [p for p in out.edges if p.to_id == e.from_id]
581-
# At least one parent should exist.
582-
assert len(parent_edges) >= 1
587+
# Parse the edge_id format: from_id:to_id:edge_type:hop
588+
parts = e.parent_edge_id.split(":")
589+
assert len(parts) == 4, f"Invalid parent_edge_id format: {e.parent_edge_id}"
590+
parent_from_id, parent_to_id, parent_edge_type, parent_hop = parts
591+
# Verify parent edge exists in result and parent.to_id == e.from_id
592+
parent_exists = any(
593+
p.from_id == parent_from_id
594+
and p.to_id == parent_to_id
595+
and p.edge_type == parent_edge_type
596+
and p.hop == int(parent_hop)
597+
for p in out.edges
598+
)
599+
assert parent_exists, f"Parent edge {e.parent_edge_id} not found in result"
600+
# Verify the parent edge reaches the current node's from_id
601+
assert parent_to_id == e.from_id, f"Parent edge {e.parent_edge_id} to_id != {e.from_id}"

0 commit comments

Comments
 (0)