Skip to content

Commit cb8ba07

Browse files
d9ngclaude
andcommitted
test(pr3): add monolith-form regression test for upstream port
Adds tests/regression/test_pr3_deterministic_sort_monolith.py — same 5 cases as test_pr3_deterministic_sort.py, but loads src/hooks/bm25-memory.py directly via importlib.util so it can run against the upstream monolith (the hyphenated filename rules out a normal `import`). Per jaytoone's request on PR pluto2060#5: the re-skinned tests are intended to ship alongside the monolith port so they run in CI without the `_bm25/` package on sys.path. Includes a graceful fork fallback: if the loaded module doesn't expose `rrf_merge` / `dense_rank_decisions` / `bm25_rank_decisions` at module level (fork orchestrator only re-exports `hybrid_rank_decisions`), fall back to `_bm25.ranker` so the test is also exercisable from the fork. Validation: 5/5 PASS on fork orchestrator + golden 26/26 unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 515c95a commit cb8ba07

1 file changed

Lines changed: 154 additions & 0 deletions

File tree

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""
2+
PR-3 regression — monolith form (drop-in for upstream `bm25-memory.py` port).
3+
4+
Same 5 cases as `test_pr3_deterministic_sort.py`, but the import path is
5+
restructured so the test loads the upstream monolith via `importlib.util`
6+
(the file name `bm25-memory.py` contains a hyphen and cannot be imported
7+
as a normal module).
8+
9+
After the monolith port lands upstream, these tests run as-is from the
10+
upstream root with no `_bm25/` package on `sys.path`. From the fork side,
11+
they also pass because our orchestrator file is at the same path.
12+
13+
Sites covered:
14+
- dense_rank_decisions
15+
- rrf_merge
16+
- bm25_rank_decisions
17+
"""
18+
import importlib.util
19+
import sys
20+
from pathlib import Path
21+
22+
23+
def _load_bm25_memory():
24+
"""Dynamically load `src/hooks/bm25-memory.py` as a module.
25+
26+
Hyphen in the filename rules out a normal import. The fork orchestrator
27+
and the upstream monolith both expose the three target functions at
28+
module level, so this loader is interchangeable between them.
29+
"""
30+
proj = Path(__file__).resolve().parents[2]
31+
monolith = proj / "src" / "hooks" / "bm25-memory.py"
32+
# Ensure the package directory is importable so the orchestrator's own
33+
# internal imports resolve (in fork: `_bm25/`; in upstream monolith
34+
# there are no such imports — the loader still works).
35+
sys.path.insert(0, str(proj / "src" / "hooks"))
36+
spec = importlib.util.spec_from_file_location("bm25_memory", monolith)
37+
mod = importlib.util.module_from_spec(spec)
38+
spec.loader.exec_module(mod)
39+
return mod
40+
41+
42+
bm25_memory = _load_bm25_memory()
43+
44+
45+
def _resolve(name):
46+
"""Resolve a target function from the monolith.
47+
48+
Upstream monolith: function defined at module level → direct attribute.
49+
Fork orchestrator (post-decomposition): orchestrator does not re-export
50+
rrf_merge / dense_rank_decisions / bm25_rank_decisions, so we fall back
51+
to the `_bm25.ranker` module that the orchestrator imports from.
52+
"""
53+
if hasattr(bm25_memory, name):
54+
return getattr(bm25_memory, name)
55+
# Fallback for fork — package present alongside the orchestrator
56+
from _bm25 import ranker # type: ignore
57+
return getattr(ranker, name)
58+
59+
60+
rrf_merge = _resolve("rrf_merge")
61+
dense_rank_decisions = _resolve("dense_rank_decisions")
62+
bm25_rank_decisions = _resolve("bm25_rank_decisions")
63+
HAS_BM25 = getattr(bm25_memory, "HAS_BM25", None)
64+
if HAS_BM25 is None:
65+
from _bm25 import ranker as _ranker # type: ignore
66+
HAS_BM25 = getattr(_ranker, "HAS_BM25", False)
67+
68+
69+
def _items(n, prefix="c"):
70+
return [{"hash": f"{prefix}{i:03d}", "text": f"item {i}", "emb": []} for i in range(n)]
71+
72+
73+
def test_rrf_merge_idempotent_same_input():
74+
"""Same input → same output across repeat calls (no hidden randomness)."""
75+
a = _items(20, "a")
76+
b = _items(20, "b")
77+
keys1 = [it["hash"] for it in rrf_merge(a, b)]
78+
keys2 = [it["hash"] for it in rrf_merge(a, b)]
79+
assert keys1 == keys2, f"rrf_merge non-idempotent: {keys1[:5]} vs {keys2[:5]}"
80+
81+
82+
def test_rrf_merge_equal_rank_tiebreak_independent_of_list_input_order():
83+
"""Items with identical RRF rank in both lists must order by hash —
84+
independent of whether item X or item Y was inserted first into list_a.
85+
86+
This is the bug that hash tiebreak fixes: previously dict-insertion
87+
order leaked into the output, so swapping list_a/list_b position of
88+
equal-rank items would shuffle the result."""
89+
a1 = [{"hash": "zzz_late", "text": "z"}]
90+
b1 = [{"hash": "aaa_early", "text": "a"}]
91+
a2 = [{"hash": "aaa_early", "text": "a"}]
92+
b2 = [{"hash": "zzz_late", "text": "z"}]
93+
keys1 = [it["hash"] for it in rrf_merge(a1, b1)]
94+
keys2 = [it["hash"] for it in rrf_merge(a2, b2)]
95+
assert keys1 == keys2 == ["aaa_early", "zzz_late"], (
96+
f"hash tiebreak failed:\n case1={keys1}\n case2={keys2}"
97+
)
98+
99+
100+
def test_rrf_merge_equal_score_tiebreak_is_hash():
101+
"""Items with identical RRF scores (same rank in both lists) must
102+
order by hash key ascending, not insertion order."""
103+
a = [{"hash": "z_high", "text": "z"}, {"hash": "a_low", "text": "a"}]
104+
b = [{"hash": "a_low", "text": "a"}, {"hash": "z_high", "text": "z"}]
105+
out = rrf_merge(a, b)
106+
keys = [it["hash"] for it in out]
107+
assert keys == ["a_low", "z_high"], f"hash tiebreak failed: got {keys}"
108+
109+
110+
def test_dense_rank_decisions_no_emb_returns_empty():
111+
"""Sanity: vec-daemon down → empty list."""
112+
corpus = _items(5)
113+
result = dense_rank_decisions(corpus, "any query")
114+
assert result == [] or all("hash" in it for it in result)
115+
116+
117+
def test_bm25_rank_decisions_index_tiebreak():
118+
"""bm25_rank_decisions: equal scores → index ascending.
119+
120+
Corpus of byte-identical entries gets identical BM25 scores. With the
121+
explicit `(-scores[i], i)` tiebreak, surviving entries (after MMR /
122+
cluster dedup) come back in ascending index order."""
123+
if not HAS_BM25:
124+
return
125+
corpus = [
126+
{"hash": f"h{i}", "subject": "identical text", "text": "identical text body for bm25"}
127+
for i in range(5)
128+
]
129+
result = bm25_rank_decisions(
130+
corpus,
131+
"identical bm25",
132+
top_k=5,
133+
min_score=0.0,
134+
adaptive_floor_ratio=0.0,
135+
mmr_jaccard_threshold=1.01, # disable MMR
136+
skip_rerank=True,
137+
)
138+
hashes = [it["hash"] for it in result]
139+
if len(hashes) > 1:
140+
assert hashes == sorted(hashes), f"index tiebreak broken: {hashes}"
141+
142+
143+
if __name__ == "__main__":
144+
test_rrf_merge_idempotent_same_input()
145+
print("PASS: rrf_merge idempotent")
146+
test_rrf_merge_equal_rank_tiebreak_independent_of_list_input_order()
147+
print("PASS: rrf_merge equal-rank tiebreak independent of input order")
148+
test_rrf_merge_equal_score_tiebreak_is_hash()
149+
print("PASS: rrf_merge equal-score tiebreak by hash")
150+
test_dense_rank_decisions_no_emb_returns_empty()
151+
print("PASS: dense_rank_decisions no-emb sanity")
152+
test_bm25_rank_decisions_index_tiebreak()
153+
print("PASS: bm25_rank_decisions index tiebreak")
154+
print("\nAll PR-3 monolith-form regression tests passed.")

0 commit comments

Comments
 (0)