mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 07:29:33 +00:00
Add regression tests for the parallel edge preservation fix
Covers the exact shape from the issue through the real CLI command
(an imports edge and a calls edge between the same pair surviving a
cluster only run), plus a direct unit test on the export function
confirming only a dangling endpoint is dropped, never a real pair.
Confirmed to fail against pre fix code.
(cherry picked from commit 5bde5b5a06)
This commit is contained in:
1 parent
7527e730b1
commit
e3ac1689e5
2 files changed
+110
No files matched your search
@@ -352,6 +352,14 @@ def to_json(G: nx.Graph, communities: dict[int, list[str]], output_path: str, *,
|
||||
# link verbatim instead, dropping only one whose endpoint no longer
|
||||
# exists in G. This also sidesteps any endpoint-order canonicalization
|
||||
# undirected storage would otherwise apply when deriving from G.
|
||||
#
|
||||
# The endpoint-existence filter below assumes original_links carry the
|
||||
# SAME ids as G's post-load nodes. That holds because build_from_json
|
||||
# rewrites a legacy node id to its canonical stem (_semantic_id_remap /
|
||||
# _doc_twin_remap) IN PLACE on the shared link dicts the caller then
|
||||
# passes here, so a remapped endpoint already matches node_ids rather
|
||||
# than being silently dropped (regression-tested in test_cli_export.py:
|
||||
# test_cluster_only_preserves_parallel_edges_across_an_id_remap).
|
||||
node_ids = {n["id"] for n in data["nodes"]}
|
||||
data["links"] = [
|
||||
dict(link) for link in original_links
|
||||
|
||||
@@ -793,6 +793,67 @@ def test_cluster_only_happy_path_exits_zero(tmp_path):
|
||||
assert "communities" in r.stdout
|
||||
|
||||
|
||||
def test_cluster_only_preserves_parallel_edges(tmp_path):
|
||||
"""#3999: cluster-only reloads graph.json into a simple Graph (one edge per
|
||||
node pair) to re-cluster, then used to write THAT back out, silently
|
||||
dropping a second edge (e.g. imports + calls) between a pair that stayed
|
||||
connected by the other. Community is a node attribute; re-clustering must
|
||||
never change the edge set."""
|
||||
out = tmp_path / "graphify-out"
|
||||
out.mkdir()
|
||||
(out / "graph.json").write_text(json.dumps({
|
||||
"directed": False, "multigraph": False, "graph": {},
|
||||
"nodes": [
|
||||
{"id": "a", "label": "a", "file_type": "code", "source_file": "a.py"},
|
||||
{"id": "b", "label": "b", "file_type": "code", "source_file": "a.py"},
|
||||
{"id": "c", "label": "c", "file_type": "code", "source_file": "a.py"},
|
||||
],
|
||||
"links": [
|
||||
{"source": "a", "target": "b", "relation": "imports", "confidence": "EXTRACTED"},
|
||||
{"source": "a", "target": "b", "relation": "calls", "confidence": "EXTRACTED"},
|
||||
{"source": "b", "target": "c", "relation": "calls", "confidence": "EXTRACTED"},
|
||||
],
|
||||
}))
|
||||
|
||||
r = _run(["cluster-only", ".", "--no-viz", "--no-label"], tmp_path)
|
||||
assert r.returncode == 0, r.stderr
|
||||
|
||||
data = json.loads((out / "graph.json").read_text())
|
||||
pairs = sorted((l["source"], l["target"], l["relation"]) for l in data["links"])
|
||||
assert pairs == [("a", "b", "calls"), ("a", "b", "imports"), ("b", "c", "calls")], pairs
|
||||
|
||||
|
||||
def test_to_json_original_links_drops_only_a_dangling_endpoint(tmp_path):
|
||||
"""#3999: original_links writes back every original link verbatim,
|
||||
except one whose endpoint no longer exists in G."""
|
||||
from graphify.build import build_from_json
|
||||
from graphify.cluster import cluster
|
||||
from graphify.export import to_json
|
||||
|
||||
extraction = {
|
||||
"directed": False,
|
||||
"nodes": [
|
||||
{"id": "a", "label": "a", "file_type": "code", "source_file": "a.py"},
|
||||
{"id": "b", "label": "b", "file_type": "code", "source_file": "a.py"},
|
||||
],
|
||||
"links": [
|
||||
{"source": "a", "target": "b", "relation": "imports", "confidence": "EXTRACTED"},
|
||||
{"source": "a", "target": "b", "relation": "calls", "confidence": "EXTRACTED"},
|
||||
],
|
||||
}
|
||||
G = build_from_json(extraction)
|
||||
communities = cluster(G)
|
||||
out = tmp_path / "graph.json"
|
||||
original_links = list(extraction["links"]) + [
|
||||
{"source": "a", "target": "gone", "relation": "calls", "confidence": "EXTRACTED"},
|
||||
]
|
||||
assert to_json(G, communities, str(out), original_links=original_links)
|
||||
|
||||
data = json.loads(out.read_text())
|
||||
pairs = sorted((l["source"], l["target"], l["relation"]) for l in data["links"])
|
||||
assert pairs == [("a", "b", "calls"), ("a", "b", "imports")], pairs
|
||||
|
||||
|
||||
def test_cluster_only_warns_when_labeling_flags_are_ignored(tmp_path):
|
||||
"""#2534 case 1: with a saved .graphify_labels.json the reuse branch never
|
||||
calls the LLM, so --backend/--model/--batch-size used to be silently
|
||||
@@ -908,3 +969,44 @@ def test_query_command_names_a_graph_outside_the_cwd(tmp_path):
|
||||
first_line = r.stdout.splitlines()[0]
|
||||
assert first_line.startswith("Graph: "), first_line
|
||||
assert "elsewhere" in first_line, first_line
|
||||
|
||||
|
||||
def test_cluster_only_preserves_parallel_edges_across_an_id_remap(tmp_path):
|
||||
"""#3999 completion: cluster-only reloads graph.json through build_from_json,
|
||||
which rewrites a legacy/pre-migration node id to its canonical stem
|
||||
(_semantic_id_remap). The preserved original_links must track the remapped
|
||||
id and still carry BOTH parallel edges — not be silently dropped because
|
||||
their endpoints no longer match the post-remap node set. build_from_json
|
||||
rewrites the shared link endpoints in place before to_json reads them;
|
||||
this test guards that invariant against a future refactor."""
|
||||
out = tmp_path / "graphify-out"
|
||||
out.mkdir()
|
||||
# `claude_alpha` + source_file `.claude/CLAUDE.md` remaps to
|
||||
# `claude_claude_alpha` (parent-dir == stem case, #1917).
|
||||
(out / "graph.json").write_text(json.dumps({
|
||||
"directed": False, "multigraph": False, "graph": {},
|
||||
"nodes": [
|
||||
{"id": "claude_alpha", "label": "alpha", "file_type": "concept",
|
||||
"source_file": ".claude/CLAUDE.md", "_origin": "semantic"},
|
||||
{"id": "beta", "label": "beta", "file_type": "code",
|
||||
"source_file": "pkg/beta.py", "_origin": "semantic"},
|
||||
],
|
||||
"links": [
|
||||
{"source": "claude_alpha", "target": "beta", "relation": "references", "confidence": "EXTRACTED"},
|
||||
{"source": "claude_alpha", "target": "beta", "relation": "mentions", "confidence": "EXTRACTED"},
|
||||
],
|
||||
}))
|
||||
|
||||
r = _run(["cluster-only", ".", "--no-viz", "--no-label"], tmp_path)
|
||||
assert r.returncode == 0, r.stderr
|
||||
|
||||
data = json.loads((out / "graph.json").read_text())
|
||||
node_ids = {n["id"] for n in data["nodes"]}
|
||||
assert "claude_claude_alpha" in node_ids, node_ids # remap fired
|
||||
assert "claude_alpha" not in node_ids
|
||||
pairs = sorted((l["source"], l["target"], l["relation"]) for l in data["links"])
|
||||
# both parallel edges survive AND are keyed on the remapped id
|
||||
assert pairs == [
|
||||
("claude_claude_alpha", "beta", "mentions"),
|
||||
("claude_claude_alpha", "beta", "references"),
|
||||
], pairs
|
||||
Reference in new issue
Block a user