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:
+11
-11
@@ -22,7 +22,7 @@
|
|||||||
* SSE events), the live collapsible Thinking block IS the
|
* SSE events), the live collapsible Thinking block IS the
|
||||||
* visible feedback (it replaces the typing dots; the UI
|
* visible feedback (it replaces the typing dots; the UI
|
||||||
* state stays "thinking" — the button stays the "Stop"
|
* state stays "thinking" — the button stays the "Stop"
|
||||||
* control) and the 120s guard clears on the first
|
* control) and the 300s guard clears on the first
|
||||||
* thinking *or* delta event.
|
* thinking *or* delta event.
|
||||||
* • streaming — the first delta removes the dots and appends live into
|
* • streaming — the first delta removes the dots and appends live into
|
||||||
* the answer bubble (auto-collapsing the Thinking block,
|
* the answer bubble (auto-collapsing the Thinking block,
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
* SAME finally path — no error banner, no scroll
|
* SAME finally path — no error banner, no scroll
|
||||||
* (phase 42).
|
* (phase 42).
|
||||||
* • error — red banner (role="alert") with an actionable retry hint;
|
* • error — red banner (role="alert") with an actionable retry hint;
|
||||||
* the 120s guard (TURN_TIMEOUT_MS) catches hung pre-token
|
* the 300s guard (TURN_TIMEOUT_MS) catches hung pre-token
|
||||||
* streams and the sawDone guard (phase 17) catches a
|
* streams and the sawDone guard (phase 17) catches a
|
||||||
* stream that dies after frames but before `done`, so the
|
* stream that dies after frames but before `done`, so the
|
||||||
* button can never sit zombified.
|
* button can never sit zombified.
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
* request (up to BOR_LLM_RETRIES retries, BOR_LLM_RETRY_DELAY seconds
|
* request (up to BOR_LLM_RETRIES retries, BOR_LLM_RETRY_DELAY seconds
|
||||||
* apart — a flat delay, no backoff, owner-locked A3) and streams one
|
* apart — a flat delay, no backoff, owner-locked A3) and streams one
|
||||||
* `retry` SSE frame per wait. The handler treats it with the same
|
* `retry` SSE frame per wait. The handler treats it with the same
|
||||||
* status pattern as the `tool` frames: the 120s guard clears (a frame
|
* status pattern as the `tool` frames: the 300s guard clears (a frame
|
||||||
* arrived) and the EXISTING #send-status live region + the
|
* arrived) and the EXISTING #send-status live region + the
|
||||||
* typing-indicator aria-label read the owner-locked copy
|
* typing-indicator aria-label read the owner-locked copy
|
||||||
* "Communication interrupted — retrying (n of N)…" (n = the attempt
|
* "Communication interrupted — retrying (n of N)…" (n = the attempt
|
||||||
@@ -255,7 +255,7 @@
|
|||||||
* question even on browsers that fire pagehide on a merely-hidden tab
|
* question even on browsers that fire pagehide on a merely-hidden tab
|
||||||
* (e.g. Safari; Chromium fires only visibilitychange — task 01); a REAL
|
* (e.g. Safari; Chromium fires only visibilitychange — task 01); a REAL
|
||||||
* navigation never runs a settle, so the leave-save is unchanged there.
|
* navigation never runs a settle, so the leave-save is unchanged there.
|
||||||
* (2) The 120s pre-token guard counts only VISIBLE time: when the tab
|
* (2) The 300s pre-token guard counts only VISIBLE time: when the tab
|
||||||
* returns to visible with the guard still armed (it clears on the first
|
* returns to visible with the guard still armed (it clears on the first
|
||||||
* thinking/delta/retry frame), it re-arms with a fresh TURN_TIMEOUT_MS —
|
* thinking/delta/retry frame), it re-arms with a fresh TURN_TIMEOUT_MS —
|
||||||
* a slow first frame arriving while the tab was hidden past the deadline
|
* a slow first frame arriving while the tab was hidden past the deadline
|
||||||
@@ -305,7 +305,7 @@ const brand = () => window.BOR_BRAND || "Brain of Reese";
|
|||||||
* cleared on the first delta (entering "streaming") and on every
|
* cleared on the first delta (entering "streaming") and on every
|
||||||
* terminal transition. Exported so the constant is testable (tests/unit/
|
* terminal transition. Exported so the constant is testable (tests/unit/
|
||||||
* test_frontend_feedback.py). */
|
* test_frontend_feedback.py). */
|
||||||
export const TURN_TIMEOUT_MS = 120_000;
|
export const TURN_TIMEOUT_MS = 300_000;
|
||||||
|
|
||||||
/* Phase 87 (TODO.md L5): the latest tool line's visible "processing"
|
/* Phase 87 (TODO.md L5): the latest tool line's visible "processing"
|
||||||
* threshold (A5): below 5s a frameless gap reads as normal latency;
|
* threshold (A5): below 5s a frameless gap reads as normal latency;
|
||||||
@@ -1023,7 +1023,7 @@ async function loadHealth() {
|
|||||||
let uiState = UI_STATE.idle;
|
let uiState = UI_STATE.idle;
|
||||||
let thinkingClock = 0; // setInterval id — elapsed-seconds hint
|
let thinkingClock = 0; // setInterval id — elapsed-seconds hint
|
||||||
let thinkingStart = 0; // Date.now() when "thinking" began
|
let thinkingStart = 0; // Date.now() when "thinking" began
|
||||||
let turnTimeout = 0; // setTimeout id — 120s pre-token guard
|
let turnTimeout = 0; // setTimeout id — 300s pre-token guard
|
||||||
let turnTimeoutCb = null; // the guard's callback — lets visibilitychange re-arm it (phase 73)
|
let turnTimeoutCb = null; // the guard's callback — lets visibilitychange re-arm it (phase 73)
|
||||||
/* Phase 87 (TODO.md L5): the per-tool-line elapsed clock — one clock per
|
/* Phase 87 (TODO.md L5): the per-tool-line elapsed clock — one clock per
|
||||||
* turn, re-armed per `tool` frame: the baseline (toolLineStart) resets on
|
* turn, re-armed per `tool` frame: the baseline (toolLineStart) resets on
|
||||||
@@ -1057,7 +1057,7 @@ let persistedOnLeave = false; // pagehide partial-persist at most once
|
|||||||
let leavePartialIndex = -1; // index of this turn's pagehide partial (-1 = none)
|
let leavePartialIndex = -1; // index of this turn's pagehide partial (-1 = none)
|
||||||
/* Phase 48 (2026-08-29, TODO.md L3): the user-stop machinery.
|
/* Phase 48 (2026-08-29, TODO.md L3): the user-stop machinery.
|
||||||
* `turnAbort` owns the in-flight fetch (the Stop button aborts it; the
|
* `turnAbort` owns the in-flight fetch (the Stop button aborts it; the
|
||||||
* 120s guard aborts the same controller as its backstop); `stoppedByUser`
|
* 300s guard aborts the same controller as its backstop); `stoppedByUser`
|
||||||
* marks a turn the Stop button took, so the catch's AbortError can tell a
|
* marks a turn the Stop button took, so the catch's AbortError can tell a
|
||||||
* user stop from the guard's own abort (the guard sets `aborted` first).
|
* user stop from the guard's own abort (the guard sets `aborted` first).
|
||||||
* Both are turn-scoped: created/reset at the top of handleSend, cleared
|
* Both are turn-scoped: created/reset at the top of handleSend, cleared
|
||||||
@@ -1159,9 +1159,9 @@ function stopToolLineClock() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Phase 73 (task 02; task 01 C2 — confirmed): a merely-HIDDEN tab must
|
/* Phase 73 (task 02; task 01 C2 — confirmed): a merely-HIDDEN tab must
|
||||||
* never stop a turn, but the 120s pre-token guard is a plain setTimeout,
|
* never stop a turn, but the 300s pre-token guard is a plain setTimeout,
|
||||||
* so hidden time counted toward it: a turn whose first frame lands while
|
* so hidden time counted toward it: a turn whose first frame lands while
|
||||||
* the tab is hidden past the 120s mark errored with the "stuck" copy
|
* the tab is hidden past the 300s mark errored with the "stuck" copy
|
||||||
* (the user perceives "switching tabs killed the answer"). When the tab
|
* (the user perceives "switching tabs killed the answer"). When the tab
|
||||||
* returns to VISIBLE with the guard still armed (it is only armed in the
|
* returns to VISIBLE with the guard still armed (it is only armed in the
|
||||||
* pre-token window — cleared on the first thinking/delta/retry frame and
|
* pre-token window — cleared on the first thinking/delta/retry frame and
|
||||||
@@ -2089,7 +2089,7 @@ async function runTurn(text, { reask = false } = {}) {
|
|||||||
|
|
||||||
let wrap = null;
|
let wrap = null;
|
||||||
let res = null;
|
let res = null;
|
||||||
let aborted = false; // the 120s guard already took the turn to error
|
let aborted = false; // the 300s guard already took the turn to error
|
||||||
// Phase 20: acc / thinkingAcc / persistedOnLeave live at module scope
|
// Phase 20: acc / thinkingAcc / persistedOnLeave live at module scope
|
||||||
// (the pagehide handler reads them) but reset here, so they stay
|
// (the pagehide handler reads them) but reset here, so they stay
|
||||||
// turn-scoped exactly like the other turn locals. Phase 73:
|
// turn-scoped exactly like the other turn locals. Phase 73:
|
||||||
@@ -2339,7 +2339,7 @@ async function runTurn(text, { reask = false } = {}) {
|
|||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (aborted) {
|
if (aborted) {
|
||||||
// The 120s guard already took the turn to the error state — its
|
// The 300s guard already took the turn to the error state — its
|
||||||
// own abort surfaces here, and it is never read as a user stop.
|
// own abort surfaces here, and it is never read as a user stop.
|
||||||
} else if (stoppedByUser || err?.name === "AbortError") {
|
} else if (stoppedByUser || err?.name === "AbortError") {
|
||||||
// Phase 48 (owner-locked): the STOP path — no error banner. When
|
// Phase 48 (owner-locked): the STOP path — no error banner. When
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ mid-stream window) the pins are:
|
|||||||
question with NO dispatched events completes identically (guards
|
question with NO dispatched events completes identically (guards
|
||||||
against an over-eager fix changing the normal path).
|
against an over-eager fix changing the normal path).
|
||||||
4. ``test_pre_token_guard_survives_hidden_window`` — task 01's C2
|
4. ``test_pre_token_guard_survives_hidden_window`` — task 01's C2
|
||||||
(confirmed): hidden time must not count toward the 120 s pre-token
|
(confirmed): hidden time must not count toward the 300 s pre-token
|
||||||
guard. Playwright's fake clock makes the guard's deadline
|
guard. Playwright's fake clock makes the guard's deadline
|
||||||
deterministic: the clock is fast-forwarded past the guard's
|
deterministic: the clock is fast-forwarded past the guard's
|
||||||
ORIGINAL 120 s deadline while the tab is (synthetically) hidden,
|
ORIGINAL 300 s deadline while the tab is (synthetically) hidden,
|
||||||
with a return to visible in between — the phase-73 re-arm gives the
|
with a return to visible in between — the phase-73 re-arm gives the
|
||||||
still-armed guard a FRESH ``TURN_TIMEOUT_MS``, so the turn still
|
still-armed guard a FRESH ``TURN_TIMEOUT_MS``, so the turn still
|
||||||
settles with the answer. Without the re-arm the fast-forwarded
|
settles with the answer. Without the re-arm the fast-forwarded
|
||||||
@@ -91,7 +91,7 @@ LONG_ANSWER = long_answer()
|
|||||||
LONG_ANSWER_END = "LONG-ANSWER-END"
|
LONG_ANSWER_END = "LONG-ANSWER-END"
|
||||||
|
|
||||||
#: The mock's only pure pre-token silence (phase-06 phrasing): a 3 s
|
#: The mock's only pure pre-token silence (phase-06 phrasing): a 3 s
|
||||||
#: delay before the FIRST frame, so the 120 s pre-token guard stays
|
#: delay before the FIRST frame, so the 300 s pre-token guard stays
|
||||||
#: armed the whole window (no thinking/delta/retry frame to clear it).
|
#: armed the whole window (no thinking/delta/retry frame to clear it).
|
||||||
SLOW_QUESTION = "pretend to think slowly then tell me about kubernetes"
|
SLOW_QUESTION = "pretend to think slowly then tell me about kubernetes"
|
||||||
MOCK_ANSWER_MARKER = "Deterministic mock answer for E2E"
|
MOCK_ANSWER_MARKER = "Deterministic mock answer for E2E"
|
||||||
@@ -435,29 +435,29 @@ def test_baseline_no_pagehide_still_completes(
|
|||||||
def test_pre_token_guard_survives_hidden_window(
|
def test_pre_token_guard_survives_hidden_window(
|
||||||
page: Page, app_url: str, mock_llm: int, db_ready: None
|
page: Page, app_url: str, mock_llm: int, db_ready: None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""The 120 s pre-token guard is a plain ``setTimeout`` — without the
|
"""The 300 s pre-token guard is a plain ``setTimeout`` — without the
|
||||||
phase-73 visibility re-arm, hidden time counts toward it and a turn
|
phase-73 visibility re-arm, hidden time counts toward it and a turn
|
||||||
whose first frame lands past the 120 s mark errors with the "stuck"
|
whose first frame lands past the 300 s mark errors with the "stuck"
|
||||||
copy (task 01 scenario B, C2 confirmed). The fake clock makes the
|
copy (task 01 scenario B, C2 confirmed). The fake clock makes the
|
||||||
deadline deterministic (waiting 120 s of real time is not an
|
deadline deterministic (waiting 300 s of real time is not an
|
||||||
option for a regression suite): the mock's 3 s real-time pre-token
|
option for a regression suite): the mock's 3 s real-time pre-token
|
||||||
silence keeps the guard armed while the FAKE clock is fast-forwarded
|
silence keeps the guard armed while the FAKE clock is fast-forwarded
|
||||||
across the original deadline — with a return to visible in between,
|
across the original deadline — with a return to visible in between,
|
||||||
which re-arms the still-armed guard with a fresh TURN_TIMEOUT_MS.
|
which re-arms the still-armed guard with a fresh TURN_TIMEOUT_MS.
|
||||||
|
|
||||||
Timeline (fake clock, the guard armed at t=0, deadline t=120 s):
|
Timeline (fake clock, the guard armed at t=0, deadline t=300 s):
|
||||||
|
|
||||||
* t=0 — the slow question is sent; the guard arms (t=120 s);
|
* t=0 — the slow question is sent; the guard arms (t=300 s);
|
||||||
* t~0 — the tab goes hidden (synthetic; pre-token, so the
|
* t~0 — the tab goes hidden (synthetic; pre-token, so the
|
||||||
pagehide handler persists nothing — the question is
|
pagehide handler persists nothing — the question is
|
||||||
already saved);
|
already saved);
|
||||||
* t=115 — fast-forward while hidden: the original timer (120 s)
|
* t=295 — fast-forward while hidden: the original timer (300 s)
|
||||||
is not due yet;
|
is not due yet;
|
||||||
* t=115 — back to visible: the re-arm fires (the guard is still
|
* t=295 — back to visible: the re-arm fires (the guard is still
|
||||||
armed — no frame has arrived) → new deadline t=235 s;
|
armed — no frame has arrived) → new deadline t=595 s;
|
||||||
* t=229 — fast-forward again: PAST the original 120 s deadline —
|
* t=585 — fast-forward again: PAST the original 300 s deadline —
|
||||||
without the re-arm the guard fires HERE with the "stuck"
|
without the re-arm the guard fires HERE with the "stuck"
|
||||||
error — but short of the re-armed 235 s deadline.
|
error — but short of the re-armed 595 s deadline.
|
||||||
|
|
||||||
Meanwhile (real time, ~3.5 s after the send) the mock's first frame
|
Meanwhile (real time, ~3.5 s after the send) the mock's first frame
|
||||||
lands, clears the guard for good, and the turn settles with the
|
lands, clears the guard for good, and the turn settles with the
|
||||||
@@ -467,7 +467,7 @@ def test_pre_token_guard_survives_hidden_window(
|
|||||||
page.set_default_timeout(30_000)
|
page.set_default_timeout(30_000)
|
||||||
|
|
||||||
# The fake clock BEFORE the first navigation: every app timer (the
|
# The fake clock BEFORE the first navigation: every app timer (the
|
||||||
# 120 s guard among them) becomes test-controlled, while the mock's
|
# 300 s guard among them) becomes test-controlled, while the mock's
|
||||||
# real-time stream is unaffected (the network is not a timer).
|
# real-time stream is unaffected (the network is not a timer).
|
||||||
page.clock.install()
|
page.clock.install()
|
||||||
login(page, app_url, next="/") # phase 79: chat is require_user-gated
|
login(page, app_url, next="/") # phase 79: chat is require_user-gated
|
||||||
@@ -481,9 +481,9 @@ def test_pre_token_guard_survives_hidden_window(
|
|||||||
expect(page.locator(ANSWER)).to_have_count(0)
|
expect(page.locator(ANSWER)).to_have_count(0)
|
||||||
|
|
||||||
page.evaluate(HIDE_JS)
|
page.evaluate(HIDE_JS)
|
||||||
page.clock.fast_forward(115_000)
|
page.clock.fast_forward(295_000)
|
||||||
page.evaluate(SHOW_JS) # the re-arm: a fresh TURN_TIMEOUT_MS
|
page.evaluate(SHOW_JS) # the re-arm: a fresh TURN_TIMEOUT_MS
|
||||||
page.clock.fast_forward(114_000) # past 120 s, short of the re-armed 235 s
|
page.clock.fast_forward(290_000) # past 300 s, short of the re-armed 595 s
|
||||||
|
|
||||||
# The turn still settles with the full answer — no "taking a long
|
# The turn still settles with the full answer — no "taking a long
|
||||||
# time" error, no aborted stream.
|
# time" error, no aborted stream.
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ def _html() -> str:
|
|||||||
return (FRONTEND / "index.html").read_text(encoding="utf-8")
|
return (FRONTEND / "index.html").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
def test_turn_timeout_constant_exported_at_120s() -> None:
|
def test_turn_timeout_constant_exported_at_300s() -> None:
|
||||||
"""The 120s client-side guard (PLAN §7.4) must be an *exported*
|
"""The 300s client-side guard (PLAN §7.4) must be an *exported*
|
||||||
constant — testable, and the single value the E2E timeout story keys
|
constant — testable, and the single value the E2E timeout story keys
|
||||||
off."""
|
off."""
|
||||||
js = _js()
|
js = _js()
|
||||||
match = re.search(r"export\s+const\s+TURN_TIMEOUT_MS\s*=\s*120_?000\s*;", js)
|
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 = 120000`"
|
assert match, "app.js must export `const TURN_TIMEOUT_MS = 300000`"
|
||||||
|
|
||||||
|
|
||||||
def test_state_machine_has_all_four_states() -> None:
|
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:
|
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)."""
|
surface the same actionable retry hint (story AC4)."""
|
||||||
js = _js()
|
js = _js()
|
||||||
assert "check the LLM is reachable" in 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:
|
def test_thinking_event_is_a_first_class_turn_branch() -> None:
|
||||||
"""Phase 17: `thinking` SSE frames stream live into the collapsible
|
"""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
|
clears (the stream is alive), and the text renders through the
|
||||||
escape-first markdown renderer (XSS-safe). While open, the stream is
|
escape-first markdown renderer (XSS-safe). While open, the stream is
|
||||||
pinned to the bottom of the block."""
|
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]
|
branch = js[thinking_idx:delta_idx]
|
||||||
assert "thinkingAcc += ev.text" in branch
|
assert "thinkingAcc += ev.text" in branch
|
||||||
assert "sawThinking = true" 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 "removeTyping()" in branch, "the live block replaces the typing dots"
|
||||||
assert "ensureThinkingBlock(wrap)" in branch
|
assert "ensureThinkingBlock(wrap)" in branch
|
||||||
assert "renderMarkdown(thinkingAcc)" in branch, "escape-first renderer (XSS-safe)"
|
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:
|
def test_abort_plumbing_owns_the_fetch() -> None:
|
||||||
"""The in-flight fetch is owned by an AbortController created at
|
"""The in-flight fetch is owned by an AbortController created at
|
||||||
turn start (module scope, cleared in the finally), passed to the
|
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
|
its backstop — with `aborted = true` FIRST, so the catch never reads
|
||||||
the guard's abort as a user stop (one owner, same outcome)."""
|
the guard's abort as a user stop (one owner, same outcome)."""
|
||||||
js = _js()
|
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
|
"""Phase 67 (owner-locked A4, task 02's contract): a `retry` SSE
|
||||||
frame (the server restarted the LLM request before its first piece
|
frame (the server restarted the LLM request before its first piece
|
||||||
— locked A2) is a first-class branch in runTurn's handler, ordered
|
— 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
|
(a frame arrived), resolves n/N from the frame, and writes the
|
||||||
owner-locked copy literal onto the EXISTING channels only — the
|
owner-locked copy literal onto the EXISTING channels only — the
|
||||||
#send-status live region + the typing-indicator aria-label. No DOM
|
#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"
|
assert js.count('ev.type === "retry"') == 1, "exactly one retry branch"
|
||||||
branch = js[retry_idx:delta_idx]
|
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.attempt)" in branch, "n = the attempt about to be tried"
|
||||||
assert "Number(ev.max_attempts)" in branch, "N = the configured total"
|
assert "Number(ev.max_attempts)" in branch, "N = the configured total"
|
||||||
assert (
|
assert (
|
||||||
|
|||||||
@@ -152,8 +152,8 @@ def test_real_navigation_behavior_is_unchanged() -> None:
|
|||||||
# The phase-48 teardown contract (real close / navigation / Stop still
|
# The phase-48 teardown contract (real close / navigation / Stop still
|
||||||
# aborts the fetch) is untouched.
|
# aborts the fetch) is untouched.
|
||||||
assert "turnAbort?.abort()" in js, "the abort owner still aborts the fetch"
|
assert "turnAbort?.abort()" in js, "the abort owner still aborts the fetch"
|
||||||
assert re.search(r"export\s+const\s+TURN_TIMEOUT_MS\s*=\s*120_?000\s*;", js), (
|
assert re.search(r"export\s+const\s+TURN_TIMEOUT_MS\s*=\s*300_?000\s*;", js), (
|
||||||
"the guard constant is still 120s (owner-locked phase-17/48 value)"
|
"the guard constant is still 300s (raised from the phase-17/48 value)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user