Skip to main content

sipx_testkit/
realtime_peer.rs

1//! A deterministic stand-in for the realtime agent endpoint.
2//!
3//! [`docs/specs/openai-realtime.md`](../../../docs/specs/openai-realtime.md) is a contract with
4//! two sides. The bridge holds one of them; this module holds the other, so every vector in that
5//! spec except the live proof runs in the default `cargo test` matrix with no account, no
6//! credential, no network beyond loopback and no container. A bridge whose only counterparty is
7//! the vendor can be tested once a day by whoever holds the key; a bridge whose counterparty is
8//! this module is tested by everyone, on every commit, including the failure rows — and the
9//! failure rows are the half nobody can produce on demand from a real endpoint.
10//!
11//! **Cleartext on loopback, deliberately.** The peer speaks `ws://127.0.0.1:<port>` rather than
12//! `wss://`. Certificates are the one fixture cost that spreads: a TLS stand-in needs a trust
13//! anchor threaded into every test that reaches it, and the first test that finds that awkward
14//! disables verification, which is a worse habit than the one the certificate was for. The
15//! client this peer exists for permits cleartext to loopback and refuses it everywhere else, so
16//! the seam is closed by the client's own rule rather than by a certificate this peer would have
17//! to issue. [`certs`](crate::certs) is still where a test that genuinely needs TLS gets one.
18//!
19//! **Scripted, never guessed.** The peer performs the protocol spine by itself — it checks the
20//! bearer, answers with `session.created`, answers a `session.update` with `session.updated`,
21//! and records every client event and every appended audio byte — and does nothing else unless
22//! a test directs it. Downlink audio, cancels, closes and every malformed frame are directives
23//! whose future resolves *after the frame is on the socket*, so a test orders its script by
24//! awaiting the peer rather than by waiting on a clock. No behaviour here is timed.
25//!
26//! **The negatives are the point.** §6's failure taxonomy is reachable one row at a time:
27//! a refused bearer ([`PeerConfig::expecting_bearer`]), withheld setup acknowledgements
28//! ([`Withhold`]), a peer that answers the upgrade and then goes silent ([`StallPoint`]), a
29//! frame over the 1 MiB bound ([`RealtimePeer::send_oversize`]), events that cannot be read
30//! ([`Malformed`]), a normal close and an abrupt reset. Each is asserted from the client's side
31//! of the socket in this crate's own tests, because a stand-in whose misbehaviour is a flag
32//! nobody observes proves nothing about the bridge tested against it.
33
34use std::fmt;
35use std::net::SocketAddr;
36use std::sync::{Arc, Mutex, PoisonError};
37use std::time::Duration;
38
39use base64::Engine as _;
40use base64::engine::general_purpose::STANDARD as BASE64;
41use futures_util::{SinkExt, StreamExt};
42use serde_json::{Value, json};
43use tokio::net::{TcpListener, TcpStream};
44use tokio::sync::{Notify, mpsc, oneshot};
45use tokio::task::{JoinHandle, JoinSet};
46use tokio_tungstenite::accept_hdr_async;
47use tokio_tungstenite::tungstenite::handshake::server::{ErrorResponse, Request, Response};
48use tokio_tungstenite::tungstenite::http::StatusCode;
49use tokio_tungstenite::tungstenite::protocol::{CloseFrame, frame::coding::CloseCode};
50use tokio_tungstenite::tungstenite::{Bytes as WsBytes, Message, Utf8Bytes};
51use tokio_util::sync::CancellationToken;
52
53/// The call's 20 ms packet: 160 bytes of G.711 at 8000 Hz, one byte per sample (RFC 3551
54/// §4.5.14), which spec §4.1 makes the unit of audio in both directions.
55pub const FRAME_BYTES: usize = 160;
56
57/// Spec §4.2's **F-silence**: 20 ms of μ-law digital silence.
58pub const F_SILENCE: [u8; FRAME_BYTES] = [0xFF; FRAME_BYTES];
59
60/// F-silence's base64, quoted from spec §4.2.
61///
62/// A literal, never a value an assertion computes — the discipline
63/// [`webhook-binding.md`](../../../docs/specs/webhook-binding.md) WB-8 states and §4.2 adopts:
64/// an expected value derived by the code under test is an expectation that cannot disagree.
65pub const F_SILENCE_BASE64: &str = "/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////w==";
66
67/// Spec §4.2's **F-ramp** in base64: the 160 bytes `0x00, 0x01, … 0x9F`.
68///
69/// Also the first frame of the tone this peer speaks ([`tone_frame`]), so a bridge test can
70/// correlate what reached the media path against a literal from the spec.
71pub const F_RAMP_BASE64: &str = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2enw==";
72
73/// The bearer the peer expects unless a test configures another one.
74///
75/// A fixture value with no resemblance to a credential shape, so a leaked log line from a test
76/// run cannot be mistaken for a real key by a scanner or by a person.
77pub const FIXTURE_BEARER: &str = "fixture-bearer";
78
79/// How long [`RealtimePeer::observe`] waits before reporting that it saw nothing.
80///
81/// A **bound on failure**: how long the fixture waits before concluding the observation is not
82/// coming. It orders nothing — every wait in this module completes on the record changing, and
83/// this only stops a broken peer from hanging a suite until CI's own timeout kills it.
84pub const OBSERVATION_BOUND: Duration = Duration::from_secs(10);
85
86/// One frame of the tone the peer speaks, by position in the response.
87///
88/// The tone is a sawtooth over the G.711 code space: frame `index` carries the bytes
89/// `index * 160 …` counting upward and wrapping. Two properties earn it:
90///
91/// - **Frame 0 is spec §4.2's F-ramp**, so the bytes a bridge test expects at `send_encoded` are
92///   the spec's own vector rather than a fixture's invention.
93/// - **Every frame differs from its neighbours**, so a test that receives audio can say *which*
94///   frames arrived and in what order — which is what makes a truncated response (ORB-8)
95///   distinguishable from a late one.
96#[must_use]
97pub fn tone_frame(index: usize) -> [u8; FRAME_BYTES] {
98    let mut frame = [0u8; FRAME_BYTES];
99    let base = index.wrapping_mul(FRAME_BYTES);
100    for (offset, byte) in frame.iter_mut().enumerate() {
101        *byte = u8::try_from(base.wrapping_add(offset) % 256).unwrap_or_default();
102    }
103    frame
104}
105
106/// The first `frames` frames of the tone, concatenated — what a bridge should hand to the media
107/// path after receiving that many deltas.
108#[must_use]
109pub fn tone_bytes(frames: usize) -> Vec<u8> {
110    (0..frames).flat_map(tone_frame).collect()
111}
112
113// ------------------------------------------------------------------------------ the record ----
114
115/// How one upgrade attempt was answered.
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum UpgradeOutcome {
118    /// The bearer matched and the peer replied 101.
119    Accepted,
120    /// The bearer was wrong or absent and the peer replied with this status before the 101,
121    /// which is what §6 records as `AuthRefused`.
122    Refused(u16),
123}
124
125/// What one upgrade attempt carried.
126///
127/// The `Authorization` header is kept verbatim because ORB-1 asserts that the resolved secret's
128/// *bytes* reached the wire, and an assertion against a redacted record could not fail. Nothing
129/// here is logged; the value only ever lives in a test's memory, and only fixture keys are ever
130/// presented to this peer.
131#[derive(Debug, Clone)]
132pub struct Upgrade {
133    /// The request target as sent — path and query, so ORB-1 can read `?model=` from it.
134    pub target: String,
135    /// The `Authorization` header verbatim, or `None` when the request carried none.
136    pub authorization: Option<String>,
137    /// Every header name the request carried, lowercased. ORB-1 asserts the retired beta header
138    /// is *absent*, which is only evidence if the peer would have seen it.
139    pub header_names: Vec<String>,
140    /// How the peer answered.
141    pub outcome: UpgradeOutcome,
142}
143
144/// A client event as the peer read it.
145///
146/// The three named variants are spec §5.1's exhaustive client subset. The other two exist so
147/// that a bridge sending anything else is a visible fact rather than an absence: ORB-5's claim is
148/// that *only* the three arrive, and a record that could not represent a fourth would prove it
149/// vacuously.
150#[derive(Debug, Clone)]
151pub enum ClientEvent {
152    /// `session.update`, kept whole — a test reads the session object from it to check the
153    /// formats §3 pins to the call's negotiated codec.
154    SessionUpdate(Value),
155    /// `input_audio_buffer.append`, with its `audio` member decoded per RFC 4648 §4.
156    Append {
157        /// The decoded payload: one 20 ms G.711 frame, per §4.1.
158        audio: Vec<u8>,
159    },
160    /// `response.cancel` — barge-in, per §4.3.
161    Cancel,
162    /// A JSON event whose `type` is outside §5.1. A bridge that sends one has a defect against
163    /// ORB-5.
164    Outside {
165        /// The `type` member as it arrived.
166        event_type: String,
167    },
168    /// A frame the peer could not read as a client event at all: not JSON, no string `type`, or
169    /// binary.
170    Unreadable {
171        /// What was wrong with it, for the failure message.
172        reason: String,
173    },
174}
175
176/// Everything the peer observed, and everything it emitted.
177///
178/// A snapshot: [`RealtimePeer::record`] clones it, so a test reads a consistent picture rather
179/// than a moving one. The counts span the peer's whole lifetime and every connection it served,
180/// which is what lets ORB-16 assert that no *second* upgrade was attempted.
181#[derive(Debug, Clone, Default)]
182pub struct Record {
183    /// Every upgrade attempt, in order.
184    pub upgrades: Vec<Upgrade>,
185    /// Every client event, in order.
186    pub client_events: Vec<ClientEvent>,
187    /// Every appended audio byte, concatenated in arrival order — the uplink as the far end
188    /// heard it.
189    pub appended_audio: Vec<u8>,
190    /// RFC 6455 Ping frames received. Liveness is the client's timer; this is how a test proves
191    /// the probe arrived at all.
192    pub pings: usize,
193    /// `response.output_audio.delta` events actually written to the socket.
194    pub deltas_sent: usize,
195    /// Deltas a test directed that the peer refused to send because the response had been
196    /// cancelled ([`CancelPolicy::Truncate`]).
197    pub deltas_suppressed: usize,
198    /// Connections that have ended, however they ended.
199    pub sessions_ended: usize,
200}
201
202impl Record {
203    /// How many `input_audio_buffer.append` events arrived.
204    #[must_use]
205    pub fn appends(&self) -> usize {
206        self.client_events
207            .iter()
208            .filter(|event| matches!(event, ClientEvent::Append { .. }))
209            .count()
210    }
211
212    /// How many `response.cancel` events arrived.
213    #[must_use]
214    pub fn cancels(&self) -> usize {
215        self.client_events
216            .iter()
217            .filter(|event| matches!(event, ClientEvent::Cancel))
218            .count()
219    }
220
221    /// Every `session.update` event, whole.
222    #[must_use]
223    pub fn session_updates(&self) -> Vec<&Value> {
224        self.client_events
225            .iter()
226            .filter_map(|event| match event {
227                ClientEvent::SessionUpdate(update) => Some(update),
228                _ => None,
229            })
230            .collect()
231    }
232
233    /// Everything the client sent that spec §5.1 does not allow it to send.
234    ///
235    /// ORB-5 passes exactly when this is empty.
236    #[must_use]
237    pub fn events_outside_the_client_subset(&self) -> Vec<String> {
238        self.client_events
239            .iter()
240            .filter_map(|event| match event {
241                ClientEvent::Outside { event_type } => Some(event_type.clone()),
242                ClientEvent::Unreadable { reason } => Some(format!("unreadable: {reason}")),
243                _ => None,
244            })
245            .collect()
246    }
247
248    /// Upgrades the peer accepted.
249    #[must_use]
250    pub fn accepted(&self) -> usize {
251        self.outcomes(UpgradeOutcome::Accepted)
252    }
253
254    /// Upgrades the peer refused, whatever the status.
255    #[must_use]
256    pub fn refused(&self) -> usize {
257        self.upgrades
258            .iter()
259            .filter(|upgrade| matches!(upgrade.outcome, UpgradeOutcome::Refused(_)))
260            .count()
261    }
262
263    fn outcomes(&self, outcome: UpgradeOutcome) -> usize {
264        self.upgrades
265            .iter()
266            .filter(|upgrade| upgrade.outcome == outcome)
267            .count()
268    }
269}
270
271// -------------------------------------------------------------------- the configured modes ----
272
273/// Which setup acknowledgement the peer withholds (ORB-15).
274///
275/// §3 gives each a 10 s bound, and `SetupTimeout` is the outcome when one is missed. Withholding
276/// is not silence on the socket: the peer keeps reading, so the client's timer is the only thing
277/// that can end the session, which is exactly the claim the vector makes.
278#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
279pub enum Withhold {
280    /// Answer setup the way the spec says.
281    #[default]
282    Nothing,
283    /// Never send `session.created`.
284    SessionCreated,
285    /// Never send `session.updated`, however many `session.update` events arrive.
286    SessionUpdated,
287}
288
289/// Where the peer stops serving the socket while holding it open (ORB-14).
290///
291/// A stall is not a close: the connection stays established and unread, so no Pong is ever
292/// written and the client's liveness timer (§6: 30 s + 10 s) is the only thing that can end it.
293#[derive(Debug, Clone, Copy, PartialEq, Eq)]
294pub enum StallPoint {
295    /// Answer the upgrade, then nothing at all — not even `session.created`. This is ORB-14's
296    /// script: "peer answers the upgrade then goes silent".
297    Upgrade,
298    /// Answer setup normally, then go silent the moment the first uplink frame arrives: the
299    /// mid-call stall, where audio was already flowing when the far end stopped. The frame is
300    /// read and recorded first, so the record shows the peer was alive when it went quiet.
301    Session,
302}
303
304/// What the peer does with a response after the client cancels it (§4.3).
305#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
306pub enum CancelPolicy {
307    /// Send no further delta for the cancelled response: a directed delta is suppressed and
308    /// counted in [`Record::deltas_suppressed`]. This is what lets a bridge test assert
309    /// truncation as a fact.
310    #[default]
311    Truncate,
312    /// Keep sending directed deltas after the cancel. ORB-8's script needs this: the spec claims
313    /// no bound on how many deltas arrive after a cancel, because that number is the peer's, and
314    /// `bridge_cancelled_deltas` counts exactly the events "the peer chose to send".
315    KeepStreaming,
316}
317
318/// One frame the peer sends that no one can read as an event (§5.3, ORB-13 and ORB-18).
319///
320/// Each variant is one row of the read-set rule, and each is a real frame on the wire rather
321/// than an error injected into the client.
322#[derive(Debug, Clone)]
323pub enum Malformed {
324    /// `not json{` — a text frame that is not JSON at all.
325    NotJson,
326    /// A JSON object with no `type` member.
327    NoType,
328    /// A binary frame. Every event in this contract is a JSON text frame.
329    Binary,
330    /// A `response.output_audio.delta` whose `delta` is `not base64!!`.
331    DeltaNotBase64 {
332        /// The response the delta claims to belong to.
333        response: String,
334    },
335    /// A `response.output_audio.delta` with no `delta` member at all.
336    DeltaMissing {
337        /// The response the delta claims to belong to.
338        response: String,
339    },
340    /// A `response.output_audio.done` with no `response_id`.
341    AudioDoneWithoutResponseId,
342}
343
344/// What became of a directed event.
345#[derive(Debug, Clone, Copy, PartialEq, Eq)]
346pub enum Emission {
347    /// The frame was written to the socket before this result was returned.
348    Sent,
349    /// The response had been cancelled and [`CancelPolicy::Truncate`] is in force, so nothing
350    /// was written.
351    SuppressedByCancel,
352}
353
354/// Everything that can go wrong driving the peer.
355#[derive(Debug, thiserror::Error)]
356#[non_exhaustive]
357pub enum PeerError {
358    /// The loopback listener could not be bound.
359    #[error("the stand-in peer could not bind a loopback listener: {0}")]
360    Bind(String),
361    /// No client is connected, so there is nothing to send the directive to.
362    #[error("the stand-in peer has no connected session")]
363    NoSession,
364    /// The connection ended before the directive could be performed.
365    #[error("the stand-in peer's session ended before the directive was performed")]
366    SessionEnded,
367    /// The record never satisfied the condition within [`OBSERVATION_BOUND`].
368    #[error("the stand-in peer did not observe {what} within {OBSERVATION_BOUND:?}")]
369    NotObserved {
370        /// What the caller was waiting for, in its own words.
371        what: String,
372    },
373}
374
375// ------------------------------------------------------------------------- the peer itself ----
376
377/// How the peer behaves, before it is started.
378#[derive(Debug, Clone)]
379pub struct PeerConfig {
380    bearer: String,
381    withhold: Withhold,
382    stall: Option<StallPoint>,
383    cancel: CancelPolicy,
384}
385
386impl Default for PeerConfig {
387    fn default() -> Self {
388        Self::new()
389    }
390}
391
392impl PeerConfig {
393    /// A peer that behaves: [`FIXTURE_BEARER`], both acknowledgements, no stall, cancel honoured.
394    #[must_use]
395    pub fn new() -> Self {
396        Self {
397            bearer: FIXTURE_BEARER.to_owned(),
398            withhold: Withhold::Nothing,
399            stall: None,
400            cancel: CancelPolicy::Truncate,
401        }
402    }
403
404    /// The bearer the upgrade must carry verbatim. Anything else is refused 401 (ORB-10).
405    #[must_use]
406    pub fn expecting_bearer(mut self, bearer: &str) -> Self {
407        bearer.clone_into(&mut self.bearer);
408        self
409    }
410
411    /// Withhold one of setup's acknowledgements (ORB-15).
412    #[must_use]
413    pub fn withholding(mut self, withhold: Withhold) -> Self {
414        self.withhold = withhold;
415        self
416    }
417
418    /// Go silent at this point while holding the socket open (ORB-14).
419    #[must_use]
420    pub fn stalling_at(mut self, stall: StallPoint) -> Self {
421        self.stall = Some(stall);
422        self
423    }
424
425    /// What to do with directed deltas after the client cancels (§4.3).
426    #[must_use]
427    pub fn on_cancel(mut self, cancel: CancelPolicy) -> Self {
428        self.cancel = cancel;
429        self
430    }
431
432    /// Bind a loopback listener and start serving.
433    ///
434    /// The peer serves connections until it is dropped or [`RealtimePeer::shutdown`] is awaited.
435    pub async fn start(self) -> Result<RealtimePeer, PeerError> {
436        let listener = TcpListener::bind("127.0.0.1:0")
437            .await
438            .map_err(|error| PeerError::Bind(error.to_string()))?;
439        let addr = listener
440            .local_addr()
441            .map_err(|error| PeerError::Bind(error.to_string()))?;
442        let shared = Arc::new(Shared::default());
443        let shutdown = CancellationToken::new();
444        let accepting = tokio::spawn(accept(
445            listener,
446            self,
447            Arc::clone(&shared),
448            shutdown.clone(),
449        ));
450        Ok(RealtimePeer {
451            url: format!("ws://{addr}/v1/realtime"),
452            addr,
453            shared,
454            shutdown,
455            accepting: Some(accepting),
456        })
457    }
458}
459
460/// A running stand-in peer.
461///
462/// Dropping it cancels the listener and every session it is serving; there is no way to leave one
463/// running past the end of a test, which is what keeps a suite's ports and tasks bounded.
464#[derive(Debug)]
465pub struct RealtimePeer {
466    url: String,
467    addr: SocketAddr,
468    shared: Arc<Shared>,
469    shutdown: CancellationToken,
470    accepting: Option<JoinHandle<()>>,
471}
472
473impl Drop for RealtimePeer {
474    fn drop(&mut self) {
475        self.shutdown.cancel();
476    }
477}
478
479impl RealtimePeer {
480    /// The URL a client connects to, without the `?model=` the client is expected to add.
481    #[must_use]
482    pub fn url(&self) -> &str {
483        &self.url
484    }
485
486    /// The loopback address the peer is listening on.
487    #[must_use]
488    pub fn addr(&self) -> SocketAddr {
489        self.addr
490    }
491
492    /// A snapshot of everything observed so far.
493    #[must_use]
494    pub fn record(&self) -> Record {
495        self.shared.snapshot()
496    }
497
498    /// Wait until the record satisfies `condition`, and return it.
499    ///
500    /// The wait completes on the record changing, never on a clock; `what` names the observation
501    /// so a peer that never produces it fails with a sentence rather than with a hang.
502    pub async fn observe<F>(&self, what: &str, condition: F) -> Result<Record, PeerError>
503    where
504        F: Fn(&Record) -> bool,
505    {
506        // OBSERVATION_BOUND bounds a failure — how long to wait before concluding the
507        // observation is not coming. The loop below completes on the notification, so this
508        // duration orders nothing.
509        tokio::time::timeout(OBSERVATION_BOUND, async {
510            loop {
511                let changed = self.shared.changed.notified();
512                {
513                    let record = self.shared.lock();
514                    if condition(&record) {
515                        return record.clone();
516                    }
517                }
518                changed.await;
519            }
520        })
521        .await
522        .map_err(|_elapsed| PeerError::NotObserved {
523            what: what.to_owned(),
524        })
525    }
526
527    /// Wait until at least one upgrade has been answered, either way.
528    pub async fn await_upgrade(&self) -> Result<Record, PeerError> {
529        self.observe("an upgrade", |record| !record.upgrades.is_empty())
530            .await
531    }
532
533    /// Wait until the client's `session.update` has been read.
534    pub async fn await_session_update(&self) -> Result<Record, PeerError> {
535        self.observe("a session.update", |record| {
536            !record.session_updates().is_empty()
537        })
538        .await
539    }
540
541    /// Wait until `count` uplink frames have been appended.
542    pub async fn await_appends(&self, count: usize) -> Result<Record, PeerError> {
543        self.observe(&format!("{count} appends"), move |record| {
544            record.appends() >= count
545        })
546        .await
547    }
548
549    /// Wait until the client has cancelled a response.
550    pub async fn await_cancel(&self) -> Result<Record, PeerError> {
551        self.observe("a response.cancel", |record| record.cancels() > 0)
552            .await
553    }
554
555    /// Send one `response.output_audio.delta` carrying these bytes, base64 per RFC 4648 §4.
556    pub async fn send_delta(&self, response: &str, audio: &[u8]) -> Result<Emission, PeerError> {
557        self.direct(Action::Delta {
558            response: response.to_owned(),
559            audio: audio.to_vec(),
560        })
561        .await
562    }
563
564    /// Speak `frames` frames of the tone as one delta each, stopping at the first one the peer
565    /// suppresses. Returns how many reached the socket.
566    pub async fn speak_tone(&self, response: &str, frames: usize) -> Result<usize, PeerError> {
567        let mut sent = 0;
568        for frame in 0..frames {
569            if self.send_delta(response, &tone_frame(frame)).await? == Emission::SuppressedByCancel
570            {
571                break;
572            }
573            sent += 1;
574        }
575        Ok(sent)
576    }
577
578    /// Send `response.output_audio.done`, which is where §4.1's partial-frame padding happens.
579    pub async fn send_audio_done(&self, response: &str) -> Result<Emission, PeerError> {
580        self.direct(Action::Scripted(Scripted::AudioDone {
581            response: Some(response.to_owned()),
582        }))
583        .await
584    }
585
586    /// Send `response.done` with this status, ending any cancel-race window (§4.3).
587    pub async fn send_response_done(
588        &self,
589        response: &str,
590        status: &str,
591    ) -> Result<Emission, PeerError> {
592        self.direct(Action::Scripted(Scripted::ResponseDone {
593            response: response.to_owned(),
594            status: status.to_owned(),
595        }))
596        .await
597    }
598
599    /// Send `input_audio_buffer.speech_started`, the barge-in trigger of §4.3.
600    pub async fn send_speech_started(&self) -> Result<Emission, PeerError> {
601        self.direct(Action::Scripted(Scripted::SpeechStarted)).await
602    }
603
604    /// Send an `error` event (§5.2), which is either the cancel race or session-fatal.
605    pub async fn send_error(&self, code: &str, message: &str) -> Result<Emission, PeerError> {
606        self.direct(Action::Scripted(Scripted::Error {
607            code: code.to_owned(),
608            message: message.to_owned(),
609        }))
610        .await
611    }
612
613    /// Send an event outside §5.2 — what ORB-12 requires be ignored with a counter.
614    pub async fn send_unknown(&self, event_type: &str) -> Result<Emission, PeerError> {
615        self.direct(Action::Scripted(Scripted::Unknown {
616            event_type: event_type.to_owned(),
617        }))
618        .await
619    }
620
621    /// Send a frame that cannot be read as an event (§5.3).
622    pub async fn send_malformed(&self, malformed: Malformed) -> Result<Emission, PeerError> {
623        self.direct(Action::Malformed(malformed)).await
624    }
625
626    /// Send one text frame of at least `bytes` bytes — §5.3's 1 MiB bound is 1 048 576 (ORB-11).
627    pub async fn send_oversize(&self, bytes: usize) -> Result<Emission, PeerError> {
628        self.direct(Action::Scripted(Scripted::Oversize { bytes }))
629            .await
630    }
631
632    /// Close the connection with 1000, the way a well-behaved far end ends a session (ORB-16).
633    pub async fn close_normally(&self) -> Result<Emission, PeerError> {
634        self.direct(Action::Close { code: 1000 }).await
635    }
636
637    /// Reset the connection: no close frame, no close handshake (ORB-16's second half).
638    pub async fn reset(&self) -> Result<Emission, PeerError> {
639        self.direct(Action::Reset).await
640    }
641
642    /// Stop serving and join every task the peer owns.
643    ///
644    /// Dropping the peer does the same thing without the join; this exists for a test that wants
645    /// to prove there is no orphan left behind.
646    pub async fn shutdown(mut self) {
647        self.shutdown.cancel();
648        if let Some(accepting) = self.accepting.take() {
649            let _joined = accepting.await;
650        }
651    }
652
653    async fn direct(&self, action: Action) -> Result<Emission, PeerError> {
654        let session = self.shared.session().ok_or(PeerError::NoSession)?;
655        let (done, performed) = oneshot::channel();
656        session
657            .send(Directive { action, done })
658            .await
659            .map_err(|_closed| PeerError::SessionEnded)?;
660        performed.await.map_err(|_closed| PeerError::SessionEnded)
661    }
662}
663
664// ------------------------------------------------------------------------------- internals ----
665
666/// The record, its change notification, and the inbox of the connection directives go to.
667#[derive(Debug, Default)]
668struct Shared {
669    record: Mutex<Record>,
670    changed: Notify,
671    session: Mutex<Option<(u64, mpsc::Sender<Directive>)>>,
672    generations: Mutex<u64>,
673}
674
675impl Shared {
676    fn lock(&self) -> std::sync::MutexGuard<'_, Record> {
677        self.record.lock().unwrap_or_else(PoisonError::into_inner)
678    }
679
680    fn update<F: FnOnce(&mut Record)>(&self, edit: F) {
681        edit(&mut self.lock());
682        self.changed.notify_waiters();
683    }
684
685    fn snapshot(&self) -> Record {
686        self.lock().clone()
687    }
688
689    /// Make this connection the one directives go to, and return its generation.
690    ///
691    /// The most recent connection wins. A peer serving two at once is a fixture serving a client
692    /// that reconnected when it should not have (ORB-16), and the record — which counts every
693    /// upgrade — is what that vector asserts on, not this slot.
694    fn register(&self, sender: mpsc::Sender<Directive>) -> u64 {
695        let mut generations = self
696            .generations
697            .lock()
698            .unwrap_or_else(PoisonError::into_inner);
699        *generations += 1;
700        let generation = *generations;
701        *self.session.lock().unwrap_or_else(PoisonError::into_inner) = Some((generation, sender));
702        generation
703    }
704
705    fn unregister(&self, generation: u64) {
706        let mut session = self.session.lock().unwrap_or_else(PoisonError::into_inner);
707        if session
708            .as_ref()
709            .is_some_and(|(open, _)| *open == generation)
710        {
711            *session = None;
712        }
713    }
714
715    fn session(&self) -> Option<mpsc::Sender<Directive>> {
716        self.session
717            .lock()
718            .unwrap_or_else(PoisonError::into_inner)
719            .as_ref()
720            .map(|(_, sender)| sender.clone())
721    }
722}
723
724/// One thing a test asked the peer to put on the socket.
725#[derive(Debug)]
726struct Directive {
727    action: Action,
728    done: oneshot::Sender<Emission>,
729}
730
731#[derive(Debug)]
732enum Action {
733    /// Downlink audio, which is the one action the cancel policy can withhold.
734    Delta { response: String, audio: Vec<u8> },
735    /// An event whose whole behaviour is the JSON it puts on the socket.
736    Scripted(Scripted),
737    /// A frame nobody can read as an event (§5.3).
738    Malformed(Malformed),
739    /// A close frame carrying this code.
740    Close { code: u16 },
741    /// An abrupt end with no close handshake at all.
742    Reset,
743}
744
745#[derive(Debug)]
746enum Scripted {
747    AudioDone { response: Option<String> },
748    ResponseDone { response: String, status: String },
749    SpeechStarted,
750    Error { code: String, message: String },
751    Unknown { event_type: String },
752    Oversize { bytes: usize },
753}
754
755/// The socket the peer serves. Not split into a sink and a stream: the connection never reads and
756/// writes at once, and keeping it whole is what leaves [`TcpStream::set_linger`] reachable for
757/// the abrupt reset.
758type Socket = tokio_tungstenite::WebSocketStream<TcpStream>;
759
760/// What the client's last `response.cancel` applies to.
761///
762/// `response.cancel` carries no `response_id` (§5.1): the in-progress response is the target. So
763/// the peer resolves it against what it was sending at the time, and a cancel that arrives with
764/// nothing in flight — which §4.3 permits — applies to whatever response starts next rather than
765/// being lost.
766#[derive(Debug, Default, PartialEq, Eq)]
767enum Cancelled {
768    #[default]
769    No,
770    Pending,
771    Response(String),
772}
773
774/// One connection's protocol state.
775#[derive(Debug, Default)]
776struct Session {
777    /// The response the last delta belonged to.
778    in_flight: Option<String>,
779    /// The outstanding cancel, if any.
780    cancelled: Cancelled,
781    events: u32,
782}
783
784impl Session {
785    fn next_event_id(&mut self) -> String {
786        self.events += 1;
787        format!("event_{:03}", self.events)
788    }
789
790    /// Whether a delta for this response must be withheld.
791    fn is_cancelled(&self, response: &str) -> bool {
792        match &self.cancelled {
793            Cancelled::No => false,
794            Cancelled::Pending => true,
795            Cancelled::Response(cancelled) => cancelled == response,
796        }
797    }
798}
799
800async fn accept(
801    listener: TcpListener,
802    config: PeerConfig,
803    shared: Arc<Shared>,
804    shutdown: CancellationToken,
805) {
806    let mut sessions = JoinSet::new();
807    loop {
808        tokio::select! {
809            () = shutdown.cancelled() => break,
810            accepted = listener.accept() => {
811                let Ok((stream, _from)) = accepted else { break };
812                sessions.spawn(serve(
813                    stream,
814                    config.clone(),
815                    Arc::clone(&shared),
816                    shutdown.child_token(),
817                ));
818            }
819            Some(_finished) = sessions.join_next(), if !sessions.is_empty() => {}
820        }
821    }
822    // Cancellation reaches the sessions through the shared token; this joins them so a peer that
823    // has been shut down leaves no task behind.
824    sessions.shutdown().await;
825}
826
827#[allow(clippy::result_large_err)] // the handshake callback's error is tungstenite's own response type
828async fn serve(
829    stream: TcpStream,
830    config: PeerConfig,
831    shared: Arc<Shared>,
832    shutdown: CancellationToken,
833) {
834    let expected = format!("Bearer {}", config.bearer);
835    let inspecting = Arc::clone(&shared);
836    let upgraded = accept_hdr_async(stream, move |request: &Request, response: Response| {
837        inspect_upgrade(request, response, &expected, &inspecting)
838    })
839    .await;
840    let Ok(mut socket) = upgraded else {
841        // A refusal is already in the record, written by the callback that decided it.
842        return;
843    };
844
845    if config.stall == Some(StallPoint::Upgrade) {
846        // Answer the upgrade and nothing else, holding the connection open (ORB-14). Nothing is
847        // read, so tungstenite never writes the Pong it would otherwise queue.
848        shutdown.cancelled().await;
849        return;
850    }
851
852    let (directives, mut inbox) = mpsc::channel::<Directive>(16);
853    // The connection holds a sender of its own, so a later connection replacing it in the shared
854    // slot cannot make this one's inbox look closed.
855    let _own = directives.clone();
856    let generation = shared.register(directives);
857    let mut session = Session::default();
858
859    if config.withhold != Withhold::SessionCreated {
860        let created = json!({
861            "type": "session.created",
862            "event_id": session.next_event_id(),
863            "session": {"id": "sess_fixture", "object": "realtime.session", "type": "realtime"},
864        });
865        if write(&mut socket, created).await == Next::End {
866            finish(&shared, generation);
867            return;
868        }
869    }
870
871    loop {
872        let step = tokio::select! {
873            biased;
874            () = shutdown.cancelled() => Step::Stop,
875            frame = socket.next() => Step::Inbound(frame),
876            directive = inbox.recv() => Step::Directed(directive),
877        };
878        let next = match step {
879            Step::Stop | Step::Directed(None) => Next::End,
880            Step::Inbound(frame) => {
881                inbound(frame, &mut socket, &config, &shared, &mut session).await
882            }
883            Step::Directed(Some(directive)) => {
884                directed(directive, &mut socket, &config, &shared, &mut session).await
885            }
886        };
887        match next {
888            Next::Serve => {}
889            Next::End => break,
890            Next::Stall => {
891                // Holding the socket is the whole behaviour: it stays established, unread and
892                // unwritten, so no Pong is ever produced and the client's liveness timer is the
893                // only thing that can end the session (ORB-14). Returning instead would send a
894                // FIN, which is a different vector.
895                shutdown.cancelled().await;
896                break;
897            }
898        }
899    }
900    finish(&shared, generation);
901}
902
903/// What the connection loop woke for.
904enum Step {
905    Stop,
906    Inbound(Option<Result<Message, tokio_tungstenite::tungstenite::Error>>),
907    Directed(Option<Directive>),
908}
909
910/// What the connection loop does next.
911#[derive(Debug, Clone, Copy, PartialEq, Eq)]
912enum Next {
913    /// Keep serving.
914    Serve,
915    /// The connection is over.
916    End,
917    /// Stop serving *without* closing: hold the socket open, read nothing, write nothing.
918    Stall,
919}
920
921fn finish(shared: &Arc<Shared>, generation: u64) {
922    shared.unregister(generation);
923    shared.update(|record| record.sessions_ended += 1);
924}
925
926// The error variant is the HTTP response tungstenite will write; its size is that crate's shape,
927// not a choice available here.
928#[allow(clippy::result_large_err)]
929fn inspect_upgrade(
930    request: &Request,
931    response: Response,
932    expected: &str,
933    shared: &Arc<Shared>,
934) -> Result<Response, ErrorResponse> {
935    let authorization = request
936        .headers()
937        .get("authorization")
938        .and_then(|value| value.to_str().ok())
939        .map(str::to_owned);
940    let authorised = authorization.as_deref() == Some(expected);
941    let upgrade = Upgrade {
942        target: request.uri().to_string(),
943        authorization,
944        header_names: request
945            .headers()
946            .keys()
947            .map(|name| name.as_str().to_owned())
948            .collect(),
949        outcome: if authorised {
950            UpgradeOutcome::Accepted
951        } else {
952            UpgradeOutcome::Refused(401)
953        },
954    };
955    shared.update(move |record| record.upgrades.push(upgrade));
956    if authorised {
957        Ok(response)
958    } else {
959        // §6: the upgrade is refused with a 4xx before the 101, and §2 forbids any outcome from
960        // carrying the credential — so this body names neither what was presented nor what was
961        // expected.
962        let mut refusal = ErrorResponse::new(Some(
963            "invalid_request_error: the bearer token is missing or does not match".to_owned(),
964        ));
965        *refusal.status_mut() = StatusCode::UNAUTHORIZED;
966        Err(refusal)
967    }
968}
969
970async fn inbound(
971    frame: Option<Result<Message, tokio_tungstenite::tungstenite::Error>>,
972    socket: &mut Socket,
973    config: &PeerConfig,
974    shared: &Arc<Shared>,
975    session: &mut Session,
976) -> Next {
977    match frame {
978        Some(Ok(Message::Text(text))) => {
979            let event = read_client_event(&text);
980            match &event {
981                ClientEvent::Cancel => {
982                    session.cancelled = session
983                        .in_flight
984                        .clone()
985                        .map_or(Cancelled::Pending, Cancelled::Response);
986                }
987                ClientEvent::Append { audio } => {
988                    let audio = audio.clone();
989                    shared.update(|record| record.appended_audio.extend_from_slice(&audio));
990                }
991                _ => {}
992            }
993            let reply_wanted = matches!(event, ClientEvent::SessionUpdate(_))
994                && config.withhold != Withhold::SessionUpdated;
995            let stall_now = config.stall == Some(StallPoint::Session)
996                && matches!(event, ClientEvent::Append { .. });
997            shared.update(move |record| record.client_events.push(event));
998            if stall_now {
999                // Mid-call silence: setup was answered and the first uplink frame was read and
1000                // recorded, and nothing is written from here on. The record proves the peer was
1001                // alive when it went quiet, which is what separates a stall from a dead socket.
1002                return Next::Stall;
1003            }
1004            if reply_wanted {
1005                let updated = json!({
1006                    "type": "session.updated",
1007                    "event_id": session.next_event_id(),
1008                    "session": {"id": "sess_fixture", "object": "realtime.session", "type": "realtime"},
1009                });
1010                return write(socket, updated).await;
1011            }
1012            Next::Serve
1013        }
1014        Some(Ok(Message::Binary(bytes))) => {
1015            shared.update(move |record| {
1016                record.client_events.push(ClientEvent::Unreadable {
1017                    reason: format!("a binary frame of {} bytes", bytes.len()),
1018                });
1019            });
1020            Next::Serve
1021        }
1022        Some(Ok(Message::Ping(_))) => {
1023            shared.update(|record| record.pings += 1);
1024            // tungstenite queues the Pong itself and writes it on the next read or write; this
1025            // flush is what makes "answered" true now rather than at the next frame.
1026            let _flushed = socket.flush().await;
1027            Next::Serve
1028        }
1029        Some(Ok(Message::Pong(_) | Message::Frame(_))) => Next::Serve,
1030        Some(Ok(Message::Close(_)) | Err(_)) | None => Next::End,
1031    }
1032}
1033
1034fn read_client_event(text: &str) -> ClientEvent {
1035    let Ok(event) = serde_json::from_str::<Value>(text) else {
1036        return ClientEvent::Unreadable {
1037            reason: "a text frame that is not JSON".to_owned(),
1038        };
1039    };
1040    let Some(event_type) = event.get("type").and_then(Value::as_str) else {
1041        return ClientEvent::Unreadable {
1042            reason: "a JSON frame with no string `type`".to_owned(),
1043        };
1044    };
1045    match event_type {
1046        "session.update" => ClientEvent::SessionUpdate(event.clone()),
1047        "response.cancel" => ClientEvent::Cancel,
1048        "input_audio_buffer.append" => match event
1049            .get("audio")
1050            .and_then(Value::as_str)
1051            .map(|audio| BASE64.decode(audio))
1052        {
1053            Some(Ok(audio)) => ClientEvent::Append { audio },
1054            Some(Err(error)) => ClientEvent::Unreadable {
1055                reason: format!("an append whose audio is not RFC 4648 §4 base64: {error}"),
1056            },
1057            None => ClientEvent::Unreadable {
1058                reason: "an append with no string `audio` member".to_owned(),
1059            },
1060        },
1061        other => ClientEvent::Outside {
1062            event_type: other.to_owned(),
1063        },
1064    }
1065}
1066
1067async fn directed(
1068    directive: Directive,
1069    socket: &mut Socket,
1070    config: &PeerConfig,
1071    shared: &Arc<Shared>,
1072    session: &mut Session,
1073) -> Next {
1074    let Directive { action, done } = directive;
1075    match action {
1076        Action::Delta { response, audio } => {
1077            if config.cancel == CancelPolicy::Truncate && session.is_cancelled(&response) {
1078                shared.update(|record| record.deltas_suppressed += 1);
1079                let _answered = done.send(Emission::SuppressedByCancel);
1080                return Next::Serve;
1081            }
1082            session.in_flight = Some(response.clone());
1083            let delta = json!({
1084                "type": "response.output_audio.delta",
1085                "event_id": session.next_event_id(),
1086                "response_id": response,
1087                "item_id": "item_fixture",
1088                "output_index": 0,
1089                "content_index": 0,
1090                "delta": BASE64.encode(&audio),
1091            });
1092            let flow = write(socket, delta).await;
1093            if flow == Next::Serve {
1094                shared.update(|record| record.deltas_sent += 1);
1095            }
1096            answer(flow, done)
1097        }
1098        Action::Scripted(scripted) => {
1099            let event = scripted_event(scripted, session);
1100            answer(write(socket, event).await, done)
1101        }
1102        Action::Malformed(malformed) => malformed_frame(malformed, socket, session, done).await,
1103        Action::Close { code } => {
1104            let close = Message::Close(Some(CloseFrame {
1105                code: CloseCode::from(code),
1106                reason: Utf8Bytes::from_static("session ended"),
1107            }));
1108            let sent = socket.send(close).await.is_ok();
1109            let _flushed = socket.flush().await;
1110            let _answered = done.send(Emission::Sent);
1111            // The connection ends when the client's close echo arrives, which the inbound arm
1112            // reads; breaking here would drop the socket before the echo had anywhere to go.
1113            if sent { Next::Serve } else { Next::End }
1114        }
1115        Action::Reset => {
1116            // A zero linger turns the close into an RST: no close frame, no handshake, which is
1117            // the second half of ORB-16 and the one a graceful shutdown cannot produce.
1118            //
1119            // tokio deprecates `set_linger` because a *non-zero* linger makes closing block the
1120            // thread that drops the socket. Zero is the opposite case — the send buffer is
1121            // discarded and the far end is told so immediately — which is exactly what this
1122            // vector needs and what no other safe API in the workspace can produce.
1123            #[allow(deprecated)]
1124            let _lingered = socket.get_ref().set_linger(Some(Duration::ZERO));
1125            let _answered = done.send(Emission::Sent);
1126            Next::End
1127        }
1128    }
1129}
1130
1131/// Build the event for an action whose whole behaviour is the JSON it puts on the socket.
1132///
1133/// Each carries members beyond the read set of §5.2 — `item_id`, `output_index`, `object` — for
1134/// the same reason the spec's own vectors do: the bridge must read what it names and ignore the
1135/// rest, and a peer that sent only the read set could not tell the two apart.
1136fn scripted_event(scripted: Scripted, session: &mut Session) -> Value {
1137    match scripted {
1138        Scripted::AudioDone { response } => {
1139            let mut event = json!({
1140                "type": "response.output_audio.done",
1141                "event_id": session.next_event_id(),
1142                "item_id": "item_fixture",
1143                "output_index": 0,
1144                "content_index": 0,
1145            });
1146            if let (Some(response), Some(members)) = (response, event.as_object_mut()) {
1147                members.insert("response_id".to_owned(), Value::String(response));
1148            }
1149            event
1150        }
1151        Scripted::ResponseDone { response, status } => {
1152            session.in_flight = None;
1153            session.cancelled = Cancelled::No;
1154            json!({
1155                "type": "response.done",
1156                "event_id": session.next_event_id(),
1157                "response": {"id": response, "object": "realtime.response", "status": status},
1158            })
1159        }
1160        Scripted::SpeechStarted => json!({
1161            "type": "input_audio_buffer.speech_started",
1162            "event_id": session.next_event_id(),
1163            "audio_start_ms": 460,
1164            "item_id": "item_fixture",
1165        }),
1166        Scripted::Error { code, message } => json!({
1167            "type": "error",
1168            "event_id": session.next_event_id(),
1169            "error": {
1170                "type": "invalid_request_error",
1171                "code": code,
1172                "message": message,
1173                "param": Value::Null,
1174            },
1175        }),
1176        Scripted::Unknown { event_type } => {
1177            json!({"type": event_type, "event_id": session.next_event_id()})
1178        }
1179        Scripted::Oversize { bytes } => {
1180            // A well-formed delta whose payload alone exceeds the bound, so the client's limit is
1181            // tested on the frame's size and not on its shape (§5.3: the bound is enforced before
1182            // any JSON parsing).
1183            json!({
1184                "type": "response.output_audio.delta",
1185                "event_id": session.next_event_id(),
1186                "response_id": "resp_oversize",
1187                "delta": "A".repeat(bytes.next_multiple_of(4)),
1188            })
1189        }
1190    }
1191}
1192
1193async fn malformed_frame(
1194    malformed: Malformed,
1195    socket: &mut Socket,
1196    session: &mut Session,
1197    done: oneshot::Sender<Emission>,
1198) -> Next {
1199    let flow = match malformed {
1200        Malformed::NotJson => socket
1201            .send(Message::Text(Utf8Bytes::from_static("not json{")))
1202            .await
1203            .map_or(Next::End, |()| Next::Serve),
1204        Malformed::NoType => {
1205            let event = json!({"event_id": session.next_event_id(), "session": {}});
1206            write(socket, event).await
1207        }
1208        Malformed::Binary => socket
1209            .send(Message::Binary(WsBytes::from_static(b"\x00\x01binary")))
1210            .await
1211            .map_or(Next::End, |()| Next::Serve),
1212        Malformed::DeltaNotBase64 { response } => {
1213            let event = json!({
1214                "type": "response.output_audio.delta",
1215                "event_id": session.next_event_id(),
1216                "response_id": response,
1217                "delta": "not base64!!",
1218            });
1219            write(socket, event).await
1220        }
1221        Malformed::DeltaMissing { response } => {
1222            let event = json!({
1223                "type": "response.output_audio.delta",
1224                "event_id": session.next_event_id(),
1225                "response_id": response,
1226                "item_id": "item_fixture",
1227            });
1228            write(socket, event).await
1229        }
1230        Malformed::AudioDoneWithoutResponseId => {
1231            let event = json!({
1232                "type": "response.output_audio.done",
1233                "event_id": session.next_event_id(),
1234                "item_id": "item_fixture",
1235            });
1236            write(socket, event).await
1237        }
1238    };
1239    answer(flow, done)
1240}
1241
1242/// Resolve the directive that produced this write.
1243///
1244/// A failed write drops the sender rather than answering it, so the caller sees
1245/// [`PeerError::SessionEnded`] instead of a claim that a frame reached a socket which had
1246/// already gone.
1247fn answer(flow: Next, done: oneshot::Sender<Emission>) -> Next {
1248    if flow == Next::Serve {
1249        let _answered = done.send(Emission::Sent);
1250    }
1251    flow
1252}
1253
1254/// Write one event as a JSON text frame, flushing it so the directive that asked for it resolves
1255/// after the bytes are on the socket rather than before.
1256async fn write(socket: &mut Socket, event: Value) -> Next {
1257    if socket
1258        .send(Message::Text(event.to_string().into()))
1259        .await
1260        .is_err()
1261    {
1262        return Next::End;
1263    }
1264    match socket.flush().await {
1265        Ok(()) => Next::Serve,
1266        Err(_) => Next::End,
1267    }
1268}
1269
1270impl fmt::Display for Emission {
1271    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
1272        formatter.write_str(match self {
1273            Self::Sent => "sent",
1274            Self::SuppressedByCancel => "suppressed by cancel",
1275        })
1276    }
1277}