use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum RealtimeProtocolVersion {
#[serde(rename = "2")]
V2,
}
impl RealtimeProtocolVersion {
pub const CURRENT: Self = Self::V2;
pub const SUPPORTED: &'static [Self] = &[Self::V2];
#[must_use]
pub fn is_supported(self) -> bool {
Self::SUPPORTED.contains(&self)
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::V2 => "2",
}
}
#[must_use]
pub fn parse(raw: &str) -> Option<Self> {
Self::SUPPORTED
.iter()
.copied()
.find(|version| version.as_str() == raw)
}
}
impl std::fmt::Display for RealtimeProtocolVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum RealtimeErrorCode {
InvalidFrame,
ExpectedChannelOpen,
InvalidOpenToken,
OpenTokenExpired,
RoleMismatch,
TurningModeMismatch,
UnsupportedTurningMode,
TargetBusy,
UnsupportedProtocolVersion,
AudioFormatMismatch,
UnauthorizedRealm,
ToolCallTimeout,
InternalError,
ReconnectExhausted,
InvalidTarget,
ChannelNotBound,
RuntimeInternal,
RuntimeNotReady,
ProviderSessionClosed,
ProviderSessionFailed,
ProviderSessionUnavailable,
UnsupportedInputKind,
NoPendingTurn,
ObserverReadOnly,
UnexpectedChannelOpen,
CommitTurnUnavailable,
ChannelReconnecting,
BindingReleased,
AuthenticationFailed,
ContentFiltered,
ModelNotFound,
InvalidRequest,
}
impl RealtimeErrorCode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::InvalidFrame => "invalid_frame",
Self::ExpectedChannelOpen => "expected_channel_open",
Self::InvalidOpenToken => "invalid_open_token",
Self::OpenTokenExpired => "open_token_expired",
Self::RoleMismatch => "role_mismatch",
Self::TurningModeMismatch => "turning_mode_mismatch",
Self::UnsupportedTurningMode => "unsupported_turning_mode",
Self::TargetBusy => "target_busy",
Self::UnsupportedProtocolVersion => "unsupported_protocol_version",
Self::AudioFormatMismatch => "audio_format_mismatch",
Self::UnauthorizedRealm => "unauthorized_realm",
Self::ToolCallTimeout => "tool_call_timeout",
Self::InternalError => "internal_error",
Self::ReconnectExhausted => "reconnect_exhausted",
Self::InvalidTarget => "invalid_target",
Self::ChannelNotBound => "channel_not_bound",
Self::RuntimeInternal => "runtime_internal",
Self::RuntimeNotReady => "runtime_not_ready",
Self::ProviderSessionClosed => "provider_session_closed",
Self::ProviderSessionFailed => "provider_session_failed",
Self::ProviderSessionUnavailable => "provider_session_unavailable",
Self::UnsupportedInputKind => "unsupported_input_kind",
Self::NoPendingTurn => "no_pending_turn",
Self::ObserverReadOnly => "observer_read_only",
Self::UnexpectedChannelOpen => "unexpected_channel_open",
Self::CommitTurnUnavailable => "commit_turn_unavailable",
Self::ChannelReconnecting => "channel_reconnecting",
Self::BindingReleased => "binding_released",
Self::AuthenticationFailed => "authentication_failed",
Self::ContentFiltered => "content_filtered",
Self::ModelNotFound => "model_not_found",
Self::InvalidRequest => "invalid_request",
}
}
}
impl std::fmt::Display for RealtimeErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeAudioFormat {
pub mime_type: String,
pub sample_rate_hz: u32,
pub channels: u8,
}
impl RealtimeAudioFormat {
#[must_use]
pub fn pcm(sample_rate_hz: u32, channels: u8) -> Self {
Self {
mime_type: "audio/pcm".to_string(),
sample_rate_hz,
channels,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct AudioFormatMismatchContext {
pub expected: RealtimeAudioFormat,
pub actual: RealtimeAudioFormat,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct ToolCallTimeoutContext {
pub call_id: String,
pub elapsed_ms: u64,
pub timeout_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RealtimeChannelTarget {
SessionTarget {
session_id: String,
},
MobMember {
mob_id: String,
agent_identity: String,
},
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum RealtimeChannelRole {
Primary,
Observer,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum RealtimeTurningMode {
ProviderManaged,
ExplicitCommit,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum RealtimeInputKind {
Text,
Audio,
Video,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum RealtimeOutputKind {
Text,
Audio,
Video,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeReconnectPolicy {
pub max_attempts: u32,
pub initial_backoff_ms: u64,
pub max_backoff_ms: u64,
pub max_total_ms: u64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RealtimeToolTimeoutPolicy {
#[default]
Default,
Disabled,
Finite { timeout_ms: u64 },
}
impl RealtimeToolTimeoutPolicy {
pub const DEFAULT_TIMEOUT_MS: u64 = 15_000;
#[must_use]
pub fn is_default(&self) -> bool {
matches!(self, Self::Default)
}
#[must_use]
pub fn timeout_ms(&self) -> Option<u64> {
match self {
Self::Default => Some(Self::DEFAULT_TIMEOUT_MS),
Self::Disabled => None,
Self::Finite { timeout_ms } => Some(*timeout_ms),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeChannelConfig {
#[serde(default, skip_serializing_if = "RealtimeToolTimeoutPolicy::is_default")]
pub tool_timeout: RealtimeToolTimeoutPolicy,
}
impl RealtimeChannelConfig {
pub const DEFAULT_TOOL_TIMEOUT_MS: u64 = RealtimeToolTimeoutPolicy::DEFAULT_TIMEOUT_MS;
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeCapabilities {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub input_kinds: Vec<RealtimeInputKind>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub output_kinds: Vec<RealtimeOutputKind>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub turning_modes: Vec<RealtimeTurningMode>,
pub interrupt_supported: bool,
pub transcript_supported: bool,
pub tool_lifecycle_events_supported: bool,
pub video_supported: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub audio_input_format: Option<RealtimeAudioFormat>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub audio_output_format: Option<RealtimeAudioFormat>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum RealtimeChannelState {
Opening,
Ready,
Interrupted,
Reconnecting,
Closed,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeChannelStatus {
pub state: RealtimeChannelState,
#[serde(default)]
pub attempt_count: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_retry_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deadline_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeOpenRequest {
pub target: RealtimeChannelTarget,
pub role: RealtimeChannelRole,
pub turning_mode: RealtimeTurningMode,
#[serde(skip_serializing_if = "Option::is_none")]
pub reconnect_policy: Option<RealtimeReconnectPolicy>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub channel_config: Option<RealtimeChannelConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeOpenInfo {
pub ws_url: String,
pub open_token: String,
pub expires_at: String,
pub target: RealtimeChannelTarget,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub supported_protocol_versions: Vec<RealtimeProtocolVersion>,
pub default_protocol_version: RealtimeProtocolVersion,
pub capabilities: RealtimeCapabilities,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeStatusParams {
pub target: RealtimeChannelTarget,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeStatusResult {
pub status: RealtimeChannelStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeCapabilitiesParams {
pub target: RealtimeChannelTarget,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeCapabilitiesResult {
pub capabilities: RealtimeCapabilities,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeTextChunk {
pub text: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeTextDelta {
pub delta: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeAudioChunk {
pub mime_type: String,
pub sample_rate_hz: u32,
pub channels: u8,
pub data: String,
}
impl RealtimeAudioChunk {
#[must_use]
pub fn format(&self) -> RealtimeAudioFormat {
RealtimeAudioFormat {
mime_type: self.mime_type.clone(),
sample_rate_hz: self.sample_rate_hz,
channels: self.channels,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeVideoChunk {
pub mime_type: String,
pub data: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RealtimeInputChunk {
TextChunk(RealtimeTextChunk),
AudioChunk(RealtimeAudioChunk),
VideoChunk(RealtimeVideoChunk),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RealtimeOutputChunk {
TextDelta(RealtimeTextDelta),
AudioChunk(RealtimeAudioChunk),
VideoChunk(RealtimeVideoChunk),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RealtimeEvent {
InputTranscriptPartial {
text: String,
},
InputTranscriptFinal {
text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
prosody_hint: Option<String>,
},
TurnStarted,
TurnCommitted,
TurnCompleted,
OutputTextDelta {
delta: String,
},
OutputAudioChunk {
chunk: RealtimeAudioChunk,
},
OutputVideoChunk {
chunk: RealtimeVideoChunk,
},
Interrupted,
ToolCallRequested {
call_id: String,
tool_name: String,
},
ToolCallCompleted {
call_id: String,
},
ToolCallFailed {
call_id: String,
error: String,
},
ToolCallTimedOut {
call_id: String,
elapsed_ms: u64,
},
AssistantTranscriptTruncated {
item_id: String,
audio_played_ms: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
truncated_text: Option<String>,
},
StatusChanged {
status: RealtimeChannelStatus,
},
NeedsReattach,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeChannelOpenFrame {
pub protocol_version: RealtimeProtocolVersion,
pub open_token: String,
pub role: RealtimeChannelRole,
pub turning_mode: RealtimeTurningMode,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeChannelInputFrame {
pub chunk: RealtimeInputChunk,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeChannelOpenedFrame {
pub protocol_version: RealtimeProtocolVersion,
pub status: RealtimeChannelStatus,
pub capabilities: RealtimeCapabilities,
pub role: RealtimeChannelRole,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeChannelStatusFrame {
pub status: RealtimeChannelStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeChannelEventFrame {
pub event: RealtimeEvent,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum RealtimeErrorDetails {
AudioFormatMismatch(AudioFormatMismatchContext),
ToolCallTimeout(ToolCallTimeoutContext),
UnsupportedProtocolVersion {
requested: String,
supported: Vec<RealtimeProtocolVersion>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeChannelErrorFrame {
pub code: RealtimeErrorCode,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub details: Option<RealtimeErrorDetails>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RealtimeActionResult {
Completed,
NoOpPreemptive,
Failed(RealtimeChannelErrorFrame),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeChannelClosedFrame {
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RealtimeBargeInTruncateFrame {
pub item_id: String,
pub content_index: u32,
pub audio_played_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "type")]
pub enum RealtimeClientFrame {
#[serde(rename = "channel.open")]
ChannelOpen(RealtimeChannelOpenFrame),
#[serde(rename = "channel.input")]
ChannelInput(RealtimeChannelInputFrame),
#[serde(rename = "channel.commit_turn")]
ChannelCommitTurn,
#[serde(rename = "channel.interrupt")]
ChannelInterrupt,
#[serde(rename = "channel.barge_in_truncate")]
ChannelBargeInTruncate(RealtimeBargeInTruncateFrame),
#[serde(rename = "channel.close")]
ChannelClose,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "type")]
pub enum RealtimeServerFrame {
#[serde(rename = "channel.opened")]
ChannelOpened(RealtimeChannelOpenedFrame),
#[serde(rename = "channel.status")]
ChannelStatus(RealtimeChannelStatusFrame),
#[serde(rename = "channel.event")]
ChannelEvent(RealtimeChannelEventFrame),
#[serde(rename = "channel.error")]
ChannelError(RealtimeChannelErrorFrame),
#[serde(rename = "channel.closed")]
ChannelClosed(RealtimeChannelClosedFrame),
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
fn minimal_capabilities() -> RealtimeCapabilities {
RealtimeCapabilities {
input_kinds: vec![RealtimeInputKind::Text],
output_kinds: vec![RealtimeOutputKind::Text],
turning_modes: vec![RealtimeTurningMode::ProviderManaged],
interrupt_supported: true,
transcript_supported: true,
tool_lifecycle_events_supported: false,
video_supported: false,
audio_input_format: None,
audio_output_format: None,
}
}
fn minimal_status() -> RealtimeChannelStatus {
RealtimeChannelStatus {
state: RealtimeChannelState::Ready,
attempt_count: 0,
next_retry_at: None,
deadline_at: None,
reason: None,
}
}
fn target() -> RealtimeChannelTarget {
RealtimeChannelTarget::SessionTarget {
session_id: "session-typed-protocol".to_string(),
}
}
#[test]
fn unknown_protocol_version_string_fails_at_typed_open_boundary() {
let payload = serde_json::json!({
"type": "channel.open",
"protocol_version": "999",
"open_token": "token-1",
"role": "primary",
"turning_mode": "provider_managed"
});
let err = serde_json::from_value::<RealtimeClientFrame>(payload)
.expect_err("unknown protocol versions must not deserialize as semantic truth");
assert!(
err.to_string().contains("unknown variant")
|| err.to_string().contains("expected")
|| err.to_string().contains("matching variant"),
"unexpected serde error: {err}"
);
assert_eq!(RealtimeProtocolVersion::parse("999"), None);
}
#[test]
fn open_info_open_and_opened_round_trip_typed_protocol_version() {
let open_info = RealtimeOpenInfo {
ws_url: "ws://127.0.0.1:4900/realtime/ws".to_string(),
open_token: "token-1".to_string(),
expires_at: "2026-04-15T12:00:00Z".to_string(),
target: target(),
supported_protocol_versions: RealtimeProtocolVersion::SUPPORTED.to_vec(),
default_protocol_version: RealtimeProtocolVersion::CURRENT,
capabilities: minimal_capabilities(),
};
let open_info_json = serde_json::to_value(&open_info).expect("open-info serializes");
assert_eq!(open_info_json["default_protocol_version"], "2");
assert_eq!(
open_info_json["supported_protocol_versions"],
serde_json::json!(["2"])
);
let decoded_open_info: RealtimeOpenInfo =
serde_json::from_value(open_info_json).expect("open-info deserializes");
assert_eq!(
decoded_open_info.default_protocol_version,
RealtimeProtocolVersion::CURRENT
);
assert_eq!(
decoded_open_info.supported_protocol_versions,
vec![RealtimeProtocolVersion::CURRENT]
);
let open = RealtimeClientFrame::ChannelOpen(RealtimeChannelOpenFrame {
protocol_version: decoded_open_info.default_protocol_version,
open_token: decoded_open_info.open_token,
role: RealtimeChannelRole::Primary,
turning_mode: RealtimeTurningMode::ProviderManaged,
});
let open_json = serde_json::to_value(&open).expect("channel.open serializes");
assert_eq!(open_json["protocol_version"], "2");
let decoded_open: RealtimeClientFrame =
serde_json::from_value(open_json).expect("channel.open deserializes");
match decoded_open {
RealtimeClientFrame::ChannelOpen(frame) => {
assert_eq!(frame.protocol_version, RealtimeProtocolVersion::CURRENT);
}
other => panic!("expected channel.open, got {other:?}"),
}
let opened = RealtimeServerFrame::ChannelOpened(RealtimeChannelOpenedFrame {
protocol_version: RealtimeProtocolVersion::CURRENT,
status: minimal_status(),
capabilities: minimal_capabilities(),
role: RealtimeChannelRole::Primary,
});
let opened_json = serde_json::to_value(&opened).expect("channel.opened serializes");
assert_eq!(opened_json["protocol_version"], "2");
let decoded_opened: RealtimeServerFrame =
serde_json::from_value(opened_json).expect("channel.opened deserializes");
match decoded_opened {
RealtimeServerFrame::ChannelOpened(frame) => {
assert_eq!(frame.protocol_version, RealtimeProtocolVersion::CURRENT);
}
other => panic!("expected channel.opened, got {other:?}"),
}
}
#[test]
fn unsupported_protocol_error_keeps_raw_requested_string_diagnostic_only() {
let error = RealtimeErrorDetails::UnsupportedProtocolVersion {
requested: "999".to_string(),
supported: vec![RealtimeProtocolVersion::CURRENT],
};
let value = serde_json::to_value(&error).expect("error details serialize");
assert_eq!(value["requested"], "999");
assert_eq!(value["supported"], serde_json::json!(["2"]));
assert_eq!(RealtimeProtocolVersion::parse("999"), None);
}
}