feat: event-completion promotion service (T56)
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
"""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") == []
|
||||
Reference in New Issue
Block a user