"""The canonical structured-hand contract (docs/HAND_HISTORY.md): normalize + export. normalize_structured() is the single guarantee that every stored / replayed / exported hand has the versioned shape RTO consumes. """ 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 def _full_hand(): return { "game": "NLH", "stakes": "1/3", "hero_pos": "BTN", "hero_cards": ["ah", "kh"], "players": [ {"pos": "BTN", "stack": 300, "name": "Hero"}, {"pos": "BB", "stack": 250, "name": "Sal", "cards": ["qs", "qd"]}, ], "actions": [ {"street": "preflop", "pos": "BTN", "action": "raise", "amount": 15}, {"street": "flop", "board": ["7♦", "2♣", "5♥"]}, {"street": "flop", "pos": "BB", "action": "check"}, ], "board": ["7♦", "2♣", "5♥"], "result": {"pot": 40, "hero_net": 25, "summary": "won at showdown"}, } def test_stamps_version(poker): out = poker.normalize_structured({"hero_pos": "CO"}) assert out["schema_version"] == poker.HAND_SCHEMA_VERSION def test_card_normalization(poker): out = poker.normalize_structured(_full_hand()) assert out["hero_cards"] == ["Ah", "Kh"] # lowercased input -> canonical assert out["board"] == ["7d", "2c", "5h"] # unicode suits -> letters assert out["actions"][1]["board"] == ["7d", "2c", "5h"] # ten + suit symbol together assert poker.normalize_structured({"board": ["10♠"]})["board"] == ["Ts"] def test_unknown_cards_preserved(poker): out = poker.normalize_structured({"hero_cards": ["Ax", "x"], "board": ["Ax", "4x", "x"]}) assert out["hero_cards"] == ["Ax", "x"] # placeholders kept, not dropped assert out["completeness"]["cards"] is False assert out["completeness"]["board"] is False def test_hero_synced_into_players(poker): out = poker.normalize_structured(_full_hand()) hero = next(p for p in out["players"] if p["pos"] == "BTN") assert hero["hero"] is True assert hero["cards"] == ["Ah", "Kh"] # mirrored from hero_cards assert sum(1 for p in out["players"] if p.get("hero")) == 1 def test_hero_inserted_when_missing_from_players(poker): out = poker.normalize_structured({"hero_pos": "SB", "hero_cards": ["As", "Ad"], "players": []}) assert out["players"] == [{"pos": "SB", "hero": True, "cards": ["As", "Ad"]}] def test_completeness_full_hand(poker): c = poker.normalize_structured(_full_hand())["completeness"] assert c == {"cards": True, "board": True, "actions": True} def test_idempotent(poker): once = poker.normalize_structured(_full_hand()) twice = poker.normalize_structured(once) assert once == twice def test_store_and_get_roundtrip_is_normalized(poker): sid = poker.start_session(venue="Meadows", stakes="1/3", buy_in=400) hid = poker.store_hand_history(_full_hand(), session_id=sid, tag="well_played") got = poker.get_hand(hid)["structured"] assert got["schema_version"] == poker.HAND_SCHEMA_VERSION assert got["board"] == ["7d", "2c", "5h"] assert got["completeness"]["cards"] is True def test_list_recent_hands_flags_structured(poker): sid = poker.start_session(venue="Meadows", stakes="1/3", buy_in=400) structured_id = poker.store_hand_history(_full_hand(), session_id=sid) flat_id = poker.log_hand(session_id=sid, position="CO", hole_cards="Jc Jd") rows = {r["id"]: r for r in poker.list_recent_hands()} assert rows[structured_id]["has_structured"] is True assert rows[flat_id]["has_structured"] is False