"""Tests for the event-completion promotion service (T56). When an event reaches ``status='completed'``, the orchestrator promotes structured artifacts the event carried (``acquired_objects``, ``knowledge_facts``, ``relationship_change``) into the appropriate state stores via downstream events. Cancelled / expired events do NOT promote — the closed event row is left in place but no follow-on events fire. """ from __future__ import annotations import json 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 from chat.services.event_promotion import promote_completed_event from chat.state.edges import get_edge import chat.state.edges # noqa: F401 - register edge_update handler import chat.state.entities # noqa: F401 - register handlers import chat.state.events # noqa: F401 - register events handlers import chat.state.manual_edit # noqa: F401 - register manual_edit handler import chat.state.world # noqa: F401 - register handlers 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 _seed_chat(conn) -> None: 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()) def _seed_event( conn, *, event_id: str, props: dict, terminal_kind: str = "event_completed", ) -> None: """Append event_planned, then a terminal transition (default completed).""" append_event( conn, kind="event_planned", payload={ "event_id": event_id, "chat_id": "chat_bot_a", "kind": "story_event", "props": props, "planned_for": "2026-04-30T18:00:00+00:00", }, ) append_event( conn, kind=terminal_kind, payload={ "event_id": event_id, "completed_at": "2026-04-30T20:00:00+00:00", }, ) project(conn) def _max_event_id(conn) -> int: return conn.execute("SELECT COALESCE(MAX(id), 0) FROM event_log").fetchone()[0] def _events_after(conn, after_id: int, kind: str) -> list[dict]: rows = conn.execute( "SELECT id, kind, payload_json FROM event_log " "WHERE id > ? AND kind = ? ORDER BY id ASC", (after_id, kind), ).fetchall() return [ {"id": r[0], "kind": r[1], "payload": json.loads(r[2])} for r in rows ] def test_empty_props_no_op(tmp_path): """Completed event with empty props produces no promotion events.""" db = tmp_path / "t.db" apply_migrations(db) with open_db(db) as conn: _seed_chat(conn) _seed_event(conn, event_id="evt_empty", props={}) before = _max_event_id(conn) counts = promote_completed_event( conn, event_id="evt_empty", chat_id="chat_bot_a", chat_clock_at="2026-04-30T20:00:00+00:00", ) assert counts == { "acquired_objects": 0, "knowledge_facts": 0, "relationship_change": 0, } # No new edge_update or manual_edit rows after the promote call. assert _events_after(conn, before, "edge_update") == [] assert _events_after(conn, before, "manual_edit") == [] def test_knowledge_facts_emits_edge_update(tmp_path): """A knowledge_facts entry promotes to an edge_update on the directed edge.""" db = tmp_path / "t.db" apply_migrations(db) with open_db(db) as conn: _seed_chat(conn) _seed_event( conn, event_id="evt_kf", props={ "knowledge_facts": [ { "owner_id": "bot_a", "target_id": "you", "fact": "Maya prefers tea over coffee", } ] }, ) before = _max_event_id(conn) counts = promote_completed_event( conn, event_id="evt_kf", chat_id="chat_bot_a", chat_clock_at="2026-04-30T20:00:00+00:00", ) assert counts["knowledge_facts"] == 1 assert counts["acquired_objects"] == 0 assert counts["relationship_change"] == 0 # An edge_update event landed in the event_log AFTER the promote call. new_edge_updates = _events_after(conn, before, "edge_update") assert len(new_edge_updates) == 1 payload = new_edge_updates[0]["payload"] assert payload["source_id"] == "bot_a" assert payload["target_id"] == "you" assert payload["knowledge_facts"] == ["Maya prefers tea over coffee"] # And the projected edge has the fact applied. edge = get_edge(conn, "bot_a", "you") assert edge is not None assert "Maya prefers tea over coffee" in edge["knowledge"] def test_relationship_change_emits_manual_edit(tmp_path): """A relationship_change promotes to a manual_edit edge_summary.""" db = tmp_path / "t.db" apply_migrations(db) with open_db(db) as conn: _seed_chat(conn) _seed_event( conn, event_id="evt_rc", props={ "relationship_change": { "source_id": "bot_a", "target_id": "you", "summary": "they're now dating", } }, ) before = _max_event_id(conn) counts = promote_completed_event( conn, event_id="evt_rc", chat_id="chat_bot_a", chat_clock_at="2026-04-30T20:00:00+00:00", ) assert counts["relationship_change"] == 1 assert counts["knowledge_facts"] == 0 assert counts["acquired_objects"] == 0 new_manual_edits = _events_after(conn, before, "manual_edit") # Filter to edge_summary only — Phase 3 stub may also emit # memory_pov_summary entries for acquired_objects, but here there # are none. edge_summary_edits = [ m for m in new_manual_edits if m["payload"].get("target_kind") == "edge_summary" ] assert len(edge_summary_edits) == 1 payload = edge_summary_edits[0]["payload"] assert payload["target_kind"] == "edge_summary" assert payload["target_id"] == {"source_id": "bot_a", "target_id": "you"} assert payload["new_value"] == "they're now dating" def test_cancelled_event_does_not_promote(tmp_path): """Cancelled events have promotable props ignored — no follow-on events.""" db = tmp_path / "t.db" apply_migrations(db) with open_db(db) as conn: _seed_chat(conn) _seed_event( conn, event_id="evt_canx", props={ "knowledge_facts": [ {"owner_id": "bot_a", "target_id": "you", "fact": "x"} ], "relationship_change": { "source_id": "bot_a", "target_id": "you", "summary": "ignored", }, }, terminal_kind="event_cancelled", ) before = _max_event_id(conn) counts = promote_completed_event( conn, event_id="evt_canx", chat_id="chat_bot_a", chat_clock_at="2026-04-30T20:00:00+00:00", ) assert counts == { "acquired_objects": 0, "knowledge_facts": 0, "relationship_change": 0, } assert _events_after(conn, before, "edge_update") == [] assert _events_after(conn, before, "manual_edit") == []