"""record_hand idempotency + straddle parse coverage. The chat turn can execute twice — the SSE stream and the blocking fallback both run server-side (two 'chat request' lines, 1s apart) — which double-logged the same hand once logging became guaranteed. A system-of-record must record an event once.""" from __future__ import annotations import importlib import pytest @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", lambda texts: [[0.1, 0.2, 0.3] for _ in texts]) import lyra.memory as memory importlib.reload(memory) import lyra.poker as poker importlib.reload(poker) return poker _PARSED = { "game": "NLH", "hero_pos": "SB", "hero_cards": ["Ah", "Kh"], "board": ["Kd", "9d", "4c", "2s"], "players": [], "actions": [], "result": {"hero_net": -200, "pot": 400}, } def test_record_hand_is_idempotent_across_double_execution(poker, monkeypatch): sid = poker.start_session(venue="Borgata", stakes="1/3", buy_in=400) monkeypatch.setattr(poker, "parse_hand", lambda *a, **k: dict(_PARSED)) first = poker.record_hand("i have AhKh in the SB, btn straddle, ...") second = poker.record_hand("i have AhKh in the SB, btn straddle, ...") # the duplicate turn assert first["id"] == second["id"] assert second.get("deduped") is True assert len(poker.list_hands(sid)) == 1 # ledger holds ONE, not two def test_record_hand_does_not_dedupe_a_genuinely_different_hand(poker, monkeypatch): sid = poker.start_session(venue="Borgata", stakes="1/3", buy_in=400) monkeypatch.setattr(poker, "parse_hand", lambda *a, **k: dict(_PARSED)) poker.record_hand("hand one") other = dict(_PARSED, hero_cards=["Qs", "Qd"], board=["Qh", "7c", "2s"]) monkeypatch.setattr(poker, "parse_hand", lambda *a, **k: dict(other)) poker.record_hand("a different hand entirely") assert len(poker.list_hands(sid)) == 2 # distinct hands both land def test_dedupe_handles_boardless_hand(poker, monkeypatch): # NULL-safe match: a preflop-only hand (no board) still dedupes. sid = poker.start_session(venue="Borgata", buy_in=400) preflop = {"game": "NLH", "hero_pos": "BTN", "hero_cards": ["As", "Ks"], "board": [], "players": [], "actions": [], "result": {"hero_net": 30}} monkeypatch.setattr(poker, "parse_hand", lambda *a, **k: dict(preflop)) a = poker.record_hand("AKs btn, i open everyone folds") b = poker.record_hand("AKs btn, i open everyone folds") assert a["id"] == b["id"] and len(poker.list_hands(sid)) == 1 def test_parse_prompt_records_straddles(): from lyra import poker as pk p = pk._HAND_PARSE_PROMPT.lower() assert "straddle" in p and "button straddle" in p assert "acts last preflop" in p or "act last preflop" in p # --- hero stack auto-fill from the last logged stack ---------------------- def test_hero_stack_filled_from_last_stack_log(poker, monkeypatch): poker.start_session(venue="Meadows", stakes="1/3", buy_in=400) poker.log_stack(275) # his last reported stack monkeypatch.setattr(poker, "parse_hand", lambda *a, **k: {"game": "NLH", "hero_involved": True, "hero_pos": "CO", "hero_cards": ["As", "Ks"], "board": ["2c"], "players": [], "actions": [], "result": {"hero_net": 50}}) out = poker.record_hand("AKs in the CO, i raise, flop 2c...") stored = poker.get_hand(out["id"])["structured"] hero = next(pl for pl in stored["players"] if pl.get("hero")) assert hero["stack"] == 275 and hero.get("stack_inferred") is True def test_stated_stack_is_never_overridden(poker, monkeypatch): poker.start_session(venue="Meadows", buy_in=400) poker.log_stack(275) monkeypatch.setattr(poker, "parse_hand", lambda *a, **k: {"game": "NLH", "hero_involved": True, "hero_pos": "BTN", "hero_cards": ["Qh", "Qd"], "players": [{"pos": "BTN", "stack": 500}], "board": [], "actions": [], "result": {}}) out = poker.record_hand("500 deep on the btn with QQ") hero = next(pl for pl in poker.get_hand(out["id"])["structured"]["players"] if pl.get("pos") == "BTN") assert hero["stack"] == 500 and not hero.get("stack_inferred") def test_observed_hand_gets_no_hero_stack(poker, monkeypatch): poker.start_session(venue="Meadows", buy_in=400) poker.log_stack(275) monkeypatch.setattr(poker, "parse_hand", lambda *a, **k: {"game": "NLH", "hero_involved": False, "hero_pos": None, "hero_cards": [], "players": [{"pos": "CO", "cards": ["Kx", "Kx"]}], "board": [], "actions": [], "result": {}}) out = poker.record_hand("the CO stacked off KK vs the nit") assert all(not pl.get("stack_inferred") for pl in poker.get_hand(out["id"])["structured"]["players"])