Skip to content

test: pin agent WebSocket supersede and close-code handling#441

Open
eric-descourtis-thenvoi wants to merge 1 commit into
band-ai:mainfrom
eric-descourtis-thenvoi:test/agent-ws-supersede-close-codes
Open

test: pin agent WebSocket supersede and close-code handling#441
eric-descourtis-thenvoi wants to merge 1 commit into
band-ai:mainfrom
eric-descourtis-thenvoi:test/agent-ws-supersede-close-codes

Conversation

@eric-descourtis-thenvoi

Copy link
Copy Markdown

Cross-repo follow-up from thenvoi/thenvoi-platform#2328 (finding M8).

Context

Since Phoenix 1.8, the platform closes evicted (superseded) agent sockets with WebSocket close code 1001, which phoenix.js-style clients read as reconnect-expected — the opposite of the platform's terminal supersession contract: the agent_control supersede push carries retryable: false and SDKs must not auto-reconnect (spec/websocket-api.draft.md). The platform side already pins the literal 1001 (agent_presence_phoenix_disconnect_test.exs) and documents the contract in the evictor moduledoc; this PR is the SDK-side verification + pins.

Verification result: the SDK already complies

  • Push receivedBandLink._on_supersedeWebSocketClient.record_terminal_disconnectPHXChannelsClient.auto_reconnect = False. The supervisor checks auto_reconnect before any close-code classification, so the push is authoritative regardless of the close code observed afterwards.
  • Push missed (closed inside the drain window — queued-but-undispatched pushes are discarded by the client's _drain_topic_queue) → close code 1001 is classified together with 1000 as do-not-reconnect under the default ReconnectPolicy (reconnect_on_normal_close=False), which WebSocketClient uses unmodified. The Python client does not have the phoenix.js 1001-reconnect behavior.
  • Push missed + codeless/abnormal close (e.g. an intermediary drops TCP) → the SDK reconnects; this is the accepted worst case, bounded platform-side by re-supersession and the reconnect rate limit.

Tests added (tests/websocket/test_close_code_handling.py)

All four drive the real PHXChannelsClient supervisor against an in-process fake Phoenix V2 server (no mocks of the reconnect loop):

  1. supersede push is terminal even when the socket then closes with a retryable code (1011)
  2. missed push + eviction close 1001 → no reconnect (literal pin, mirroring the platform test)
  3. abnormal codeless close without the push still reconnects (documents the bounded worst case)
  4. default ReconnectPolicy classification pin: 1000/1001 terminal, None/1006 retryable

Mutation-verified: ignoring the supersede push, or replacing the 1001 close with 1011, makes the corresponding test fail with the expected assertion.

Test evidence

  • tests/websocket/ — 70 passed
  • full unit suite (--ignore=tests/integration/ --ignore=tests/e2e/ semantics): 3349 passed, 168 skipped; the only failures locally are tests/integration/* (need live platform keys) and tests/integrations/acp/test_e2e_codex_acp.py (needs a working codex login) — both pre-existing on clean main in the same environment and skipped in CI
  • ruff check, ruff format, pyrefly check clean

Related

  • JS SDK (band-ai/fern-javascript-sdk): Fern-generated REST only, no WebSocket client — nothing to verify there.
  • tjam's Rust Band transport does not comply (never joins agent_control, reconnects on any close code) — tracked separately.

🤖 Generated with Claude Code

Cross-repo follow-up from thenvoi/thenvoi-platform#2328 (finding M8):
since Phoenix 1.8 the platform closes evicted agent sockets with
WebSocket close code 1001, which phoenix.js-style clients treat as
reconnect-expected — the opposite of the terminal supersession
contract (supersede push carries retryable: false).

Verified the SDK already complies, and pins that behavior with tests
driving the real PHXChannelsClient supervisor against an in-process
fake Phoenix server:

- supersede push is terminal regardless of the observed close code
  (server closes 1011 after the push; no reconnect)
- missed supersede push + eviction close 1001 does not reconnect
  (literal 1001 pin, mirroring the platform-side close-code pin)
- abnormal codeless close without the push still reconnects — the
  accepted worst case, bounded platform-side by re-supersession and
  reconnect rate limiting
- the default ReconnectPolicy classification the SDK relies on
  (1000/1001 terminal, None/1006 retryable)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant