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>
This commit is contained in:
safishamsi
2026-07-01 10:49:43 +01:00
co-authored by Claude Opus 4.8
parent 64a6093376
commit 6eb7c014f4
2 changed files with 59 additions and 0 deletions
+6
View File
@@ -4,6 +4,12 @@ Full release notes with details on each version: [GitHub Releases](https://githu
## Unreleased
- 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.
- 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.
- 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).
- 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`).
- 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.
- 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).
- 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`.
- 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.
- 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.
+53
View File
@@ -0,0 +1,53 @@
"""Corrupt graph.json produces an actionable error, not a raw traceback (#1536/#1537).
Three load paths call json.loads on graph.json — build_merge (`--update`),
affected.load_graph (`graphify prs`), and diagnostics._read_json_file
(`graphify diagnose`). A truncated / invalid file (incomplete write, power loss,
manual edit) must raise a clear RuntimeError with recovery guidance at each.
"""
from __future__ import annotations
import pytest
from graphify.build import build_merge
from graphify.affected import load_graph
from graphify.diagnostics import _read_json_file
_CORRUPT = '{"nodes": [{"id": "a", "labe' # truncated mid-object
def _corrupt(tmp_path):
p = tmp_path / "graph.json"
p.write_text(_CORRUPT, encoding="utf-8")
return p
def test_build_merge_corrupt_graph_raises_runtimeerror(tmp_path):
p = _corrupt(tmp_path)
with pytest.raises(RuntimeError, match=r"Cannot read .*incremental merge|rebuild"):
build_merge([], graph_path=p, dedup=False)
def test_affected_load_graph_corrupt_raises_runtimeerror(tmp_path):
p = _corrupt(tmp_path)
with pytest.raises(RuntimeError, match=r"Cannot read graph file|regenerate"):
load_graph(p)
def test_diagnostics_read_corrupt_raises_runtimeerror(tmp_path):
p = _corrupt(tmp_path)
with pytest.raises(RuntimeError, match=r"Cannot parse|corrupted"):
_read_json_file(p)
def test_valid_graph_still_loads(tmp_path):
"""Happy path unchanged: a well-formed graph.json loads without raising."""
p = tmp_path / "graph.json"
p.write_text(
'{"nodes": [{"id": "a", "label": "a", "file_type": "code"}], "edges": []}',
encoding="utf-8",
)
# none of these should raise
load_graph(p)
_read_json_file(p)
build_merge([], graph_path=p, dedup=False)