mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-15 00:19:33 +00:00
test(build): cover AST-twin shrink-guard fix end to end; sync alias docstrings
- Add to_json() and watch._check_shrink() regression tests that build a real
graph where build_from_json folds a semantic ghost into its AST twin (#4389),
plus a negative control showing a genuine semantic loss is still refused.
Both positive tests fail on v8 and pass with this branch.
- Update _fold_node_aliases / _fold_edge_aliases docstrings and the build()
dedup comment to list the new file/filepath and relationship aliases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 946333b2ab)
This commit is contained in:
1 parent
3c5b1687fb
commit
61ce767dab
2 files changed
+52
-9
No files matched your search
+10
-8
@@ -450,10 +450,11 @@ def _normalize_hyperedge_members(he: object) -> None:
|
||||
def _fold_node_aliases(node: dict) -> None:
|
||||
"""Fold legacy node field aliases onto canonical keys, in place (#2194).
|
||||
|
||||
``name`` -> ``label`` and ``path`` -> ``source_file``. Uses an empty-check
|
||||
(not mere key presence) so a node carrying ``label: ""``/``None`` next to a
|
||||
real ``name`` is healed too. When the canonical field already holds a value
|
||||
it wins and the alias key is left untouched. Without this fold an alias-only
|
||||
``name`` -> ``label`` and ``file``/``path``/``filepath`` -> ``source_file``
|
||||
(first non-blank alias in that order wins). Uses an empty-check (not mere
|
||||
key presence) so a node carrying ``label: ""``/``None`` next to a real
|
||||
``name`` is healed too. When the canonical field already holds a value it
|
||||
wins and the alias key is left untouched. Without this fold an alias-only
|
||||
node enters the graph with no label/source_file: it fails validation, gets
|
||||
``norm_label == ""`` (invisible to query/explain), and is excluded from every
|
||||
label-keyed merge/dedup — a permanent ghost that ``graphify update``
|
||||
@@ -472,9 +473,10 @@ def _fold_node_aliases(node: dict) -> None:
|
||||
def _fold_edge_aliases(edge: dict) -> None:
|
||||
"""Fold legacy edge field aliases onto canonical keys, in place (#2194).
|
||||
|
||||
``type`` -> ``relation``. A ``confidence_score`` float with no ``confidence``
|
||||
enum backfills ``confidence: "INFERRED"`` — never EXTRACTED (alias recovery
|
||||
is not provenance) and never a threshold mapping of the float. The
|
||||
``type``/``relationship`` -> ``relation`` (``type`` checked first). A
|
||||
``confidence_score`` float with no ``confidence`` enum backfills
|
||||
``confidence: "INFERRED"`` — never EXTRACTED (alias recovery is not
|
||||
provenance) and never a threshold mapping of the float. The
|
||||
``confidence_score`` key itself is NOT popped: it is a legitimate companion
|
||||
field that the edge loop sanitizes and to_json round-trips.
|
||||
|
||||
@@ -1830,7 +1832,7 @@ def build(
|
||||
# coerces too, but that runs after dedup — too late for this path.
|
||||
_coerce_non_string_ids(combined)
|
||||
# Fold legacy node field aliases before dedup (#2194): dedup runs BEFORE
|
||||
# build_from_json and keys on `label`, so a `name`/`path` alias node
|
||||
# build_from_json and keys on `label`, so a `name`/`file`/`path` alias node
|
||||
# would be invisible to it and only label-dedup one build later, after
|
||||
# build_from_json's own fold has healed the persisted graph.json.
|
||||
for n in combined["nodes"]:
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
(#1145 extended) is preserved, not lost."""
|
||||
from __future__ import annotations
|
||||
|
||||
from graphify.build import _lost_saved_semantic_nodes
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from graphify.build import _lost_saved_semantic_nodes, build_from_json
|
||||
from graphify.export import to_json
|
||||
from graphify.watch import _check_shrink
|
||||
|
||||
|
||||
def _sem(node_id: str, label: str, sf: str) -> dict:
|
||||
@@ -99,3 +104,39 @@ def test_malformed_non_string_source_file_does_not_crash():
|
||||
new = [{"id": "n1", "label": "N1", "source_file": True}]
|
||||
lost = _lost_saved_semantic_nodes(saved, new)
|
||||
assert [n["id"] for n in lost] == ["s1"]
|
||||
|
||||
|
||||
# --- end-to-end: the write paths that call the guard (#4389) ---
|
||||
|
||||
|
||||
def _saved_graph(tmp_path: Path, sem: dict) -> tuple[Path, dict]:
|
||||
out = tmp_path / "graph.json"
|
||||
assert to_json(build_from_json({"nodes": [sem], "edges": []}), {}, str(out), force=True)
|
||||
return out, json.loads(out.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def _node_data(G) -> dict:
|
||||
return {"nodes": [{"id": nid, **attrs} for nid, attrs in G.nodes(data=True)], "links": []}
|
||||
|
||||
|
||||
def test_to_json_writes_after_ghost_dedup(tmp_path):
|
||||
sem = _sem("src_models_user_ts_user", "User", "src/models/user.ts")
|
||||
out, _ = _saved_graph(tmp_path, sem)
|
||||
G = build_from_json({"nodes": [sem, _ast("user_user", "User", "src/models/user.ts")], "edges": []})
|
||||
assert list(G.nodes) == ["user_user"] # ghost id folded into its AST twin
|
||||
assert to_json(G, {}, str(out)) is True
|
||||
|
||||
|
||||
def test_to_json_still_refuses_genuine_loss(tmp_path, capsys):
|
||||
sem = _sem("src_models_user_ts_user", "User", "src/models/user.ts")
|
||||
out, _ = _saved_graph(tmp_path, sem)
|
||||
G = build_from_json({"nodes": [_ast("user_account", "Account", "src/models/user.ts")], "edges": []})
|
||||
assert to_json(G, {}, str(out)) is False
|
||||
assert "src_models_user_ts_user" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_check_shrink_allows_ghost_dedup(tmp_path):
|
||||
sem = _sem("src_models_user_ts_user", "User", "src/models/user.ts")
|
||||
_, existing = _saved_graph(tmp_path, sem)
|
||||
G = build_from_json({"nodes": [sem, _ast("user_user", "User", "src/models/user.ts")], "edges": []})
|
||||
assert _check_shrink(force=False, existing_data=existing, new_data=_node_data(G)) is True
|
||||
Reference in new issue
Block a user