mirror of
https://github.com/block/buzz.git
synced 2026-10-30 22:11:36 +00:00
> Opened by Brain (agent) on behalf of @wesbillman. ## Problem Users report the desktop app doesn't reliably reconnect and can wedge in states where only CMD+R (or a full restart) restores connectivity (thread `c2205e2b` in #desktop-reconnecting). Pinky's empirical light-switch matrix (real `buzz-relay`, SIGTERM/1012 + SIGKILL × 1s/45s/3min, at `f18a9cb10`) passed 4/4 — the backoff state machine recovers cleanly from ordinary relay loss. That isolates the user-stuck states to four special cases a reload resets but the auto flow never did. ## Fixes | Gap | Change | |---|---| | **G1** — recovery rode solely on the backoff timer (max 30s), throttled by WKWebView in occluded/background windows; nothing fired on network return or wake | New `useRelayResumeTriggers`: `online`, window focus, and visibility→visible call `preconnect()` when the session is `reconnecting`/`stalled`, rate-limited to one attempt per 5s (`relayResumeTriggerPolicy.ts`). Deliberately inert for the terminal `disconnected` state. | | **G2** — any AUTH `OK false` latched the session terminal forever, though the relay also rejects for transient causes (duplicate-AUTH "already authenticated" race, ±60s clock skew, fail-closed allowlist DB errors) | New `AuthOkTracker` (`relayAuthPolicy.ts`): "already authenticated" resolves as success; transient rejections retry with normal backoff; latch only on `restricted:` or after 3 consecutive rejections. | | **G3** — an `auth-required:` CLOSED (REQ racing AUTH after reconnect) permanently deleted the live subscription with no UI signal — frozen channel while state reads "connected" | Reclassified `auth-required:` as retryable in `relayClosedPolicy.ts`. Genuinely terminal classes (`restricted:`, `invalid:`, …) still delete. Can't loop: a truly unauthenticated session latches terminal at the connection level. | | **G4** — `useRelayAutoHeal` observed the 2s-debounced connection hook, so sub-2s flaps never triggered the heal even though `resetConnection` had already rejected every in-flight query | Auto-heal now observes the raw connection-state emitter. The existing 15s heal rate-limit still guards against flap storms. | Each fix is a colocated pure-policy module + unit tests, matching the existing `relayReconnectPolicy`/`relayClosedPolicy` pattern. ## Validation - Full desktop unit suite: **4151 pass, 0 fail** (at branch tip, `pnpm -C desktop test`) - `pnpm -C desktop typecheck` and `pnpm -C desktop check` clean (file-size ratchet respected — `relayClientSession.ts` net −2 lines despite the tracker wiring) - Evidence trail: `RESEARCH/DESKTOP_RECONNECT_CMDR_GAP_AUDIT.md` (audit), `RESEARCH/DESKTOP_RECONNECT_LIGHT_SWITCH_RESULTS.md` (Pinky's matrix) ## Not covered / follow-ups - Native macOS sleep-wake was not automated (would kill the harness session); G1's focus trigger is the mechanism that covers wake in practice, but a manual sleep-wake verification on a real build is worthwhile. - G3 terminal-CLOSED classes (`restricted:` etc.) still silently delete subs with no UI signal — surfacing that is a separate UX decision. - Stall-watchdog latency (60s idle + 10s check) left unchanged; G1 triggers largely mask it. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Signed-off-by: npub1gjuws2a2dc8z2nszprtg7v6u9q7ffeah3hgl5yx45jwn7y7aqs6s5e9xj6 <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@buzz.block.builderlab.xyz> Co-authored-by: npub1yxv5wk0u0fh6dwt925wntn7h397jvteyj4r87ttcd9xae7n2t3lqqj9jmm <21994759fc7a6fa6b965551d35cfd7897d262f2495467f2d78694ddcfa6a5c7e@buzz.block.builderlab.xyz> Co-authored-by: npub1gjuws2a2dc8z2nszprtg7v6u9q7ffeah3hgl5yx45jwn7y7aqs6s5e9xj6 <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@buzz.block.builderlab.xyz>
87 lines
3.2 KiB
JavaScript
87 lines
3.2 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { classifyRelayClosed } from "./relayClosedPolicy.ts";
|
|
|
|
// ── classifyRelayClosed ───────────────────────────────────────────────────────
|
|
|
|
test("classifyRelayClosed: rate-limited messages return rate-limited", () => {
|
|
for (const message of [
|
|
"rate-limited: quota exceeded; retry in 4s",
|
|
"rate-limited: slow down",
|
|
"rate-limited:",
|
|
]) {
|
|
assert.equal(classifyRelayClosed(message), "rate-limited", message);
|
|
}
|
|
});
|
|
|
|
test("classifyRelayClosed: terminal messages return terminal", () => {
|
|
for (const message of [
|
|
"restricted: not a channel member",
|
|
"restricted: channel access revoked",
|
|
"blocked: banned",
|
|
"invalid: malformed filter",
|
|
"pow: difficulty too low",
|
|
"duplicate: subscription exists",
|
|
"unsupported: filter",
|
|
"error: mixed search and non-search filters not supported",
|
|
"error: too many subscriptions",
|
|
]) {
|
|
assert.equal(classifyRelayClosed(message), "terminal", message);
|
|
}
|
|
});
|
|
|
|
test("classifyRelayClosed: transient errors return retryable", () => {
|
|
for (const message of ["error: database error", "server shutting down", ""]) {
|
|
assert.equal(classifyRelayClosed(message), "retryable", message);
|
|
}
|
|
});
|
|
|
|
test("classifyRelayClosed: auth-required is retryable (REQ/AUTH reconnect race)", () => {
|
|
// A REQ that lands before the AUTH handshake completes gets CLOSED with
|
|
// auth-required. Deleting the subscription would silently freeze the
|
|
// channel while the connection state still reads "connected" — the sub
|
|
// must survive and be retried after backoff.
|
|
assert.equal(
|
|
classifyRelayClosed("auth-required: not authenticated"),
|
|
"retryable",
|
|
);
|
|
assert.equal(
|
|
classifyRelayClosed("auth-required: authenticate before subscribing"),
|
|
"retryable",
|
|
);
|
|
});
|
|
|
|
// ── Subscription-survival semantics ──────────────────────────────────────────
|
|
// These replace the removed isRetryableRelayClosed wrapper tests.
|
|
// rate-limited must not delete the subscription; terminal must.
|
|
|
|
test("classifyRelayClosed: rate-limited class survives (subscription must not be deleted)", () => {
|
|
// Subscription deletion is gated on === "terminal"; rate-limited must survive.
|
|
assert.notEqual(
|
|
classifyRelayClosed("rate-limited: quota exceeded; retry in 4s"),
|
|
"terminal",
|
|
);
|
|
});
|
|
|
|
test("classifyRelayClosed: retryable class survives (subscription must not be deleted)", () => {
|
|
for (const message of ["error: database error", "server shutting down", ""]) {
|
|
assert.notEqual(classifyRelayClosed(message), "terminal", message);
|
|
}
|
|
});
|
|
|
|
test("classifyRelayClosed: terminal class triggers deletion (no retry)", () => {
|
|
for (const message of [
|
|
"restricted: not a channel member",
|
|
"blocked: banned",
|
|
"invalid: malformed filter",
|
|
"pow: difficulty too low",
|
|
"duplicate: subscription exists",
|
|
"unsupported: filter",
|
|
"error: mixed search and non-search filters not supported",
|
|
"error: too many subscriptions",
|
|
]) {
|
|
assert.equal(classifyRelayClosed(message), "terminal", message);
|
|
}
|
|
});
|