From 35547feb6a97e7a5e84f69467db8f2a6407af58d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 May 2026 14:22:04 +0200 Subject: [PATCH] Add server-initiated-multi-server-arbitration scenario (#62) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the multi-server arbitration scenario from Sendspin/conformance#62. The spec's rules — "must complete handshake first, prefer last-played server, send `another_server` on switch" — are unenforced today. Per the audit, only sendspin-cpp and sendspin-cli (via aiosendspin) implement them; six client SDKs collapse to "whichever connection happened first" and default to the wrong goodbye reason on switch. `multi-server-arbitration` is declared on the aiosendspin server only. Every client case fails fast until each adapter declares the capability based on the SDK's exposed surface. --- README.md | 1 + src/conformance/implementations.py | 1 + src/conformance/scenarios.py | 22 ++++++++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/README.md b/README.md index 5f3c3c0..ad507b6 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Current scenarios: - `server-initiated-request-format` (Client requests a mid-stream codec switch): start the server first, then the client. The server begins streaming in Opus; the client is instructed to emit `stream/request-format` with FLAC at a fixed timestamp and the server responds with a fresh `stream/start`. No SDK currently emits `stream/request-format`, so every case currently fails fast on the `stream-request-format-emit` capability - `server-initiated-pcm-24bit` (Server initiates connection and client wants 24-bit PCM): start the server first, then the client. The server re-packs the fixture as 24-bit PCM (3-byte packed, little-endian, two's complement). No SDK in the matrix declares the `pcm-24bit-decode` capability yet, so every case currently fails fast and the matrix surfaces the gap (notably the `sendspin-go` SDK has no 24-bit code path per the audit) - `server-initiated-external-source` (Client enters and leaves `external_source` mid-stream): start the server first, then the client. The client transitions to `external_source` and back; the harness verifies the server emits the right `group/update` and `stream/end` and that the previous group is restored. Per the audit only SendspinKit exposes a client-side API; no client in the matrix declares the `external-source-client-api` capability yet, so every case fails fast +- `server-initiated-multi-server-arbitration` (Client arbitrates between two connected servers): two servers connect to one client with different `connection_reason` values; the harness verifies the client sends `client/goodbye` with `'another_server'` on the right socket and ends up speaking to the right server per the spec's decision table. Per the audit only sendspin-cpp and sendspin-cli implement these rules; no client in the matrix declares the `multi-server-arbitration` capability yet, so every case fails fast ## Current coverage diff --git a/src/conformance/implementations.py b/src/conformance/implementations.py index cfe1ea8..85cd7ae 100644 --- a/src/conformance/implementations.py +++ b/src/conformance/implementations.py @@ -67,6 +67,7 @@ "stream-request-format-emit", "pcm-24bit-decode", "external-source-client-api", + "multi-server-arbitration", ), ), ), diff --git a/src/conformance/scenarios.py b/src/conformance/scenarios.py index 07b9cbe..d84d81b 100644 --- a/src/conformance/scenarios.py +++ b/src/conformance/scenarios.py @@ -196,6 +196,27 @@ ) +SERVER_INITIATED_MULTI_SERVER_ARBITRATION = ScenarioSpec( + id="server-initiated-multi-server-arbitration", + display_name="Client arbitrates between two connected servers", + description=( + "Two servers connect to one client (one with " + "`connection_reason: 'discovery'`, one with `'playback'`). The harness " + "asserts the client sends `client/goodbye` with the right reason " + "(`'another_server'`) on the right socket and ends up speaking to the " + "right server per the spec's decision table. Per the audit, only " + "sendspin-cpp and sendspin-cli (via aiosendspin) implement these rules; " + "every other client SDK collapses to \"whichever connection happened " + "first\" and defaults to the wrong goodbye reason on switch." + ), + initiator_role="server", + preferred_codec="pcm", + required_role_families=("player",), + verification_mode="capability-only", + required_capability="multi-server-arbitration", +) + + SERVER_INITIATED_EXTERNAL_SOURCE = ScenarioSpec( id="server-initiated-external-source", display_name="Client enters and leaves `external_source` mid-stream", @@ -247,6 +268,7 @@ SERVER_INITIATED_REQUEST_FORMAT, SERVER_INITIATED_PCM_24BIT, SERVER_INITIATED_EXTERNAL_SOURCE, + SERVER_INITIATED_MULTI_SERVER_ARBITRATION, ) SCENARIOS: dict[str, ScenarioSpec] = {scenario.id: scenario for scenario in SCENARIO_LIST}