270 lines
8.9 KiB
Python
270 lines
8.9 KiB
Python
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") == []
|