feat(chat): stop an in-flight answer — Send becomes Stop, the partial is kept and persisted, the model stream is torn down
This commit is contained in:
@@ -9,7 +9,8 @@ Determinism comes from two mock-LLM behaviors (tests/e2e/mock_llm.py):
|
||||
|
||||
* ``pretend to think slowly`` in the user message → a 3s warm-up before
|
||||
the first token, wide enough to assert the pre-token UI (typing dots +
|
||||
busy "Thinking…" button) at a known timestamp;
|
||||
the enabled Stop button — phase 48 revised the busy-button contract) at
|
||||
a known timestamp;
|
||||
* the ``POST /__shutdown__`` hook → the ``llm_down`` fixture stops the
|
||||
shared mock to simulate an LLM outage, then restores a fresh instance
|
||||
on the same port so later tests keep working.
|
||||
@@ -205,8 +206,10 @@ def test_typing_indicator_during_slow_think(
|
||||
def test_button_state_machine(
|
||||
page: Page, app_url: str, mock_llm: int, db_ready: None
|
||||
) -> None:
|
||||
"""AC1/AC3: disabled + spinner + 'Thinking…' while in flight; enabled
|
||||
+ 'Send' + focused input after done."""
|
||||
"""AC1/AC3 (phase 48 revised contract, owner-locked 2026-08-29):
|
||||
in flight the button is the enabled Stop control — "Stop" label,
|
||||
.is-stop class, spinner hidden; after done it is enabled + "Send"
|
||||
(class removed) + the input is focused back."""
|
||||
_reset_db(mock_llm, seed=True)
|
||||
page.set_default_timeout(30_000)
|
||||
page.goto(app_url)
|
||||
@@ -216,15 +219,17 @@ def test_button_state_machine(
|
||||
|
||||
btn = page.locator("#send-btn")
|
||||
label = page.locator("#send-label")
|
||||
expect(btn).to_be_disabled(timeout=500)
|
||||
expect(label).to_have_text("Thinking…")
|
||||
expect(btn.locator(".spinner")).to_be_visible()
|
||||
expect(btn).to_be_enabled(timeout=500)
|
||||
expect(label).to_have_text("Stop")
|
||||
expect(btn).to_have_class(re.compile(r"is-stop"))
|
||||
expect(btn.locator(".spinner")).to_be_hidden()
|
||||
expect(page.locator("#send-status")).to_contain_text("thinking")
|
||||
|
||||
# Done: button recovers and the input is focused back.
|
||||
# Done: button recovers to the Send state and the input is focused back.
|
||||
page.locator(ANSWER).wait_for(state="visible", timeout=30_000)
|
||||
expect(label).to_have_text("Send", timeout=30_000)
|
||||
expect(btn).to_be_enabled()
|
||||
expect(btn).not_to_have_class(re.compile(r"is-stop"))
|
||||
expect(btn.locator(".spinner")).to_be_hidden()
|
||||
expect(page.locator("#message-input")).to_be_focused()
|
||||
|
||||
@@ -266,8 +271,10 @@ def test_streaming_appends_live(
|
||||
def test_reduced_motion_keeps_feedback(
|
||||
page: Page, app_url: str, mock_llm: int, db_ready: None
|
||||
) -> None:
|
||||
"""AC7: under prefers-reduced-motion the dots/spinner stay visible
|
||||
(static/slower) — feedback is never removed, only calmed."""
|
||||
"""AC7: under prefers-reduced-motion the typing dots stay visible
|
||||
(static) — feedback is never removed, only calmed. Phase 48: the
|
||||
in-flight button is the enabled Stop control (the spinner is no
|
||||
longer part of the in-flight feedback)."""
|
||||
_reset_db(mock_llm, seed=True)
|
||||
page.emulate_media(reduced_motion="reduce")
|
||||
page.set_default_timeout(30_000)
|
||||
@@ -278,7 +285,9 @@ def test_reduced_motion_keeps_feedback(
|
||||
|
||||
expect(page.locator(TYPING)).to_be_visible(timeout=500)
|
||||
expect(page.locator(TYPING).locator(".bubble span")).to_have_count(3)
|
||||
expect(page.locator("#send-btn .spinner")).to_be_visible()
|
||||
expect(page.locator("#send-btn")).to_have_class(re.compile(r"is-stop"))
|
||||
expect(page.locator("#send-label")).to_have_text("Stop")
|
||||
expect(page.locator("#send-btn .spinner")).to_be_hidden()
|
||||
|
||||
# And the turn still completes and recovers.
|
||||
expect(page.locator("#send-label")).to_have_text("Send", timeout=30_000)
|
||||
|
||||
Reference in New Issue
Block a user