fix(desktop): report authenticated relay recovery (#3812)

## Summary

- report the relay as connected immediately after socket open and
successful AUTH
- keep rate-limited subscription replay, the connect promise, and
reconnect listeners unchanged
- cover authenticated reconnect while replay is held behind the shared
rate-limit gate

## Why

After WARP recovery, the socket could reopen and authenticate
successfully while subscription replay waited behind the existing
rate-limit gate. `connect()` kept `ConnectionState` at `reconnecting`
during that intentional delay, so the desktop displayed “Can’t reach the
relay” despite authenticated traffic already flowing.

This is separate from #3774: that fix keeps routine operations from
bypassing scheduled reconnect backoff. This patch preserves those
protections and only corrects the authenticated transport-state
boundary.

## Failure semantics

If replay fails after the early `connected` transition, the existing
`replayLiveSubscriptions()` catch calls `resetConnection()`, closes the
socket, returns state to `reconnecting`, and schedules recovery.
Operation waiters and reconnect notifications still do not complete
until replay succeeds.

## Validation

At commit `c8a4308e1079f4f9e6a72f0f0bfba280fe822ec0` with a clean
working tree:

- `pnpm --dir desktop typecheck`
- `pnpm --dir desktop test` — 3,847 passed
- `pnpm --dir desktop check` — passed; two pre-existing informational
template-literal notices
- `pnpm --dir desktop exec playwright test
tests/e2e/relay-reconnect.spec.ts` — 8 passed
- regression test proven red before the production ordering change
(`reconnecting` after 3 seconds) and green after it

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
This commit is contained in:
Wes
2026-07-30 16:04:14 -06:00
committed by GitHub
parent 29dfe4821e
commit 74cd571219
3 changed files with 41 additions and 1 deletions

View File

@@ -566,8 +566,8 @@ export class RelayClient {
this.reconnectDelayMs = RECONNECT_BASE_DELAY_MS;
}, BACKOFF_RESET_STABLE_MS);
await this.replayLiveSubscriptions();
this.connectionStateEmitter.set("connected");
await this.replayLiveSubscriptions();
this.stallWatchdog.start();
this.emitReconnectIfNeeded();
} catch (error) {

View File

@@ -11,6 +11,7 @@ import {
} from "./e2eBridgeCustomHarnesses.ts";
import { relayClient } from "@/shared/api/relayClient";
import { activateRateLimit } from "@/shared/api/relayRateLimitGate";
import type { ConnectionState } from "@/shared/api/relayClientShared";
import type { ChannelTemplate, RelayEvent } from "@/shared/api/types";
import { getMarkdownParseCount } from "@/shared/ui/markdown/nodeCache";
@@ -1115,6 +1116,7 @@ declare global {
unavailable: boolean,
) => void;
__BUZZ_E2E_GET_WEBSOCKET_CONNECT_ATTEMPTS__?: () => number[];
__BUZZ_E2E_ACTIVATE_RELAY_RATE_LIMIT__?: (seconds: number) => void;
__BUZZ_E2E_RESET_WEBSOCKET_CONNECT_ATTEMPTS__?: () => void;
__BUZZ_E2E_SET_MESH__?: (mesh: {
admitted?: boolean;
@@ -9706,6 +9708,9 @@ export function maybeInstallE2eTauriMocks() {
window.__BUZZ_E2E_GET_WEBSOCKET_CONNECT_ATTEMPTS__ = () => [
...relayWebsocketConnectAttemptStarts,
];
window.__BUZZ_E2E_ACTIVATE_RELAY_RATE_LIMIT__ = (seconds) => {
activateRateLimit(seconds);
};
window.__BUZZ_E2E_RESET_WEBSOCKET_CONNECT_ATTEMPTS__ = () => {
relayWebsocketConnectAttemptStarts.length = 0;
};

View File

@@ -62,6 +62,19 @@ async function setMockWebsocketUnavailable(
}, unavailable);
}
async function activateRelayRateLimit(
page: import("@playwright/test").Page,
seconds: number,
) {
await page.evaluate((duration) => {
const activate = window.__BUZZ_E2E_ACTIVATE_RELAY_RATE_LIMIT__;
if (!activate) {
throw new Error("E2E relay rate-limit seam is not installed.");
}
activate(duration);
}, seconds);
}
async function getMockWebsocketConnectAttempts(
page: import("@playwright/test").Page,
) {
@@ -195,6 +208,28 @@ test("routine traffic cannot bypass outage backoff and recovery stays automatic"
);
});
test("authenticated reconnect reports connected while replay is rate-limited", async ({
page,
}) => {
await page.goto("/");
await expect(page.getByTestId("channel-general")).toBeVisible();
await activateRelayRateLimit(page, 5);
await disconnectMockWebsockets(page);
// Replay remains intentionally blocked behind admission control, but socket
// open + successful AUTH is already a healthy connection. The UI must not
// claim the relay is unreachable for the rest of the gate window.
await expect
.poll(
() =>
page.evaluate(() => window.__BUZZ_E2E_GET_RELAY_CONNECTION_STATE__?.()),
{ timeout: 3_000 },
)
.toBe("connected");
await expect(page.getByTestId("sidebar-relay-unreachable")).toHaveCount(0);
});
test("service restart close resets accumulated backoff", async ({ page }) => {
await installMockBridge(page, {
websocketConnectErrors: ["down 1", "down 2", "down 3"],