77 lines
3.1 KiB
Python
77 lines
3.1 KiB
Python
"""UI journey: reload hydrates history from the snapshot, then continue.
|
|
|
|
A reload kills the SPA's in-memory state, so the turn-1 bubble can only
|
|
re-render from ``GET /v1/sessions/{id}``; the post-reload turn proves
|
|
the re-established stream still dispatches and sees the old context.
|
|
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import uuid
|
|
|
|
import pytest
|
|
from playwright.sync_api import Page, expect
|
|
|
|
from tests.e2e_ui.conftest import configure_mock_llm
|
|
|
|
_COMPOSER = "Ask the agent anything…"
|
|
_ASSISTANT = '[data-testid="message-bubble"][data-role="assistant"]'
|
|
_USER = '[data-testid="message-bubble"][data-role="user"]'
|
|
_WORKING = '[data-testid="working-indicator"]'
|
|
|
|
# Turn-2 prompt's stable substring — present only in turn 2, so the mock can
|
|
# content-route it to the token-echo response.
|
|
_REPEAT_PROMPT = "Repeat the token from my first message, exactly, and nothing else."
|
|
|
|
|
|
@pytest.mark.nightly
|
|
def test_reload_hydrates_history_and_continues(
|
|
page: Page,
|
|
seeded_session: tuple[str, str],
|
|
mock_llm_server_url: str,
|
|
) -> None:
|
|
base_url, session_id = seeded_session
|
|
token = f"ui-reload-{uuid.uuid4().hex[:8]}"
|
|
|
|
# Turn 1 (the only message carrying the token) replies "stored"; turn 2
|
|
# (the repeat prompt) replies with the token. Content-routing keys each
|
|
# queue to a substring unique to its turn, so both fire deterministically.
|
|
configure_mock_llm(mock_llm_server_url, [{"text": "stored"}], key="reload-store", match=token)
|
|
configure_mock_llm(
|
|
mock_llm_server_url, [{"text": token}], key="reload-echo", match=_REPEAT_PROMPT
|
|
)
|
|
|
|
page.goto(f"{base_url}/c/{session_id}")
|
|
|
|
composer = page.get_by_placeholder(_COMPOSER)
|
|
expect(composer).to_be_visible()
|
|
composer.fill(
|
|
f"Remember this token exactly, you will be asked to repeat it "
|
|
f"verbatim later: {token}. Reply with just the word 'stored'."
|
|
)
|
|
page.get_by_role("button", name="Send", exact=True).click()
|
|
expect(page.locator(_ASSISTANT).first).to_be_visible(timeout=60_000)
|
|
expect(page.locator(_WORKING)).to_have_count(0, timeout=60_000)
|
|
|
|
page.reload()
|
|
|
|
# Hydration: the optimistic in-memory bubble died with the reload,
|
|
# so a visible turn-1 user bubble must have come from the snapshot.
|
|
expect(page.locator(_USER, has_text=token).first).to_be_visible(timeout=15_000)
|
|
|
|
composer = page.get_by_placeholder(_COMPOSER)
|
|
expect(composer).to_be_visible()
|
|
composer.fill(_REPEAT_PROMPT)
|
|
page.get_by_role("button", name="Send", exact=True).click()
|
|
|
|
# Turn 2 rendered (2 user bubbles) and produced a second assistant
|
|
# bubble; asserting the count first keeps the `.last` content check
|
|
# from matching a turn-1 echo of the token.
|
|
expect(page.locator(_USER)).to_have_count(2, timeout=15_000)
|
|
expect(page.locator(_ASSISTANT).nth(1)).to_be_visible(timeout=60_000)
|
|
expect(page.locator(_WORKING)).to_have_count(0, timeout=60_000)
|
|
# Post-reload context retention: only server-side history can
|
|
# supply the token after the in-memory state was destroyed.
|
|
expect(page.locator(_ASSISTANT, has_text=token).last).to_be_visible(timeout=15_000)
|