From eab243473d1ea328d0f98a22e48161efcb228e35 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 27 May 2026 14:21:04 +0200 Subject: [PATCH] Add server-initiated-external-source scenario (#61) Adds the external-source group-swap scenario from Sendspin/conformance#61. Per the audit, only SendspinKit exposes a client-side `enterExternalSource()` / `exitExternalSource()` API today; every other client SDK has no embedder-facing handle. The aiosendspin server already handles the full external-source group flow (`server/roles/controller/v1.py:93-119`). `external-source-client-api` 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 | 20 ++++++++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/README.md b/README.md index 53f7237..5f3c3c0 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Current scenarios: - `server-initiated-burst-cadence` (Server observes client/time burst cadence): start the server first, then the client. The server counts `client/time` messages over a fixed observation window and asserts the distribution matches the recommended cadence (bursts of 8, ~10 s apart). No SDK in the matrix declares the `client-time-burst-cadence` capability yet, so every case currently fails fast and the matrix surfaces the gap - `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 ## Current coverage diff --git a/src/conformance/implementations.py b/src/conformance/implementations.py index 82bc16f..cfe1ea8 100644 --- a/src/conformance/implementations.py +++ b/src/conformance/implementations.py @@ -66,6 +66,7 @@ "client-time-burst-cadence", "stream-request-format-emit", "pcm-24bit-decode", + "external-source-client-api", ), ), ), diff --git a/src/conformance/scenarios.py b/src/conformance/scenarios.py index 728d7ca..07b9cbe 100644 --- a/src/conformance/scenarios.py +++ b/src/conformance/scenarios.py @@ -196,6 +196,25 @@ ) +SERVER_INITIATED_EXTERNAL_SOURCE = ScenarioSpec( + id="server-initiated-external-source", + display_name="Client enters and leaves `external_source` mid-stream", + description=( + "Start the server first, then the client. The client enters " + "`external_source` mid-stream; the harness asserts the server emits the right " + "`group/update` and `stream/end`; then the client leaves `external_source` and " + "the previous group is restored. Per the audit, only SendspinKit exposes a " + "client-side API today, so every other client fails fast on the " + "`external-source-client-api` capability." + ), + initiator_role="server", + preferred_codec="pcm", + required_role_families=("player",), + verification_mode="capability-only", + required_capability="external-source-client-api", +) + + SERVER_INITIATED_REQUEST_FORMAT = ScenarioSpec( id="server-initiated-request-format", display_name="Client requests a mid-stream codec switch", @@ -227,6 +246,7 @@ SERVER_INITIATED_BURST_CADENCE, SERVER_INITIATED_REQUEST_FORMAT, SERVER_INITIATED_PCM_24BIT, + SERVER_INITIATED_EXTERNAL_SOURCE, ) SCENARIOS: dict[str, ScenarioSpec] = {scenario.id: scenario for scenario in SCENARIO_LIST}