fix(review): WS state machine, per-tab supersede key, backoff, dot CSS (PR #149)

- _wsState now transitions through the full lifecycle: _connectWs() sets
  'connecting', ws.onopen (inside the this._ws === ws guard) sets 'connected',
  _disconnectWs() resets to 'disconnected' — the connection chip's "WS" state
  was previously unreachable (stuck on "WS…"/"HTTP" forever).
- WS registry supersede is now keyed per TAB: the upgrade URL sends
  cid = clientId + ':' + per-page nonce (reusing the constructor's page UUID),
  while input frames keep the bare browser clientId for seq dedup — two
  tabs/windows on one session coexist instead of 4010-evicting each other in a
  perpetual 5s ping-pong; a genuine same-tab reconnect still supersedes.
- Exponential backoff engages: _disconnectWs() no longer zeroes
  _wsReconnectAttempts (it's called at the top of _connectWs, so every retry
  replanned at attempt 0 → ~0ms tight reconnect loop during outages); onopen
  resets the counter on success.
- styles.css: add .connection-dot.connected (green) and .connection-dot.fallback
  (yellow) — both states rendered an invisible dot (no rule existed).
- Remove smuggled dead code: resolveMonitorRowLabels/CodemanMonitorLabels
  (COD-122, no consumer, referenced test doesn't exist) and the never-written
  _wsLastClose/_wsInputSendCount/_httpFallbackSendCount diagnostics.
- Tests: new test/ws-state-lifecycle.test.ts drives the REAL
  _connectWs/onopen/onclose/timer cycle (state transitions, escalating backoff
  delays, composite cid on the upgrade URL); registry two-tab coexistence test;
  static check that every emitted connection-dot class has a styles.css rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-07-12 18:42:36 +02:00
parent 584910f645
commit 6e417d69dc
8 changed files with 373 additions and 52 deletions
+15
View File
@@ -318,6 +318,21 @@ describe('_computeConnectionDescriptor — pure render per state (COD-136)', ()
});
});
describe('connection-dot CSS — every emitted dot class has a styles.css rule', () => {
// The descriptor emits these dot variants; each needs a visible rule or the
// 8px dot renders as an invisible blob (the base .connection-dot rule has no
// background). 'connected' and 'fallback' were missing when this PR shipped.
const DOT_CLASSES = ['connected', 'fallback', 'offline', 'reconnecting', 'draining'];
const css = readFileSync(resolve(import.meta.dirname, '../src/web/public/styles.css'), 'utf8');
for (const cls of DOT_CLASSES) {
it(`.connection-dot.${cls} is styled`, () => {
const rule = new RegExp(`\\.connection-dot\\.${cls}\\s*\\{[^}]*background`, 'm');
expect(css).toMatch(rule);
});
}
});
/** A DOM element fake that COUNTS each property write — used to detect the skip. */
function countingElement() {
const writes = { display: 0, className: 0, textContent: 0, title: 0 };