177 lines
5.7 KiB
Python
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
|