|
| 1 | +"""Test non-bijunctive recall collapse (Elyan Edition).""" |
| 2 | +import sys |
| 3 | +import os |
| 4 | + |
| 5 | +sys.path.insert(0, os.path.dirname(__file__)) |
| 6 | + |
| 7 | +from icarus.collapse import ( |
| 8 | + tokenize, salience, score_all, collapse, DEFAULTS, |
| 9 | + physical_entropy, attest, verify_attestation, |
| 10 | +) |
| 11 | + |
| 12 | +all_ok = True |
| 13 | + |
| 14 | + |
| 15 | +def check(name, cond): |
| 16 | + global all_ok |
| 17 | + if not cond: |
| 18 | + print(f"FAIL: {name}") |
| 19 | + all_ok = False |
| 20 | + |
| 21 | + |
| 22 | +# ── tokenize ── |
| 23 | +check("tokenize strips stopwords", tokenize("the quick brown fox") == {"quick", "brown", "fox"}) |
| 24 | +check("tokenize empty -> empty set", tokenize("") == set()) |
| 25 | +check("tokenize lowercases", tokenize("RustChain POWER8") == {"rustchain", "power8"}) |
| 26 | + |
| 27 | +# ── salience monotonic with overlap ── |
| 28 | +q = tokenize("rustchain ed25519 attestation signature") |
| 29 | +hi = salience({"text": "rustchain ed25519 attestation signature node", "source": "facts"}, q) |
| 30 | +lo = salience({"text": "unrelated gardening tomatoes weather", "source": "facts"}, q) |
| 31 | +check("salience rewards overlap", hi > lo) |
| 32 | + |
| 33 | +# qdrant score lifts a candidate with no overlap above a zero-score one |
| 34 | +sc_hi = salience({"text": "zzz none", "source": "qdrant", "score": 0.9}, q) |
| 35 | +sc_lo = salience({"text": "zzz none", "source": "qdrant", "score": 0.1}, q) |
| 36 | +check("salience rewards score", sc_hi > sc_lo) |
| 37 | + |
| 38 | +# rank decay: later rank => lower salience, all else equal |
| 39 | +r0 = salience({"text": "rustchain ed25519", "source": "fabric", "rank": 0}, q) |
| 40 | +r3 = salience({"text": "rustchain ed25519", "source": "fabric", "rank": 3}, q) |
| 41 | +check("rank decay lowers later ranks", r0 > r3) |
| 42 | + |
| 43 | +# ── collapse: prune weak relative to strong ── |
| 44 | +cands = [ |
| 45 | + {"key": "strong", "source": "facts", "text": "rustchain ed25519 attestation signature verified node", "rank": 0}, |
| 46 | + {"key": "mid", "source": "sessions", "text": "rustchain notes about something", "rank": 0}, |
| 47 | + {"key": "weak", "source": "qdrant", "text": "completely unrelated gardening tomatoes", "score": 0.0, "rank": 0}, |
| 48 | +] |
| 49 | +out = collapse(cands, q, budget=6, prune_ratio=0.35) |
| 50 | +keys = [c["key"] for c in out] |
| 51 | +check("strong survives", "strong" in keys) |
| 52 | +check("weak pruned relative to strong", "weak" not in keys) |
| 53 | +check("survivors carry _salience", all("_salience" in c for c in out)) |
| 54 | +check("survivors sorted strongest-first", out == sorted(out, key=lambda c: c["_salience"], reverse=True)) |
| 55 | + |
| 56 | +# ── collapse: budget cap ── |
| 57 | +many = [ |
| 58 | + {"key": f"k{i}", "source": "facts", "text": f"rustchain ed25519 attestation node {i}", "rank": 0} |
| 59 | + for i in range(20) |
| 60 | +] |
| 61 | +out2 = collapse(many, q, budget=4) |
| 62 | +check("budget caps survivors", len(out2) <= 4) |
| 63 | + |
| 64 | +# ── collapse: near-duplicate suppression ── |
| 65 | +dups = [ |
| 66 | + {"key": "a", "source": "facts", "text": "rustchain ed25519 attestation signature verified", "rank": 0}, |
| 67 | + {"key": "b", "source": "qdrant", "text": "rustchain ed25519 attestation signature verified", "score": 0.9, "rank": 0}, |
| 68 | + {"key": "c", "source": "sessions", "text": "totally different power8 numa coffer topic entirely", "rank": 0}, |
| 69 | +] |
| 70 | +out3 = collapse(dups, tokenize("rustchain ed25519 attestation signature power8 numa"), budget=6, dup_overlap=0.82) |
| 71 | +ids = [c["key"] for c in out3] |
| 72 | +check("near-duplicate suppressed (a or b, not both)", not ("a" in ids and "b" in ids)) |
| 73 | + |
| 74 | +# ── edge cases ── |
| 75 | +check("empty input -> []", collapse([], q) == []) |
| 76 | +check("zero budget -> []", collapse(cands, q, budget=0) == []) |
| 77 | +mixed = collapse([None, "x", 42, {"key": "ok", "source": "facts", "text": "rustchain ed25519 attestation"}], q) |
| 78 | +check("non-dict items ignored (only the dict survives)", [c["key"] for c in mixed] == ["ok"]) |
| 79 | + |
| 80 | +# no query tokens: must NOT collapse to empty when there was real signal |
| 81 | +out4 = collapse(cands, set(), budget=2) |
| 82 | +check("empty query still returns survivors (no firehose, no blackout)", 0 < len(out4) <= 2) |
| 83 | + |
| 84 | +# DEFAULTS sanity |
| 85 | +check("DEFAULTS present", {"budget", "prune_ratio", "dup_overlap"} <= set(DEFAULTS)) |
| 86 | +check("DEFAULTS has amplify knobs", {"corroboration_overlap", "amplify_gain", "amplify_cap"} <= set(DEFAULTS)) |
| 87 | + |
| 88 | +# ── Hebbian cross-source amplify ── |
| 89 | +qh = tokenize("rustchain ed25519 attestation signature") |
| 90 | +# Same fact from TWO different sources (fabric + qdrant) should amplify; a lone |
| 91 | +# unrelated item should not. Corroboration counts cross-source only. |
| 92 | +corro_set = [ |
| 93 | + {"key": "fab", "source": "fabric", "text": "rustchain ed25519 attestation signature verified", "rank": 0}, |
| 94 | + {"key": "qdr", "source": "qdrant", "text": "rustchain ed25519 attestation signature verified", "score": 0.5, "rank": 0}, |
| 95 | + {"key": "lone", "source": "sessions", "text": "rustchain ed25519 attestation signature note", "rank": 0}, |
| 96 | +] |
| 97 | +scored = {r["candidate"]["key"]: r for r in score_all(corro_set, qh)} |
| 98 | +check("cross-source corroboration counted", scored["fab"]["corroboration"] >= 1) |
| 99 | +check("corroboration amplifies salience above base", scored["fab"]["salience"] > scored["fab"]["base"]) |
| 100 | +# same-source duplicates do NOT corroborate (must be cross-source) |
| 101 | +same_src = score_all([ |
| 102 | + {"key": "f1", "source": "facts", "text": "rustchain ed25519 attestation", "rank": 0}, |
| 103 | + {"key": "f2", "source": "facts", "text": "rustchain ed25519 attestation", "rank": 1}, |
| 104 | +], qh) |
| 105 | +check("same-source agreement does NOT amplify", all(r["corroboration"] == 0 for r in same_src)) |
| 106 | +# survivors carry _corroboration |
| 107 | +amp_out = collapse(corro_set, qh, budget=6) |
| 108 | +check("survivors annotated with _corroboration", all("_corroboration" in c for c in amp_out)) |
| 109 | + |
| 110 | +# ── physical-entropy attestation ── |
| 111 | +ent = bytes(range(16)) # injected => deterministic for the test |
| 112 | +a1 = attest(amp_out, entropy=ent) |
| 113 | +check("attestation has hash+nonce+algo", {"hash", "nonce", "count", "algo"} <= set(a1)) |
| 114 | +check("attestation algo is blake2b-256", a1["algo"] == "blake2b-256") |
| 115 | +check("attestation verifies for unchanged survivors", verify_attestation(amp_out, a1) is True) |
| 116 | +# tamper-evidence: drop a survivor => verification fails |
| 117 | +check("attestation FAILS when survivor set tampered", verify_attestation(amp_out[:-1], a1) is False if len(amp_out) > 1 else True) |
| 118 | +# order-independent commitment: shuffled survivors verify the same |
| 119 | +check("attestation order-independent", verify_attestation(list(reversed(amp_out)), a1) is True) |
| 120 | +# determinism: same survivors + same nonce => same hash |
| 121 | +check("attestation deterministic under fixed nonce", attest(amp_out, entropy=ent)["hash"] == a1["hash"]) |
| 122 | +# physical entropy: live nonce is non-empty and (essentially always) varies |
| 123 | +e_a, e_b = physical_entropy(16), physical_entropy(16) |
| 124 | +check("physical_entropy returns requested length", len(e_a) == 16) |
| 125 | +check("physical_entropy is live (two draws differ)", e_a != e_b) |
| 126 | +# different selection => different commitment under same nonce |
| 127 | +other = collapse([{"key": "z", "source": "facts", "text": "unrelated power8 numa coffer", "rank": 0}], tokenize("power8 numa")) |
| 128 | +check("different selection => different hash", attest(other, entropy=ent)["hash"] != a1["hash"]) |
| 129 | + |
| 130 | +# default (LIVE physical-entropy) attest path round-trips — exercises the impure |
| 131 | +# branch, not just the injected-entropy one. |
| 132 | +live = attest(amp_out) |
| 133 | +check("default attest path verifies round-trip", verify_attestation(amp_out, live) is True) |
| 134 | +check("default attest carries a live nonce", len(live["nonce"]) > 0 and live["nonce"] != a1["nonce"]) |
| 135 | + |
| 136 | +# identity (not text/salience) is committed: two DISTINCT survivors with the |
| 137 | +# SAME source+text+salience but different keys must NOT cross-verify. |
| 138 | +twinA = [{"key": "A", "source": "facts", "text": "same text", "_salience": 0.5}] |
| 139 | +twinB = [{"key": "B", "source": "facts", "text": "same text", "_salience": 0.5}] |
| 140 | +attA = attest(twinA, entropy=ent) |
| 141 | +check("same source/text/salience but different key => different commitment", |
| 142 | + verify_attestation(twinB, attA) is False) |
| 143 | + |
| 144 | +# physical_entropy clamps oversized requests instead of raising (blake2b max 64) |
| 145 | +check("physical_entropy clamps >64 without raising", 1 <= len(physical_entropy(200)) <= 64) |
| 146 | + |
| 147 | +# ── adapter tests: hooks._apply_collapse (the hot-path wiring) ── |
| 148 | +# Silence the fail-open WARNING+traceback that the intentional malformed-input |
| 149 | +# test below triggers by design — keeps test output clean. |
| 150 | +import logging as _logging |
| 151 | +_logging.disable(_logging.CRITICAL) |
| 152 | +from icarus import hooks as _hooks |
| 153 | + |
| 154 | +# strong fabric + relevant session survive; irrelevant zero-score qdrant pruned |
| 155 | +af, aq, asn, afc = _hooks._apply_collapse( |
| 156 | + "rustchain ed25519 attestation signature", |
| 157 | + [{"id": "f1", "summary": "rustchain ed25519 attestation signature verified"}], |
| 158 | + [{"id": "q1", "title": "gardening", "content_preview": "tomatoes weather unrelated", "score": 0.0}], |
| 159 | + [{"session_id": "s1", "title": "rustchain", "snippet": "ed25519 attestation work"}], |
| 160 | + ["power8 numa coffer unrelated topic"], |
| 161 | +) |
| 162 | +check("adapter: strong fabric survives", [e["id"] for e in af] == ["f1"]) |
| 163 | +check("adapter: weak zero-score qdrant pruned", aq == []) |
| 164 | +check("adapter: returns four lists", all(isinstance(x, list) for x in (af, aq, asn, afc))) |
| 165 | + |
| 166 | +# qdrant text now reads `content`/`body`, not just title+preview (Codex fix) |
| 167 | +qtxt = _hooks._qdrant_text({"content": "rustchain ed25519 attestation node verified"}) |
| 168 | +check("adapter: _qdrant_text reads content field", "ed25519" in qtxt) |
| 169 | + |
| 170 | +# fail-open: malformed inputs must return unchanged tuple, never raise |
| 171 | +bad = _hooks._apply_collapse("q", [{"no": "text"}], [None], [], []) |
| 172 | +check("adapter: fail-open returns 4-tuple", len(bad) == 4) |
| 173 | + |
| 174 | +# safe env parser: garbage value falls back to default, never raises |
| 175 | +check("adapter: _env_num bad value -> default", _hooks._env_num("X_NOPE_BAD", 6, int) == 6) |
| 176 | + |
| 177 | +if all_ok: |
| 178 | + print("=== ALL COLLAPSE TESTS PASS ===") |
| 179 | + sys.exit(0) |
| 180 | +else: |
| 181 | + print("=== COLLAPSE TESTS FAILED ===") |
| 182 | + sys.exit(1) |
0 commit comments