feat: frontend turn_html_replace SSE handler for regenerate live-swap (T86)
This commit is contained in:
@@ -85,3 +85,26 @@ def test_render_prose_mixed_full_message():
|
||||
assert '<em class="action">looks up</em>' in out
|
||||
# The apostrophe in ``she's`` is HTML-escaped to ``'``.
|
||||
assert '<span class="ooc">((she's tired))</span>' in out
|
||||
|
||||
|
||||
def test_render_turn_html_stamps_event_id_when_provided():
|
||||
"""T86 follow-up: when ``event_id`` is supplied the wrapper DIV
|
||||
carries ``id="turn-<event_id>"`` so the chat-page
|
||||
``turn_html_replace`` SSE handler can locate the prior turn DOM
|
||||
node by id and swap it in-place. Without the id the handler's
|
||||
``getElementById('turn-' + supersedes_id)`` lookup misses and
|
||||
the regenerated turn appends instead of replaces.
|
||||
"""
|
||||
out = render_turn_html("BotA", "Hello.", role="bot", event_id=42)
|
||||
assert 'id="turn-42"' in out
|
||||
# The id must sit on the wrapper DIV, not somewhere nested inside.
|
||||
assert out.startswith('<div id="turn-42" class="turn turn-bot">')
|
||||
|
||||
|
||||
def test_render_turn_html_omits_id_when_event_id_missing():
|
||||
"""Legacy callers (no ``event_id`` passed) get a clean DIV with no
|
||||
id attribute — preserves the pre-T86 fragment shape.
|
||||
"""
|
||||
out = render_turn_html("BotA", "Hello.", role="bot")
|
||||
assert "id=" not in out
|
||||
assert out.startswith('<div class="turn turn-bot">')
|
||||
|
||||
@@ -174,3 +174,74 @@ def test_chat_html_includes_stop_streaming_script(client, tmp_path):
|
||||
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
|
||||
|
||||
|
||||
def test_chat_html_has_turn_html_replace_listener(client, tmp_path):
|
||||
"""T86: the chat shell wires a JS handler for the ``turn_html_replace``
|
||||
SSE event so regenerate-driven swaps land in connected tabs without a
|
||||
page refresh.
|
||||
|
||||
This is a presence / string-check test: it verifies the handler is
|
||||
embedded in the rendered template but does NOT drive a real browser
|
||||
(no headless runner is wired into this test environment). The end-to-
|
||||
end behaviour — receiving the event over SSE and replacing the prior
|
||||
turn's DOM node — is therefore not exercised here; a manual smoke
|
||||
check or future browser-driven test would close that gap.
|
||||
"""
|
||||
_seed_chat(tmp_path / "test.db")
|
||||
response = client.get("/chats/chat_bot_a")
|
||||
assert response.status_code == 200
|
||||
body = response.text
|
||||
# The handler must be wired against the SSE event name the backend
|
||||
# publishes (chat.services.regenerate -> "turn_html_replace").
|
||||
assert "turn_html_replace" in body
|
||||
# Confirm the handler reads the JSON payload's ``supersedes_id`` so
|
||||
# it can locate the prior turn node. The exact lookup mechanism may
|
||||
# vary, but the field name is part of the contract with the backend.
|
||||
assert "supersedes_id" in body
|
||||
|
||||
|
||||
def test_rendered_turn_html_includes_event_id(client, tmp_path):
|
||||
"""T86 follow-up: the chat-detail Jinja loop stamps
|
||||
``id="turn-<event_id>"`` on every rendered turn DIV. Without this id
|
||||
the ``turn_html_replace`` SSE handler's ``getElementById`` lookup
|
||||
misses, falls through to ``insertAdjacentHTML('beforeend', …)``, and
|
||||
the regenerated turn appears APPENDED instead of swapped in-place
|
||||
(rendering the primary handler path dead code — exactly the gap the
|
||||
T86 reviewer flagged).
|
||||
|
||||
Seed a user_turn + assistant_turn, GET the chat page, and assert the
|
||||
response body carries both turns' event ids on the wrapper DIVs.
|
||||
"""
|
||||
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": "hello bot",
|
||||
"segments": [],
|
||||
},
|
||||
)
|
||||
at_id = append_event(
|
||||
conn,
|
||||
kind="assistant_turn",
|
||||
payload={
|
||||
"chat_id": "chat_bot_a",
|
||||
"speaker_id": "bot_a",
|
||||
"text": "Hi there.",
|
||||
"truncated": False,
|
||||
"user_turn_id": ut_id,
|
||||
},
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
response = client.get("/chats/chat_bot_a")
|
||||
assert response.status_code == 200
|
||||
body = response.text
|
||||
# Both seeded turns must carry ``id="turn-<event_id>"`` so the SSE
|
||||
# in-place swap can find them.
|
||||
assert f'id="turn-{ut_id}"' in body
|
||||
assert f'id="turn-{at_id}"' in body
|
||||
|
||||
@@ -104,10 +104,16 @@ def test_read_recent_dialogue_returns_chronological_pairs(tmp_path):
|
||||
with open_db(db) as conn:
|
||||
out = read_recent_dialogue(conn, "chat_a", limit=10)
|
||||
|
||||
assert out == [
|
||||
{"speaker": "you", "text": "hello"},
|
||||
{"speaker": "bot_a", "text": "Original."},
|
||||
# Each entry now carries the source ``event_log.id`` as ``event_id``
|
||||
# (T86 follow-up) so the chat-detail Jinja loop can stamp
|
||||
# ``id="turn-<n>"`` on each rendered turn DIV — needed by the
|
||||
# ``turn_html_replace`` SSE handler for in-place regenerate swaps.
|
||||
speakers = [(e["speaker"], e["text"]) for e in out]
|
||||
assert speakers == [
|
||||
("you", "hello"),
|
||||
("bot_a", "Original."),
|
||||
]
|
||||
assert all("event_id" in e and isinstance(e["event_id"], int) for e in out)
|
||||
|
||||
|
||||
def test_read_recent_dialogue_filters_superseded_and_other_chats(tmp_path):
|
||||
|
||||
Reference in New Issue
Block a user