test(detect): cover ast-kind no-op churn and corrupt-manifest write (#2838)

Adds the two cases the deep-dive flagged: the kind='ast' path (the literal
graphify update scenario) stays byte-identical on a no-op and updates on a real
edit; and a corrupt/unparseable existing manifest still gets written (the
byte-equality skip's except must never silently drop a real update). Also adds
the CHANGELOG entry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
safishamsi
2026-08-18 18:09:59 +01:00
co-authored by Claude Opus 4.8
parent 173a8814d2
commit 97f2bb7540
2 changed files with 39 additions and 0 deletions
+1
View File
@@ -4,6 +4,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu
## 0.9.47 (unreleased)
- Fix: `graphify update` / `save_manifest` no longer rewrites `manifest.json` timestamps on a no-op run, so `graphify-out/` stops showing as dirty (and stops producing a trailing graph commit) when nothing changed; a genuine change still updates and persists (#2838, thanks @hopstreax).
- Fix: a C# 12 primary constructor's parameters are now walked, so `class Svc(IRepo repo)` emits the `references` edge to `IRepo` and calls through `repo` resolve — previously the class silently dropped its dependency; built-in and type-parameter types are not fabricated (#2829, thanks @brobl2008).
- Fix: AST-derived INFERRED edges now carry a rubric `confidence_score` keyed to the relation (`uses` 0.95, `indirect_call`/unresolved cross-file `calls` 0.85) instead of landing at the rubric-forbidden 0.5 or a flat 0.8; the INFERRED default moves 0.5→0.55 so every score-less INFERRED edge is on the discrete rubric set (#2813, thanks @abhay-codes07).
- Fix: a legacy `graph.json` that stored a numeric edge `confidence` (from a pre-enum version) no longer warns once per edge on every incremental reload; the numeric value is normalized to the `INFERRED` tag with the original float preserved in `confidence_score` (thanks @Trantor-develops).
+38
View File
@@ -3051,6 +3051,44 @@ def test_save_manifest_noop_skips_disk_write(tmp_path):
assert mtime_1 == mtime_2
def test_save_manifest_ast_kind_noop_then_change(tmp_path):
"""#2838's literal path: `graphify update` calls save_manifest with kind='ast'.
A no-op re-run must leave the manifest byte-identical; a real edit must update it."""
import json
a = tmp_path / "a.py"
a.write_text("x = 1\n", encoding="utf-8")
manifest_path = Path(tmp_path / "graphify-out" / "manifest.json")
save_manifest({"code": [str(a)]}, str(manifest_path), root=tmp_path, kind="ast")
bytes_1 = manifest_path.read_bytes()
seen_1 = json.loads(bytes_1)["a.py"]["seen"]
save_manifest({"code": [str(a)]}, str(manifest_path), root=tmp_path, kind="ast") # no-op
assert manifest_path.read_bytes() == bytes_1, "ast-kind no-op re-run churned the manifest"
import time as _t; _t.sleep(0.01)
a.write_text("x = 2\n", encoding="utf-8")
save_manifest({"code": [str(a)]}, str(manifest_path), root=tmp_path, kind="ast")
raw2 = json.loads(manifest_path.read_text(encoding="utf-8"))
assert raw2["a.py"]["ast_hash"] != json.loads(bytes_1)["a.py"]["ast_hash"]
assert raw2["a.py"]["seen"] >= seen_1
def test_save_manifest_corrupt_existing_manifest_still_writes(tmp_path):
"""The byte-equality skip must never turn an unparseable on-disk manifest into
a silent no-write — a real update has to persist over corruption."""
import json
a = tmp_path / "a.py"
a.write_text("x = 1\n", encoding="utf-8")
manifest_path = Path(tmp_path / "graphify-out" / "manifest.json")
manifest_path.parent.mkdir(parents=True, exist_ok=True)
manifest_path.write_text("{ this is not valid json", encoding="utf-8")
save_manifest({"code": [str(a)]}, str(manifest_path), root=tmp_path)
raw = json.loads(manifest_path.read_text(encoding="utf-8")) # must parse now
assert "a.py" in raw and isinstance(raw["a.py"]["seen"], (int, float))
# ── #2106: sensitive-filter over-match (prose/source rescued, real secrets kept) ──
@pytest.mark.parametrize("path", [