fix(chat): raise pre-token guard from 120 s to 300 s
The client-side TURN_TIMEOUT_MS was the binding constraint: turns with slow prompt processing (no first SSE frame within 120 s of visible time) errored with the 'stuck' copy even though nginx (300 s) and BOR_LLM_TIMEOUT (300 s) would have let them run. Raise the guard to 300 s so the upstream timeouts are reachable, and re-pin the tests: the unit constant pins and the fake-clock E2E timeline (295 s hidden + 290 s after the re-arm = 585 s: past the original 300 s deadline, short of the re-armed 595 s deadline). Verified: tests/unit (full, 100% pass), tests/e2e/test_hidden_tab_stream.py and tests/e2e/test_loading_feedback.py in isolation.
This commit is contained in:
@@ -27,13 +27,13 @@ def _html() -> str:
|
||||
return (FRONTEND / "index.html").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_turn_timeout_constant_exported_at_120s() -> None:
|
||||
"""The 120s client-side guard (PLAN §7.4) must be an *exported*
|
||||
def test_turn_timeout_constant_exported_at_300s() -> None:
|
||||
"""The 300s client-side guard (PLAN §7.4) must be an *exported*
|
||||
constant — testable, and the single value the E2E timeout story keys
|
||||
off."""
|
||||
js = _js()
|
||||
match = re.search(r"export\s+const\s+TURN_TIMEOUT_MS\s*=\s*120_?000\s*;", js)
|
||||
assert match, "app.js must export `const TURN_TIMEOUT_MS = 120000`"
|
||||
match = re.search(r"export\s+const\s+TURN_TIMEOUT_MS\s*=\s*300_?000\s*;", js)
|
||||
assert match, "app.js must export `const TURN_TIMEOUT_MS = 300000`"
|
||||
|
||||
|
||||
def test_state_machine_has_all_four_states() -> None:
|
||||
@@ -69,7 +69,7 @@ def test_typing_indicator_contract_strings() -> None:
|
||||
|
||||
|
||||
def test_error_banner_has_actionable_hint() -> None:
|
||||
"""Every error path (SSE error event, non-2xx, 120s timeout) must
|
||||
"""Every error path (SSE error event, non-2xx, 300s timeout) must
|
||||
surface the same actionable retry hint (story AC4)."""
|
||||
js = _js()
|
||||
assert "check the LLM is reachable" in js
|
||||
@@ -118,7 +118,7 @@ def test_busy_button_style_tokens() -> None:
|
||||
|
||||
def test_thinking_event_is_a_first_class_turn_branch() -> None:
|
||||
"""Phase 17: `thinking` SSE frames stream live into the collapsible
|
||||
Thinking block — the typing dots make way, the 120s pre-token guard
|
||||
Thinking block — the typing dots make way, the 300s pre-token guard
|
||||
clears (the stream is alive), and the text renders through the
|
||||
escape-first markdown renderer (XSS-safe). While open, the stream is
|
||||
pinned to the bottom of the block."""
|
||||
@@ -129,7 +129,7 @@ def test_thinking_event_is_a_first_class_turn_branch() -> None:
|
||||
branch = js[thinking_idx:delta_idx]
|
||||
assert "thinkingAcc += ev.text" in branch
|
||||
assert "sawThinking = true" in branch
|
||||
assert "clearTurnTimeout()" in branch, "first thinking frame clears the 120s guard"
|
||||
assert "clearTurnTimeout()" in branch, "first thinking frame clears the 300s guard"
|
||||
assert "removeTyping()" in branch, "the live block replaces the typing dots"
|
||||
assert "ensureThinkingBlock(wrap)" in branch
|
||||
assert "renderMarkdown(thinkingAcc)" in branch, "escape-first renderer (XSS-safe)"
|
||||
@@ -232,7 +232,7 @@ def test_in_flight_button_is_the_stop_control() -> None:
|
||||
def test_abort_plumbing_owns_the_fetch() -> None:
|
||||
"""The in-flight fetch is owned by an AbortController created at
|
||||
turn start (module scope, cleared in the finally), passed to the
|
||||
fetch as its signal; the 120s guard aborts the same controller as
|
||||
fetch as its signal; the 300s guard aborts the same controller as
|
||||
its backstop — with `aborted = true` FIRST, so the catch never reads
|
||||
the guard's abort as a user stop (one owner, same outcome)."""
|
||||
js = _js()
|
||||
@@ -614,7 +614,7 @@ def test_retry_frame_is_a_first_class_branch_between_tool_and_delta() -> None:
|
||||
"""Phase 67 (owner-locked A4, task 02's contract): a `retry` SSE
|
||||
frame (the server restarted the LLM request before its first piece
|
||||
— locked A2) is a first-class branch in runTurn's handler, ordered
|
||||
BETWEEN the `tool` and `delta` branches. It clears the 120s guard
|
||||
BETWEEN the `tool` and `delta` branches. It clears the 300s guard
|
||||
(a frame arrived), resolves n/N from the frame, and writes the
|
||||
owner-locked copy literal onto the EXISTING channels only — the
|
||||
#send-status live region + the typing-indicator aria-label. No DOM
|
||||
@@ -630,7 +630,7 @@ def test_retry_frame_is_a_first_class_branch_between_tool_and_delta() -> None:
|
||||
)
|
||||
assert js.count('ev.type === "retry"') == 1, "exactly one retry branch"
|
||||
branch = js[retry_idx:delta_idx]
|
||||
assert "clearTurnTimeout()" in branch, "a frame arrived — the 120s guard clears"
|
||||
assert "clearTurnTimeout()" in branch, "a frame arrived — the 300s guard clears"
|
||||
assert "Number(ev.attempt)" in branch, "n = the attempt about to be tried"
|
||||
assert "Number(ev.max_attempts)" in branch, "N = the configured total"
|
||||
assert (
|
||||
|
||||
Reference in New Issue
Block a user