from __future__ import annotations from chat.db.connection import open_db from chat.db.migrate import apply_migrations from chat.eventlog.log import append_event from chat.eventlog.projector import project import chat.state.entities # registers handlers import chat.state.world # registers handlers import chat.state.meanwhile # registers handlers from chat.state.meanwhile import ( get_parent_scene, list_meanwhile_scenes, list_pending_meanwhile_digests, ) from chat.state.world import active_scene def _bot_payload(bot_id: str, name: str) -> dict: return { "id": bot_id, "name": name, "persona": "thoughtful, observant", "voice_samples": [], "traits": [], "backstory": "", "initial_relationship_to_you": "coworker", "kickoff_prose": "", } def _chat_payload(chat_id: str = "chat_bot_a") -> dict: return { "id": chat_id, "host_bot_id": "bot_a", "guest_bot_id": "bot_b", "initial_time": "2026-04-26T20:00:00+00:00", "narrative_anchor": "Day 1 evening", "weather": "clear", } def test_meanwhile_started_creates_scene_with_correct_present_set_kind(tmp_path): db = tmp_path / "t.db" apply_migrations(db) with open_db(db) as conn: append_event( conn, kind="bot_authored", payload=_bot_payload("bot_a", "BotA") ) append_event( conn, kind="bot_authored", payload=_bot_payload("bot_b", "BotB") ) append_event(conn, kind="chat_created", payload=_chat_payload()) # Parent (you-scene) — uses existing scene_opened handler. Will get # the default present_set_kind='you_host' from the new column. append_event( conn, kind="scene_opened", payload={ "chat_id": "chat_bot_a", "started_at": "2026-04-26T20:00:00+00:00", "participants": ["you", "bot_a", "bot_b"], }, ) # Now the meanwhile child scene — bot_a + bot_b only. append_event( conn, kind="meanwhile_scene_started", payload={ "scene_id": 2, "chat_id": "chat_bot_a", "parent_scene_id": 1, "host_bot_id": "bot_a", "guest_bot_id": "bot_b", "started_at": "2026-04-26T20:05:00+00:00", }, ) project(conn) meanwhile_scenes = list_meanwhile_scenes( conn, "chat_bot_a", status="active" ) assert len(meanwhile_scenes) == 1 m = meanwhile_scenes[0] assert m["id"] == 2 assert m["chat_id"] == "chat_bot_a" assert m["status"] == "active" assert m["present_set_kind"] == "host_guest" assert m["parent_scene_id"] == 1 assert m["started_at"] == "2026-04-26T20:05:00+00:00" assert m["closed_at"] is None # Parent linkage helper. parent = get_parent_scene(conn, 2) assert parent is not None assert parent["id"] == 1 assert parent["present_set_kind"] == "you_host" def test_meanwhile_closed_marks_scene_closed(tmp_path): db = tmp_path / "t.db" apply_migrations(db) with open_db(db) as conn: append_event( conn, kind="bot_authored", payload=_bot_payload("bot_a", "BotA") ) append_event( conn, kind="bot_authored", payload=_bot_payload("bot_b", "BotB") ) append_event(conn, kind="chat_created", payload=_chat_payload()) append_event( conn, kind="scene_opened", payload={ "chat_id": "chat_bot_a", "started_at": "2026-04-26T20:00:00+00:00", "participants": ["you", "bot_a", "bot_b"], }, ) append_event( conn, kind="meanwhile_scene_started", payload={ "scene_id": 2, "chat_id": "chat_bot_a", "parent_scene_id": 1, "host_bot_id": "bot_a", "guest_bot_id": "bot_b", "started_at": "2026-04-26T20:05:00+00:00", }, ) append_event( conn, kind="meanwhile_scene_closed", payload={ "scene_id": 2, "closed_at": "2026-04-26T20:15:00+00:00", }, ) project(conn) assert list_meanwhile_scenes(conn, "chat_bot_a", status="active") == [] closed = list_meanwhile_scenes(conn, "chat_bot_a", status="closed") assert len(closed) == 1 assert closed[0]["id"] == 2 assert closed[0]["status"] == "closed" assert closed[0]["closed_at"] == "2026-04-26T20:15:00+00:00" def test_active_you_scene_can_coexist_with_active_meanwhile_scene(tmp_path): db = tmp_path / "t.db" apply_migrations(db) with open_db(db) as conn: append_event( conn, kind="bot_authored", payload=_bot_payload("bot_a", "BotA") ) append_event( conn, kind="bot_authored", payload=_bot_payload("bot_b", "BotB") ) append_event(conn, kind="chat_created", payload=_chat_payload()) append_event( conn, kind="scene_opened", payload={ "chat_id": "chat_bot_a", "started_at": "2026-04-26T20:00:00+00:00", "participants": ["you", "bot_a", "bot_b"], }, ) append_event( conn, kind="meanwhile_scene_started", payload={ "scene_id": 2, "chat_id": "chat_bot_a", "parent_scene_id": 1, "host_bot_id": "bot_a", "guest_bot_id": "bot_b", "started_at": "2026-04-26T20:05:00+00:00", }, ) project(conn) # The you-scene is still the active scene from the chat's POV. you_scene = active_scene(conn, "chat_bot_a") assert you_scene is not None assert you_scene["id"] == 1 # And the meanwhile child is independently active. meanwhile_scenes = list_meanwhile_scenes( conn, "chat_bot_a", status="active" ) assert len(meanwhile_scenes) == 1 assert meanwhile_scenes[0]["id"] == 2 assert meanwhile_scenes[0]["present_set_kind"] == "host_guest" # Cross-check via raw query: one row per present_set_kind, both unended. rows = conn.execute( "SELECT id, present_set_kind FROM scenes " "WHERE chat_id = ? AND ended_at IS NULL ORDER BY id", ("chat_bot_a",), ).fetchall() kinds = sorted(r[1] for r in rows) assert kinds == ["host_guest", "you_host"] def test_meanwhile_digest_created_and_consumed(tmp_path): db = tmp_path / "t.db" apply_migrations(db) with open_db(db) as conn: append_event( conn, kind="bot_authored", payload=_bot_payload("bot_a", "BotA") ) append_event( conn, kind="bot_authored", payload=_bot_payload("bot_b", "BotB") ) append_event(conn, kind="chat_created", payload=_chat_payload()) append_event( conn, kind="scene_opened", payload={ "chat_id": "chat_bot_a", "started_at": "2026-04-26T20:00:00+00:00", "participants": ["you", "bot_a", "bot_b"], }, ) append_event( conn, kind="meanwhile_scene_started", payload={ "scene_id": 2, "chat_id": "chat_bot_a", "parent_scene_id": 1, "host_bot_id": "bot_a", "guest_bot_id": "bot_b", "started_at": "2026-04-26T20:05:00+00:00", }, ) append_event( conn, kind="meanwhile_digest_created", payload={ "scene_id": 2, "chat_id": "chat_bot_a", "summary": "BotA confides in BotB about the missing file.", }, ) project(conn) pending = list_pending_meanwhile_digests(conn, "chat_bot_a") assert len(pending) == 1 digest_id = pending[0]["id"] assert pending[0]["scene_id"] == 2 assert pending[0]["summary"].startswith("BotA confides") # Use append_and_apply for the second beat: re-running project() # would re-fire non-idempotent handlers (chat_created, scene_opened) # whose INSERTs conflict on UNIQUE constraints. from chat.eventlog.log import append_and_apply append_and_apply( conn, kind="meanwhile_digest_consumed", payload={ "digest_id": digest_id, "consumed_at": "2026-04-26T20:30:00+00:00", }, ) assert list_pending_meanwhile_digests(conn, "chat_bot_a") == []