From 5929edf984cc366af927d9398424ac249b781dc0 Mon Sep 17 00:00:00 2001 From: safishamsi Date: Mon, 17 Aug 2026 15:35:53 +0100 Subject: [PATCH] test(dedup): pin chained-collapse and kept-single-member for hyperedge rewire (#2805) Adds the two cases the deep-dive flagged: a chained collapse (a_old, a_mid -> a) lands directly on the final survivor (the whole fix rests on the union-find remap being fully flattened), and a hyperedge collapsing to one distinct member is kept, not dropped (pinning the deliberate sub-two-member policy). Adds the CHANGELOG entry. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 1 + tests/test_dedup_remaps_hyperedges.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d477a85c..4e31c62f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ Full release notes with details on each version: [GitHub Releases](https://githu ## 0.9.46 (unreleased) +- Fix: when node dedup merges two nodes, any hyperedge that listed the merged-away node as a member now rewires that member to the survivor instead of silently dropping it, so a grouping no longer loses participants on dedup (#2805, thanks @abhay-codes07). - Fix: pruning a source file now also sweeps the external-import placeholder nodes it strands at degree 0, instead of leaving them to accumulate in the node count, `GRAPH_REPORT.md`, and exports; genuinely-isolated real nodes (which carry a `source_file`) are never touched (#2807, thanks @abhay-codes07). - Fix: when two edges connect the same node pair with different relations, the graph builder now keeps the more specific relation (`calls`, `imports`, `inherits`, ...) instead of letting a generic `references`/`uses`/`mentions` overwrite it; previously a real `calls` could be downgraded to `references` and then dropped from the call graph (#2803, thanks @abhay-codes07). diff --git a/tests/test_dedup_remaps_hyperedges.py b/tests/test_dedup_remaps_hyperedges.py index 45a59686..446eb5ef 100644 --- a/tests/test_dedup_remaps_hyperedges.py +++ b/tests/test_dedup_remaps_hyperedges.py @@ -125,3 +125,22 @@ def test_an_empty_remap_changes_nothing(): hes = [{"id": "h", "nodes": ["a", "b", "c"]}] _remap_hyperedge_members(hes, {}) assert hes[0]["nodes"] == ["a", "b", "c"] + + +def test_chained_collapse_lands_on_the_final_survivor(): + """A dedup remap built from union-find is fully flattened (path-compressed), + so a member of a chained component (a_old -> a_mid -> a) rewires directly to + the final survivor in a single lookup, never to an intermediate.""" + hes = [{"id": "h", "nodes": ["a_old", "a_mid", "b"]}] + # what components()/UnionFind produces: every non-winner maps to the winner + _remap_hyperedge_members(hes, {"a_old": "a", "a_mid": "a"}) + assert hes[0]["nodes"] == ["a", "b"] + + +def test_a_hyperedge_collapsing_to_one_member_is_kept(): + """Sub-two-member hyperedges are kept by design (build_from_json only drops + the zero-valid-member case). Pin it so a future refactor doesn't silently + start dropping a 1-member group after a collapse.""" + hes = [{"id": "h", "nodes": ["a_old", "a_new"]}] + _remap_hyperedge_members(hes, {"a_old": "a", "a_new": "a"}) + assert hes[0]["nodes"] == ["a"] # collapsed to one, still present