fix(poker): idempotent hand logging + straddle capture
Two issues from live testing:
- Double-logged hand. The chat turn can execute TWICE — the SSE stream and the
blocking fallback both run server-side (two 'chat request' lines, 1s apart) — a
pre-existing double-execution (it also duplicated user messages) that the new
logging guarantee turned into duplicate HANDS. record_hand is now idempotent:
_recent_duplicate_hand returns an identical hand (same session, hole cards, board;
NULL-safe) recorded in the last few minutes, so the second run reuses it instead
of inserting. A system-of-record records an event once.
- Button straddle dropped. The parse prompt had no straddle logic. Added a STRADDLES
rule: record any straddle as a preflop `post` by the straddler with its amount and
respect the action order (button straddle acts last preflop, action opens in the
SB; UTG straddle opens to its left). Verified: a btn-straddle hand now parses the
straddle as {pos: BTN, action: post, amount: 6}.
Note: the underlying double turn-execution (stream + fallback) is a separate web-layer
bug worth fixing at the source — it wastes a full LLM turn and still double-persists
chat messages. Filed for a follow-up. 6 tests; suite 223 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user