Skip to content

Commit 6f72ff8

Browse files
Wolfvinclaude
andcommitted
test(graph): golden cases for Python module-level calls (#291)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent aec039f commit 6f72ff8

1 file changed

Lines changed: 85 additions & 0 deletions

File tree

tests/test_graph_accuracy_golden.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,37 @@ def _callers_of(backend: dict, fn: str) -> set:
153153
"""
154154

155155

156+
_MODULE_LEVEL_CALL_PY = """\
157+
# Guards #291: Python calls at module top level (not inside any def/class) must
158+
# count toward the callee's rc — analogous to #219 for TS/JS. Before #291 the
159+
# Python parser only emitted edges for calls inside a function body, so a
160+
# function called ONLY at module level got rc=0 / status=dead (false positive).
161+
def setup_app():
162+
return 1
163+
164+
165+
def helper():
166+
return 2
167+
168+
169+
def caller():
170+
return helper()
171+
172+
173+
def py_never_called():
174+
# Control: genuinely dead — never called anywhere. Must STAY dead.
175+
return 99
176+
177+
178+
setup_app() # module-level call -> synthetic <module> caller
179+
caller() # module-level call -> synthetic <module> caller
180+
"""
181+
182+
156183
def _build_workspace(tmp_path) -> str:
157184
ws = tmp_path / "golden_ws"
158185
(ws / "src").mkdir(parents=True)
186+
(ws / "src" / "mod_level.py").write_text(_MODULE_LEVEL_CALL_PY)
159187
(ws / "src" / "mod_level.ts").write_text(_MODULE_LEVEL_CALL_TS)
160188
(ws / "src" / "handler.ts").write_text(_ASYNC_HANDLER_TS)
161189
(ws / "src" / "svc.ts").write_text(_SVC_TS)
@@ -279,6 +307,63 @@ def test_same_file_rust_const_not_dead(self, scanned):
279307
f"#220 regression: same-file-used Rust const RED flagged dead by the engine: {red_hits[:1]}"
280308
)
281309

310+
def test_python_module_level_call_counts_toward_rc(self, backend):
311+
"""#291: Python `setup_app`/`caller` called only at module top level.
312+
313+
Both are called via a bare top-level statement (not inside any function
314+
body). Before #291 the Python parser emitted no edge for module-level
315+
calls, so both had rc=0 / status=dead (false positive). Each must now
316+
have rc>=1 with a caller from mod_level.py.
317+
"""
318+
for fn in ("setup_app", "caller"):
319+
rc = _rc(backend, fn)
320+
assert rc >= 1, f"#291 regression: Python {fn} rc={rc}, expected >=1 (module-level call)"
321+
callers = _callers_of(backend, fn)
322+
assert any("mod_level.py" in c for c in callers), (
323+
f"#291: expected a caller from mod_level.py for {fn}, got {callers}"
324+
)
325+
326+
def test_python_module_level_caller_visible_in_trace_up(self, scanned):
327+
"""#291/#223: `trace --direction up setup_app` surfaces the <module> caller.
328+
329+
The module-level caller uses the synthetic `<file>:0:<module>` id (same
330+
format as TS/JS), so `graph_model.is_module_level_source_id()` recognises
331+
it and trace-up emits a `module_level=True` / `fn="<module>"` entry.
332+
"""
333+
ws, _ = scanned
334+
from commands.trace import execute
335+
336+
class _Args:
337+
name = "setup_app"
338+
direction = "up"
339+
depth = 10
340+
domain = "auto"
341+
limit = 20
342+
offset = 0
343+
max_results = 1000
344+
use_graph = True
345+
deep = False
346+
format = "json"
347+
348+
result = execute(_Args(), ws)
349+
up = result.get("chains", {}).get("up", [])
350+
module_callers = [c for c in up if c.get("module_level") or c.get("fn") == "<module>"]
351+
assert module_callers, (
352+
f"#291 regression: module-level caller of setup_app dropped from trace-up. up callers: {up}"
353+
)
354+
355+
def test_python_genuinely_dead_still_detected(self, backend):
356+
"""#291 control: an unreferenced Python function IS still dead (rc 0).
357+
358+
Guards against a fix that inflates rc to hide false positives — a
359+
genuinely-never-called Python function must retain rc 0.
360+
"""
361+
nodes = _nodes_named(backend, "py_never_called")
362+
assert nodes, "control node py_never_called missing"
363+
assert nodes[0].get("ref_count", 0) == 0, (
364+
f"#291 control: py_never_called should have rc 0, got {nodes[0].get('ref_count')}"
365+
)
366+
282367
def test_genuinely_dead_still_detected(self, backend):
283368
"""Control: an unreferenced internal function IS still dead (rc 0)."""
284369
nodes = _nodes_named(backend, "trulyUnusedInternal")

0 commit comments

Comments
 (0)