"""Nameless-villain identity resolution: descriptor matching, merge, distinct, queue.""" from __future__ import annotations import importlib import numpy as np import pytest def _fake_embed(texts): """Overlap-sensitive bag-of-words vectors so cosine reflects shared tokens.""" out = [] for t in texts: v = np.zeros(64, dtype=np.float32) for w in t.lower().split(): v[hash(w) % 64] += 1.0 out.append((v if v.any() else np.full(64, 1e-6, dtype=np.float32)).tolist()) return out @pytest.fixture def poker(tmp_path, monkeypatch): monkeypatch.setenv("LYRA_DB_PATH", str(tmp_path / "test.db")) from lyra import llm monkeypatch.setattr(llm, "embed", _fake_embed) import lyra.memory as memory importlib.reload(memory) import lyra.poker as poker importlib.reload(poker) return poker def test_distinctiveness_distinctive_vs_generic(poker): assert poker.distinctiveness("guy with a neck tattoo") > 0.6 assert poker.distinctiveness("mid-aged white dude with glasses") < 0.3 def test_generic_descriptor_never_resolves_to_a_guess(poker): poker.create_descriptor_villain("neck tattoo sleeve arm", venue="Meadows") r = poker.resolve_villain("mid aged white guy with glasses", venue="Meadows") assert r["band"] == "generic" assert r["match_id"] is None def test_exact_name_match_is_deterministic(poker): pid = poker.upsert_player("Sleepy John", venue="Meadows") r = poker.resolve_villain("sleepy john") assert r["band"] == "name" and r["match_id"] == pid def test_rephrased_descriptor_resolves_high(poker): pid = poker.create_descriptor_villain("neck tattoo sleeve arm", venue="Meadows") r = poker.resolve_villain("neck tattoo sleeve", venue="Meadows") assert r["band"] == "high" and r["match_id"] == pid assert r["confidence"] >= 0.80 def test_partial_descriptor_is_ambiguous_not_high(poker): poker.create_descriptor_villain("neck tattoo sleeve arm", venue="Meadows") r = poker.resolve_villain("neck tattoo", venue="Meadows") assert r["band"] == "ambiguous" # plausible, but don't guess live def test_merge_repoints_observations_and_deletes_dup(poker): keep = poker.create_descriptor_villain("neck tattoo", venue="Meadows") dup = poker.create_descriptor_villain("neck ink tatted", venue="Meadows") poker._c().execute( "INSERT INTO player_observations (player_id, session_id, created_at) VALUES (?,1,?)", (dup, poker._now())) poker._c().commit() assert poker.merge_players(keep, dup) is True assert poker.get_villain_file() and all(p["id"] != dup for p in poker.get_villain_file()) obs = poker._c().execute( "SELECT COUNT(*) n FROM player_observations WHERE player_id = ?", (keep,)).fetchone()["n"] assert obs == 1 def test_merge_prefers_a_real_name(poker): named = poker.upsert_player("Danny", venue="Meadows") desc = poker.create_descriptor_villain("neck tattoo", venue="Meadows") poker.merge_players(desc, named) # keep the descriptor id, but name should win row = dict(poker._c().execute("SELECT name, named FROM poker_players WHERE id = ?", (desc,)).fetchone()) assert row["name"] == "Danny" and row["named"] == 1 def test_mark_distinct_blocks_merge_scan(poker): a = poker.create_descriptor_villain("neck tattoo sleeve", venue="Meadows") b = poker.create_descriptor_villain("neck tattoo sleeve", venue="Meadows") poker.mark_distinct(a, b, note="one's taller") assert poker.are_distinct(a, b) assert poker.scan_merge_candidates() == 0 # confirmed-distinct pair is skipped def test_scan_files_merge_candidate_for_near_duplicates(poker): poker.create_descriptor_villain("neck tattoo sleeve", venue="Meadows") poker.create_descriptor_villain("neck tattoo sleeve", venue="Meadows") filed = poker.scan_merge_candidates() assert filed == 1 q = poker.list_identity_queue() assert q and q[0]["kind"] == "merge_candidate" and len(q[0]["players"]) == 2 def test_queue_dedupes_identical_pending_task(poker): a = poker.create_descriptor_villain("neck tattoo", venue="Meadows") b = poker.create_descriptor_villain("neck ink", venue="Meadows") t1 = poker.queue_identity_task("merge_candidate", [a, b]) t2 = poker.queue_identity_task("merge_candidate", [b, a]) # same pair, reversed assert t1 == t2 assert len(poker.list_identity_queue()) == 1 def test_name_villain_flips_named_flag(poker): pid = poker.create_descriptor_villain("neck tattoo", venue="Meadows") poker.name_villain(pid, "Danny") r = poker.resolve_villain("Danny") assert r["band"] == "name" and r["match_id"] == pid