Skip to content

Commit 6eb7c01

Browse files
safishamsiclaude
andcommitted
test(build): add regression tests for corrupt graph.json (#1537)
#1537 shipped with a manual test checklist only. Add automated tests that corrupt a graph.json and assert the actionable RuntimeError at all three load paths (build_merge, affected.load_graph, diagnostics._read_json_file) plus a happy-path guard. Also record the six merged small fixes in the changelog. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 64a6093 commit 6eb7c01

2 files changed

Lines changed: 59 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ Full release notes with details on each version: [GitHub Releases](https://githu
44

55
## Unreleased
66

7+
- Fix: Ruby class inheritance now emits an `inherits` edge (#1535, thanks @Synvoya). `class Dog < Animal` produced `contains`/method/call edges but no `inherits` edge — the inheritance handler had branches for Java/Kotlin/C#/Scala/C++/PHP/Swift/Python but none for Ruby, so the `superclass` field was never read. Handles both bare (`< Animal`) and qualified (`< M::Base`) superclasses.
8+
- Fix: Groovy `extends`/`implements` now emit `inherits`/`implements` edges (#1534, thanks @Synvoya). tree-sitter-groovy exposes inheritance through the same grammar shape as tree-sitter-java, but the handler was gated to Java only, so every Groovy inheritance relationship was dropped.
9+
- Fix: corrupt `graph.json` now raises a clear, actionable error instead of a raw traceback (#1537 / #1536, thanks @guyoron1). The three graph-loading paths — `build_merge` (`--update`), `load_graph` (`graphify prs`), and diagnostics (`graphify diagnose`) — wrap `json.loads` and raise a `RuntimeError` with recovery guidance on a truncated/invalid file (incomplete write, power loss, manual edit).
10+
- Fix: cross-chunk node-ID collisions now warn instead of silently dropping a node (#1508 / #1504, thanks @nuthalapativarun). When two nodes share an ID but come from different source files (two same-named files in different directories), dedup keeps the first and now prints a warning naming both files and how to avoid the loss (`graphify extract` per subfolder + `merge-graphs`).
11+
- Fix: git hooks on Windows/MSYS default to sequential rebuilds (#1554, thanks @matiasduartee). Hook-triggered rebuilds now export `GRAPHIFY_MAX_WORKERS=1` on Windows/MSYS (explicit user value still wins), avoiding fragile inherited pipe handles; and the Windows-path hooks guard is a no-op on native Windows, where such paths are legitimate.
12+
- Docs: correct the `deduplicate_by_label` docstring — it is dormant, not auto-called by `build()` (#1514, thanks @TPAteeq). The active dedup path is `deduplicate_entities`; the note that `deduplicate_by_label` runs automatically was never true, and it must not be enabled for code nodes (it merges by label with no file_type guard, conflating same-named symbols across files).
713
- Feat: deterministic hub community labels, readable without an LLM (#1576, thanks @sheik-hiiobd). When no LLM backend is configured, community labels used to fall back to `Community 70`, making the report and its Suggested Questions unreadable. Each community is now named after its highest-degree member (the structural hub, ties broken by node id for run-to-run stability) — so a plain `graphify` run reads `auth` / `log_action` at zero token cost. A configured LLM naming pass still overrides these with richer names; `--no-label` still yields bare `Community N`.
814
- Feat: extend `indirect_call` to `getattr(obj, "name")` reflective dispatch (#1575, #1566 slice 3, thanks @sheik-hiiobd). A callable looked up by a string literal — `fn = getattr(obj, "handler")` — now emits an `indirect_call` edge (context `getattr`, INFERRED) so `affected` reaches it. Only a plain string literal resolves; a variable, f-string, or concatenation is dynamic and emits nothing. Unlike the identifier paths, a getattr string names an attribute, not a binding, so it is never shadowed by a param/local — `def via(handler): getattr(x, "handler")` still resolves to the module `handler`. Function and module scope; cross-file handled by the shared resolver. Python only for now.
915
- Fix: `graphify --update` no longer drops hyperedges from unchanged files (#1574, thanks @socar-tender). `build_merge` read only nodes and edges from the existing `graph.json`, never hyperedges — so every incremental update collapsed the graph's hyperedge set (the semantic domain-flow groupings) down to just the re-extracted files'. Existing hyperedges are now carried forward: re-extracted files' prior hyperedges are replaced by their new version (by `source_file`), deleted files' are pruned, and the rest are preserved with id-dedup — mirroring how `watch` already handled it.

tests/test_corrupt_graph_json.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Corrupt graph.json produces an actionable error, not a raw traceback (#1536/#1537).
2+
3+
Three load paths call json.loads on graph.json — build_merge (`--update`),
4+
affected.load_graph (`graphify prs`), and diagnostics._read_json_file
5+
(`graphify diagnose`). A truncated / invalid file (incomplete write, power loss,
6+
manual edit) must raise a clear RuntimeError with recovery guidance at each.
7+
"""
8+
from __future__ import annotations
9+
10+
import pytest
11+
12+
from graphify.build import build_merge
13+
from graphify.affected import load_graph
14+
from graphify.diagnostics import _read_json_file
15+
16+
_CORRUPT = '{"nodes": [{"id": "a", "labe' # truncated mid-object
17+
18+
19+
def _corrupt(tmp_path):
20+
p = tmp_path / "graph.json"
21+
p.write_text(_CORRUPT, encoding="utf-8")
22+
return p
23+
24+
25+
def test_build_merge_corrupt_graph_raises_runtimeerror(tmp_path):
26+
p = _corrupt(tmp_path)
27+
with pytest.raises(RuntimeError, match=r"Cannot read .*incremental merge|rebuild"):
28+
build_merge([], graph_path=p, dedup=False)
29+
30+
31+
def test_affected_load_graph_corrupt_raises_runtimeerror(tmp_path):
32+
p = _corrupt(tmp_path)
33+
with pytest.raises(RuntimeError, match=r"Cannot read graph file|regenerate"):
34+
load_graph(p)
35+
36+
37+
def test_diagnostics_read_corrupt_raises_runtimeerror(tmp_path):
38+
p = _corrupt(tmp_path)
39+
with pytest.raises(RuntimeError, match=r"Cannot parse|corrupted"):
40+
_read_json_file(p)
41+
42+
43+
def test_valid_graph_still_loads(tmp_path):
44+
"""Happy path unchanged: a well-formed graph.json loads without raising."""
45+
p = tmp_path / "graph.json"
46+
p.write_text(
47+
'{"nodes": [{"id": "a", "label": "a", "file_type": "code"}], "edges": []}',
48+
encoding="utf-8",
49+
)
50+
# none of these should raise
51+
load_graph(p)
52+
_read_json_file(p)
53+
build_merge([], graph_path=p, dedup=False)

0 commit comments

Comments
 (0)