"""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")