Files
project-lyra/tests/test_modes.py
T
serversdown e1e89c07e4 feat: poker session history — browse, delete, and Lyra lookup
Answers three gaps: no way to delete a single poker session (only clear_all),
no way to browse past sessions, and Lyra could only see aggregate stats.

- poker.list_sessions() (per-session summary + hand count + recap flag) and
  poker.delete_session() (removes a session + its hands/reads/observations/
  stacks/rituals; keeps the persistent villain file).
- /history page (date, stakes, venue, net, hours, recap link, per-row delete with
  confirm) + /history/data + DELETE /history/{id}. Nav links from chat + HUD.
- recent_sessions read tool, added to the shared lookups so Lyra can answer
  "how'd my last few sessions go?" in either mode.
- Delete is UI/CLI only — deliberately not a Lyra tool.
- test_modes.py +2 (list/delete, recent_sessions); 44 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 00:21:48 +00:00

213 lines
8.1 KiB
Python

"""Conversation modes: tool gating, mode persistence, stack tracking + HUD."""
from __future__ import annotations
import importlib
import pytest
@pytest.fixture
def lyra(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)
import lyra.modes as modes
importlib.reload(modes)
import lyra.tools as tools
importlib.reload(tools)
return memory, poker, modes, tools
def _names(specs):
return {s["function"]["name"] for s in specs}
def test_tool_gating_by_mode(lyra):
_, _, modes, tools = lyra
talk = _names(tools.specs(modes.TALK.tools))
cash = _names(tools.specs(modes.CASH.tools))
# Cash is the full live toolset.
assert {"log_hand", "log_stack", "analyze_spot", "end_session"} <= cash
# Talk hides the live write tools...
assert "log_hand" not in talk and "log_stack" not in talk
# ...but keeps her agency + read-only lookups + the session entry point.
assert {"journal_write", "note", "player_profile", "start_session"} <= talk
# No allow-list = every registered tool.
assert _names(tools.specs()) == set(tools.TOOLS)
def test_every_mode_tool_exists(lyra):
_, _, modes, tools = lyra
for mode in modes.MODES.values():
assert set(mode.tools) <= set(tools.TOOLS), f"{mode.key} references unknown tools"
def test_mode_resolution_and_persistence(lyra):
memory, _, modes, _ = lyra
assert modes.get(None).key == modes.DEFAULT
assert modes.get("nonsense").key == modes.DEFAULT
assert modes.get("poker_cash") is modes.CASH
memory.ensure_session("s1")
assert memory.get_session_mode("s1") is None # unset -> caller applies default
memory.set_session_mode("s1", "poker_cash")
assert memory.get_session_mode("s1") == "poker_cash"
# set on an unknown session creates the row
memory.set_session_mode("s2", "conversation")
assert memory.get_session_mode("s2") == "conversation"
def test_stack_log_and_live_net(lyra):
_, poker, _, _ = lyra
poker.start_session(venue="Meadows", stakes="2/5", buy_in=500)
assert poker.current_stack() is None # nothing logged yet
st = poker.log_stack(700)
assert st["current"] == 700 and st["net"] == 200 # up 200 on a 500 buy-in
poker.log_stack(350)
assert poker.current_stack() == 350
assert poker.stack_state()["net"] == -150
assert len(poker.stack_log()) == 2
def test_log_stack_requires_live_session(lyra):
_, poker, _, _ = lyra
with pytest.raises(ValueError):
poker.log_stack(300)
def test_hud_bundle(lyra):
_, poker, _, _ = lyra
assert poker.hud() is None # no session -> nothing to show
sid = poker.start_session(venue="Meadows", stakes="2/5", game="NLH", buy_in=500)
poker.log_stack(620)
poker.log_hand(position="BTN", hole_cards="AKs", result=120, tag="confidence")
poker.add_read(note="3bets light from the SB", name="Round Mike", seat="SB")
hud = poker.hud()
assert hud["session"]["id"] == sid and hud["session"]["stakes"] == "2/5"
assert hud["stack"]["current"] == 620 and hud["stack"]["net"] == 120
assert len(hud["stack"]["log"]) == 1
assert len(hud["hands"]) == 1 and hud["hands"][0]["hole_cards"] == "AKs"
assert any(v["name"] == "Round Mike" for v in hud["villains"])
assert hud["stats"]["hands_logged"] == 1
def test_log_stack_tool_handler(lyra):
_, poker, _, tools = lyra
poker.start_session(stakes="1/3", buy_in=300)
out = tools.dispatch("log_stack", {"amount": 450}, {})
assert "450" in out and "150" in out # confirms stack + live net
# graceful when there's no number
assert "number" in tools.dispatch("log_stack", {}, {}).lower()
# --- mental-game rituals ---
def test_ritual_tools_in_cash_only(lyra):
_, _, modes, tools = lyra
cash = _names(tools.specs(modes.CASH.tools))
talk = _names(tools.specs(modes.TALK.tools))
rituals = {"scar_note", "confidence_bank", "alligator_blood", "reset_ritual"}
assert rituals <= cash
assert not (rituals & talk)
def test_scar_and_confidence_capture(lyra):
_, poker, _, tools = lyra
poker.start_session(stakes="2/5", buy_in=500)
tools.dispatch("scar_note", {"content": "punted bottom set", "classification": "punt"}, {})
tools.dispatch("scar_note", {"content": "ran KK into AA", "classification": "cooler"}, {})
tools.dispatch("confidence_bank", {"content": "disciplined river fold"}, {})
scars = poker.list_rituals(kinds=("scar",))
assert len(scars) == 2
assert {s["classification"] for s in scars} == {"punt", "cooler"}
conf = poker.list_rituals(kinds=("confidence",))
assert len(conf) == 1 and "fold" in conf[0]["content"]
# bogus classification is dropped, not stored
tools.dispatch("scar_note", {"content": "x", "classification": "nonsense"}, {})
assert poker.list_rituals(kinds=("scar",))[-1]["classification"] is None
def test_alligator_toggle_and_state(lyra):
_, poker, _, tools = lyra
poker.start_session(stakes="2/5", buy_in=500)
assert poker.alligator_active() is False
tools.dispatch("alligator_blood", {"on": True}, {})
assert poker.alligator_active() is True
tools.dispatch("alligator_blood", {"on": False}, {})
assert poker.alligator_active() is False # latest toggle wins
def test_rituals_in_hud(lyra):
_, poker, _, tools = lyra
poker.start_session(stakes="2/5", buy_in=500)
tools.dispatch("scar_note", {"content": "overplayed top pair"}, {})
tools.dispatch("confidence_bank", {"content": "good value bet"}, {})
tools.dispatch("reset_ritual", {"content": "lost a flip"}, {})
tools.dispatch("alligator_blood", {"on": True}, {})
r = poker.hud()["rituals"]
assert r["alligator"] is True
assert len(r["scars"]) == 1 and len(r["confidence"]) == 1 and len(r["resets"]) == 1
def test_session_state_readback(lyra):
_, poker, _, tools = lyra
assert "no live session" in tools.dispatch("session_state", {}, {}).lower()
poker.start_session(venue="Meadows", stakes="2/5", buy_in=500)
tools.dispatch("log_stack", {"amount": 720}, {})
tools.dispatch("confidence_bank", {"content": "great river fold"}, {})
tools.dispatch("alligator_blood", {"on": True}, {})
out = tools.dispatch("session_state", {}, {})
assert "720" in out # current stack
assert "+220" in out or "220" in out # live net
assert "Alligator Blood is ON" in out
assert "great river fold" in out
def test_list_and_delete_session(lyra):
_, poker, _, tools = lyra
keep = poker.start_session(venue="Meadows", stakes="1/3", buy_in=300)
poker.end_session(cash_out=400, session_id=keep)
drop = poker.start_session(venue="Bellagio", stakes="2/5", buy_in=500)
poker.log_hand(position="BTN", hole_cards="AKs", session_id=drop)
poker.log_stack(620, session_id=drop)
poker.log_ritual("scar", content="punt", session_id=drop)
sessions = poker.list_sessions()
assert {s["id"] for s in sessions} == {keep, drop}
assert next(s for s in sessions if s["id"] == drop)["hands"] == 1
removed = poker.delete_session(drop)
assert removed["poker_sessions"] == 1 and removed["poker_hands"] == 1
assert removed["poker_stack_log"] == 1 and removed["poker_rituals"] == 1
assert {s["id"] for s in poker.list_sessions()} == {keep} # only the survivor
assert poker.get_session(drop) is None
def test_recent_sessions_tool(lyra):
_, poker, modes, tools = lyra
assert "recent_sessions" in modes.TALK.tools # available even when just talking
poker.import_session(date="2026-06-01", venue="Meadows", stakes="1/3",
buy_in_total=300, cash_out=520, hours=5)
out = tools.dispatch("recent_sessions", {}, {})
assert "Meadows" in out and "+220" in out
def test_rituals_require_live_session(lyra):
_, poker, _, tools = lyra
# tools degrade gracefully (no exception) when nothing is open
assert "no live session" in tools.dispatch("scar_note", {"content": "x"}, {}).lower()
with pytest.raises(ValueError):
poker.log_ritual("scar", content="x")