Files
chat/tests/test_streaming_ux.py

177 lines
5.7 KiB
Python

"""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