fix(webui): stop "Reconnecting" badge flashing on every streamed chunk (#7126)

* fix(webui): stop rendering "Reconnecting" badge during streaming (#7071)

A proxy (e.g. Railway) that closes the SSE stream body between streamed
frames makes event-source-plus retry the connection on every chunk. The
transport status flips to RECONNECTING for each retry and back to
CONNECTED on the next chunk, so the "Reconnecting" badge blinked on
every streamed update — a misleading impression of instability while the
response was being delivered successfully.

RECONNECTING remains an internal connection state: it still feeds
run-failure attribution via isConnectionLostStatus and the transport
retry loop is untouched. Only the header badge stops rendering it.
Sustained loss still escalates to DISCONNECTED, which is rendered as
before, so genuine connection failures remain visible.

Add CONNECTION_STATUS.RECONNECTING to HIDDEN_STATUSES in
connection-status.tsx. The i18n keys, the dot/breathing styles, and the
sseStatus plumbing are unchanged; useSSE.ts is untouched.

Update connection-status.test.ts to cover RECONNECTING as a hidden
routine state and assert the expanded mobile label via DISCONNECTED
instead. Update the e2e smoke test so the offline and retryable
stream-interruption paths assert the badge stays absent, while the
terminal (non-retryable) path still asserts "Disconnected".

Closes #7071

* test(webui): add regression test for hidden Reconnecting badge (#7071)

Add a dedicated regression assertion proving the ConnectionStatus badge
no longer renders for the RECONNECTING state (the per-chunk transport
retry that blinked on every streamed frame), while DISCONNECTED still
renders. Satisfies the regression-test enforcement gate for the fix in
the parent commit.

* test(webui): wait for fake SSE stream before forced failure (#7071)

Hiding the RECONNECTING badge meant expect(connection_status).to_have_count(0)
no longer waits for the fake SSE reconnect to ready its stream, so
__failLatestV2Sse(0) raced the stream lifecycle and threw
'no event stream is open'. Add explicit readiness probes
(__v2SseHasOpenStream / __v2SseHasHeldConnection) and wait for them
before forcing a retryable or terminal stream failure.
This commit is contained in:
firat.sertgoz
2026-08-04 15:59:28 +03:00
committed by GitHub
parent 283e1f6b7c
commit 676d86ce02
3 changed files with 81 additions and 24 deletions

View File

@@ -73,6 +73,11 @@ test("ConnectionStatus keeps an empty live region mounted for routine states", (
CONNECTION_STATUS.IDLE,
CONNECTION_STATUS.CONNECTING,
CONNECTION_STATUS.CONNECTED,
// RECONNECTING is an internal state only: a proxy that closes the SSE
// body between streamed frames makes the transport retry on every chunk,
// so rendering it would only blink. It stays surfaced for run-failure
// attribution via `isConnectionLostStatus` but is not rendered.
CONNECTION_STATUS.RECONNECTING,
]) {
const rendered = ConnectionStatus({ status });
assert.notEqual(rendered, null, status);
@@ -99,7 +104,6 @@ test("ConnectionStatus renders static desktop status and mobile disclosure", ()
const ConnectionStatus = loadConnectionStatusForTest();
for (const [status, style] of [
[CONNECTION_STATUS.RECONNECTING, "--v2-warning-soft"],
[CONNECTION_STATUS.DISCONNECTED, "--v2-danger-soft"],
[CONNECTION_STATUS.PAUSED, "--v2-surface-soft"],
]) {
@@ -143,7 +147,7 @@ test("ConnectionStatus renders static desktop status and mobile disclosure", ()
test("ConnectionStatus exposes the expanded mobile label state", () => {
const ConnectionStatus = loadConnectionStatusForTest({ expanded: true });
const rendered = ConnectionStatus({ status: CONNECTION_STATUS.RECONNECTING });
const rendered = ConnectionStatus({ status: CONNECTION_STATUS.DISCONNECTED });
const toggle = nodeByTestId(rendered, "connection-status-toggle");
const floatingLabel = nodeByTestId(rendered, "connection-status-label");
@@ -161,3 +165,43 @@ test("ConnectionStatus falls back safely for an unknown interruption", () => {
assert.ok(floatingLabel.props.className.includes("--v2-surface-soft"));
assert.equal(floatingLabel.children[0], "blocked");
});
test("ConnectionStatus does not render a Reconnecting badge during streaming (#7071)", () => {
// Regression for #7071: a proxy that closes the SSE body between streamed
// frames makes the transport retry on every chunk, flipping the status to
// RECONNECTING for each retry. The badge must not surface that routine
// in-flight reconnect — only a sustained loss that escalates to
// DISCONNECTED is user-visible.
const ConnectionStatus = loadConnectionStatusForTest();
const reconnecting = ConnectionStatus({ status: CONNECTION_STATUS.RECONNECTING });
assert(
nodeByTestId(reconnecting, "connection-status") === null,
"RECONNECTING must not render the desktop status badge",
);
assert(
nodeByTestId(reconnecting, "connection-status-toggle") === null,
"RECONNECTING must not render the mobile status toggle",
);
assert(
nodeByTestId(reconnecting, "connection-status-label") === null,
"RECONNECTING must not render the mobile status label",
);
const liveStatus = findNode(
reconnecting,
(node) => node.props?.role === "status",
);
assert(liveStatus !== null, "the live region stays mounted for RECONNECTING");
assert.equal(liveStatus.children[0], "", "RECONNECTING keeps the live region empty");
const disconnected = ConnectionStatus({ status: CONNECTION_STATUS.DISCONNECTED });
assert(
nodeByTestId(disconnected, "connection-status") !== null,
"DISCONNECTED still renders the desktop status badge",
);
assert(
nodeByTestId(disconnected, "connection-status-label").children[0] === "disconnected",
"DISCONNECTED still labels the badge",
);
});

View File

@@ -26,10 +26,17 @@ const STATUS_DOT_STYLES: Partial<Record<ConnectionStatus, string>> = {
const DEFAULT_DOT_STYLE = "text-[var(--v2-text-muted)]";
// `RECONNECTING` is kept as an internal connection state (it still feeds
// run-failure attribution via `isConnectionLostStatus`) but is intentionally
// not rendered: a proxy that closes the SSE body between streamed frames
// makes the transport retry on every chunk, and surfacing "Reconnecting" for
// those routine in-flight reconnects only produces a misleading blinking
// badge. Sustained loss still escalates to `DISCONNECTED`, which is shown.
const HIDDEN_STATUSES: ReadonlySet<ConnectionStatus> = new Set([
CONNECTION_STATUS.IDLE,
CONNECTION_STATUS.CONNECTING,
CONNECTION_STATUS.CONNECTED,
CONNECTION_STATUS.RECONNECTING,
]);
export function ConnectionStatus({ status }: ConnectionStatusProps) {

View File

@@ -220,6 +220,14 @@ async def _install_fake_v2_event_stream(page) -> None:
return activeStream;
};
// Readiness probes so tests do not race forced failures against the
// fake stream lifecycle. A hidden RECONNECTING badge can no longer
// double as a wait for reconnect readiness.
window.__v2SseHasOpenStream = () =>
Boolean(activeStream && !activeStream.closed && activeStream.controller);
window.__v2SseHasHeldConnection = () =>
Boolean(activeStream && !activeStream.closed && activeStream.resolve);
const closeStream = (stream, error = null) => {
if (!stream || stream.closed) return;
stream.closed = true;
@@ -1663,44 +1671,42 @@ async def test_reborn_v2_disconnected_run_shows_status_and_stops_typing(
connection_status = page.locator(SEL_V2["connection_status"])
await context.set_offline(True)
await expect(connection_status).to_have_text("Reconnecting...", timeout=5000)
await expect(connection_status).to_have_css("position", "static")
assert await connection_status.evaluate("node => Boolean(node.closest('header'))")
await expect(connection_status).to_be_in_viewport()
# RECONNECTING is no longer rendered (internal state only): a proxy
# that closes the SSE body between streamed frames would otherwise
# blink the badge on every chunk. The badge stays absent during a
# transient/retryable reconnect and only reappears on a terminal
# DISCONNECTED state.
await expect(connection_status).to_have_count(0, timeout=5000)
await page.set_viewport_size({"width": 390, "height": 844})
connection_status_toggle = page.locator(SEL_V2["connection_status_toggle"])
connection_status_label = page.locator(SEL_V2["connection_status_label"])
disclosure_id = await connection_status_label.get_attribute("id")
assert disclosure_id
await expect(connection_status_label).to_be_hidden()
await expect(connection_status_label).to_have_attribute("aria-hidden", "true")
await expect(connection_status_toggle).to_have_attribute("aria-expanded", "false")
await expect(connection_status_toggle).to_have_attribute("aria-controls", disclosure_id)
await expect(connection_status_toggle).to_be_in_viewport()
await connection_status_toggle.click()
await expect(connection_status_toggle).to_have_attribute("aria-expanded", "true")
await expect(connection_status_label).to_have_attribute("aria-hidden", "false")
await expect(connection_status_label).to_be_visible()
await expect(connection_status_label).to_have_text("Reconnecting...")
await expect(connection_status_label).to_have_css("position", "absolute")
await expect(connection_status_toggle).to_be_in_viewport()
await expect(connection_status_label).to_be_in_viewport()
# No visible status affordance while RECONNECTING is hidden.
await expect(connection_status_toggle).to_have_count(0, timeout=5000)
await expect(connection_status_label).to_have_count(0, timeout=5000)
await expect(page.locator(SEL_V2["header_logs_link"])).to_be_visible()
await expect(page.locator(SEL_V2["header_docs_link"])).to_be_visible()
await page.set_viewport_size({"width": 1280, "height": 720})
await context.set_offline(False)
await page.wait_for_function("() => window.__v2SseHasOpenStream?.() === true")
await expect(connection_status).to_have_count(0, timeout=5000)
await composer.fill("summarize 3 X/Twitter posts")
await composer.press("Enter")
await expect(page.locator(SEL_V2["typing_indicator"])).to_be_visible(timeout=5000)
# A retryable stream interruption (readyState 0) stays RECONNECTING
# internally and is not rendered; the badge remains absent. Wait for
# an open stream first so the forced failure does not race the fake
# stream lifecycle, then wait for the held pending connection so the
# terminal failure below targets the held promise.
await page.wait_for_function("() => window.__v2SseHasOpenStream?.() === true")
await page.evaluate("() => window.__failLatestV2Sse(0)")
await expect(connection_status).to_have_text("Reconnecting...", timeout=5000)
await page.wait_for_function("() => window.__v2SseHasHeldConnection?.() === true")
await expect(connection_status).to_have_count(0, timeout=5000)
# A terminal (non-retryable) failure escalates to DISCONNECTED, which
# is still rendered.
await page.evaluate("() => window.__failLatestV2Sse(2)")
await expect(connection_status).to_have_text("Disconnected", timeout=5000)