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:
co-authored by
Claude Opus 4.8
parent
64a6093376
commit
6eb7c014f4
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user