Files
serversdown 2be43848a7 fix: don't attribute observed hands to the hero
When Brian narrates a hand he watched between OTHER players, the parser was still
filling hero_pos/hero_cards — pinning someone else's cards, position, and result
to him. Now:
- parser prompt adds hero_involved detection: fill hero_pos/hero_cards ONLY if he
  was actually in the hand; a hand he only watched has hero_involved=false and
  null hero fields, with the other players recorded normally.
- normalize_structured enforces it as a safety net (hero_involved=false → null
  hero_pos/cards, hero_net) even if the model slips.
- record_hand tool confirms an observed hand as "not yours" instead of implying
  it was his.

2 tests. Restarting web for the live session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 02:58:02 +00:00

127 lines
5.0 KiB
Python

"""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_observed_hand_never_attributed_to_hero(poker):
# Brian narrated a hand between two other players — hero_involved=false.
out = poker.normalize_structured({
"hero_involved": False,
"hero_pos": "CO", "hero_cards": ["Kx", "Kx"], # model slipped these in
"players": [{"pos": "CO", "cards": ["Kx", "Kx"]}, {"pos": "BB", "cards": ["Ax", "Ax"]}],
"result": {"pot": 600, "hero_net": 300},
})
assert out["hero_pos"] is None # not pinned to Brian
assert out["hero_cards"] == []
assert out["result"]["hero_net"] is None # a pot he wasn't in
assert not any(pl.get("hero") for pl in out["players"]) # nobody flagged hero
def test_hero_hand_still_attributed(poker):
out = poker.normalize_structured({
"hero_involved": True, "hero_pos": "BTN", "hero_cards": ["As", "Ks"],
"players": [{"pos": "BTN"}]})
assert out["hero_pos"] == "BTN"
hero = next(pl for pl in out["players"] if pl.get("pos") == "BTN")
assert hero.get("hero") and hero["cards"] == ["As", "Ks"]
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