"""Streaming UX tests (T34): cancel route, recent-dialogue user_turn_edit inclusion, and the chat-shell embeds the streaming JS hooks. The cancel route is exercised at the no-op level only — the full mid-stream cancel path is covered indirectly by T19's CancelledError handling. We verify here that the route itself is registered and silently 204s when no in-flight task exists, since the JS Stop button fires unconditionally. The user_turn_edit inclusion test is the T29 follow-up fix: without it, the original user_turn drops out of the timeline (correctly) but the edited prose never lands (incorrectly), so the rendered chat detail is missing the user's most recent words. """ from __future__ import annotations from pathlib import Path import pytest from fastapi.testclient import TestClient from chat.app import app from chat.db.connection import open_db from chat.eventlog.log import append_event from chat.eventlog.projector import project @pytest.fixture def client(tmp_path, monkeypatch): cfg = tmp_path / "config.toml" cfg.write_text('featherless_api_key = "test"\n') monkeypatch.setenv("CHAT_CONFIG_PATH", str(cfg)) db = tmp_path / "test.db" monkeypatch.setenv("CHAT_DB_PATH", str(db)) with TestClient(app) as c: # Disable the lifespan-managed background worker so it doesn't # try to score significance through Featherless with the fake key. worker = getattr(app.state, "background_worker", None) if worker is not None: worker.enabled = False yield c def _seed_chat( db_path: Path, bot_id: str = "bot_a", chat_id: str = "chat_bot_a", ) -> None: """Seed a bot + chat with the activity rows the prompt assembler expects.""" with open_db(db_path) as conn: append_event( conn, kind="bot_authored", payload={ "id": bot_id, "name": "BotA", "persona": "...", "voice_samples": [], "traits": [], "backstory": "", "initial_relationship_to_you": "", "kickoff_prose": "...", }, ) append_event( conn, kind="chat_created", payload={ "id": chat_id, "host_bot_id": bot_id, "initial_time": "2026-04-26T20:00:00+00:00", "narrative_anchor": "Day 1", "weather": "", }, ) append_event( conn, kind="edge_update", payload={ "source_id": bot_id, "target_id": "you", "chat_id": chat_id, }, ) append_event( conn, kind="edge_update", payload={ "source_id": "you", "target_id": bot_id, "chat_id": chat_id, }, ) append_event( conn, kind="activity_change", payload={ "entity_id": "you", "posture": "sitting", "action": {"verb": "talking"}, }, ) append_event( conn, kind="activity_change", payload={ "entity_id": bot_id, "posture": "sitting", "action": {"verb": "listening"}, }, ) project(conn) def test_cancel_route_no_op_when_no_in_flight(client, tmp_path): """Hitting cancel with nothing streaming returns 204 silently.""" _seed_chat(tmp_path / "test.db") response = client.post("/chats/chat_bot_a/turns/cancel") assert response.status_code == 204 def test_user_turn_edit_appears_in_recent_dialogue(client, tmp_path): """The chat-detail timeline includes a user_turn_edit's prose. Original user_turn is superseded by the edit, so it drops out, but the edit's prose should render in its place. """ db_path = tmp_path / "test.db" _seed_chat(db_path) with open_db(db_path) as conn: ut_id = append_event( conn, kind="user_turn", payload={ "chat_id": "chat_bot_a", "prose": "OriginalUserText", "segments": [], }, ) edit_id = append_event( conn, kind="user_turn_edit", payload={ "chat_id": "chat_bot_a", "prose": "EditedUserText", "supersedes_user_turn_id": ut_id, }, ) conn.execute( "UPDATE event_log SET superseded_by = ? WHERE id = ?", (edit_id, ut_id), ) conn.commit() # No project() call — user_turn / user_turn_edit have no projector # handlers (transcript-only kinds), and re-projecting would replay # chat_created and trip its UNIQUE constraint. response = client.get("/chats/chat_bot_a") assert response.status_code == 200 body = response.text assert "EditedUserText" in body # The original (now-superseded) prose must not render. assert "OriginalUserText" not in body def test_chat_html_includes_stop_streaming_script(client, tmp_path): """The chat shell embeds the streaming-JS hooks (Stop button + send-lock).""" _seed_chat(tmp_path / "test.db") response = client.get("/chats/chat_bot_a") assert response.status_code == 200 body = response.text # Either the CSS class for the Stop button or the JS state flag must # appear in the embedded script — both are load-bearing for T34. assert "stop-streaming" in body or "isStreaming" in body # Cancel route reference must be wired so the Stop button can call it. assert "/turns/cancel" in body