"""Pin the contract: ``_apply_chat_created`` is NOT replay-safe. See ``docs/audits/2026-04-27-project-callers.md`` for the full audit. The handler at ``chat/state/world.py:_apply_chat_created`` uses raw ``INSERT INTO chats ...`` and ``INSERT INTO chat_state ...`` with no ``OR REPLACE``/``OR IGNORE``. Running ``project()`` twice over the same ``chat_created`` event MUST raise ``sqlite3.IntegrityError`` on the second pass — this is the bug that produced the 500 fixed in commit ``0f8bf94`` (and the latent equivalents fixed in this commit). Pinning the contract here means any future "make it idempotent" change to the handler MUST update this test, which forces a deliberate review of the trade-offs: most notably, that ``chat_state`` columns mutated by later events (``time_skip_elision`` bumps ``time``; ``scene_opened``/``scene_closed`` toggle ``active_scene_id``) would be silently overwritten by an ``INSERT OR REPLACE`` on every replay. The audit explains why we keep the handler raw-INSERT and enforce the rule at the call site via ``append_and_apply`` instead. """ from __future__ import annotations import sqlite3 import pytest 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.world # noqa: F401 — import registers the handler def _chat_payload(): return { "id": "chat_bot_a", "host_bot_id": "bot_a", "guest_bot_id": None, "initial_time": "2026-04-27T12:00:00+00:00", "narrative_anchor": "Day 1 noon", "weather": "clear", } def test_chat_created_handler_is_not_replay_safe(tmp_path): """A second projection over an extra ``chat_created`` for the same id raises. This is the exact failure shape from incident ``0f8bf94``: a raw INSERT against ``chats.id`` (PK) trips ``UNIQUE constraint failed`` on the second pass. If this test ever starts FAILING (i.e. the second project() succeeds), someone has changed the handler to be idempotent — read the audit before approving. """ db = tmp_path / "t.db" apply_migrations(db) with open_db(db) as conn: # First chat_created + first project: must succeed. append_event(conn, kind="chat_created", payload=_chat_payload()) project(conn) # Append a SECOND chat_created with the same id. project() will # walk both, re-INSERT the same chats row, and trip the UNIQUE # constraint on chats.id. append_event(conn, kind="chat_created", payload=_chat_payload()) with pytest.raises(sqlite3.IntegrityError) as exc_info: project(conn) # Match on the column to make sure we caught the *intended* # constraint, not some unrelated FK/check failure that happens # to also be an IntegrityError. assert "chats.id" in str(exc_info.value)