use std::sync::Arc;
use std::sync::Mutex as StdMutex;
use std::sync::OnceLock;
use serde::{Deserialize, Serialize};
use serde_json::Value;
pub const RAW_PROTOCOL_VERSION: u32 = 5;
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct RawRequestEnvelope {
pub protocol_version: u32,
pub request_id: String,
#[serde(flatten)]
pub request: RawRequest,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(tag = "op", rename_all = "snake_case")]
pub enum RawRequest {
UserTurn {
text: String,
},
InsertSystemMessage {
text: String,
},
PromptResponse {
prompt_id: String,
response: String,
},
Interrupt,
ShowAgent,
ListAgents,
SwitchAgent {
agent: String,
},
Compact,
Clear,
SetSpend {
dollars: Option<f64>,
},
SaveTranscript {
path: String,
},
LoadTranscript {
path: String,
},
Stats,
ShowConfig,
Shutdown,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum RawServerMessage {
Hello(RawHello),
ReplayComplete(RawReplayComplete),
Request(RawAcceptedRequest),
Event(RawEventEnvelope),
Prompt(RawPrompt),
PromptAck(RawPromptAck),
Result(RawResultEnvelope),
}
impl RawServerMessage {
pub fn with_sequence(mut self, sequence: u64) -> Self {
match &mut self {
RawServerMessage::Hello(message) => message.sequence = sequence,
RawServerMessage::ReplayComplete(message) => message.sequence = sequence,
RawServerMessage::Request(message) => message.sequence = sequence,
RawServerMessage::Event(message) => message.sequence = sequence,
RawServerMessage::Prompt(message) => message.sequence = sequence,
RawServerMessage::PromptAck(message) => message.sequence = sequence,
RawServerMessage::Result(message) => message.sequence = sequence,
}
self
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct RawReplayComplete {
pub protocol_version: u32,
#[serde(default)]
pub sequence: u64,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct RawHello {
pub protocol_version: u32,
#[serde(default)]
pub sequence: u64,
pub session_id: String,
pub session_dir: String,
pub workspace_root: String,
pub current_agent: String,
pub model: String,
pub resumed: bool,
pub startup_confirmation_required: bool,
pub sandbox_available: bool,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct RawAcceptedRequest {
pub protocol_version: u32,
#[serde(default)]
pub sequence: u64,
pub request_id: String,
#[serde(flatten)]
pub request: RawRequest,
}
impl RawAcceptedRequest {
pub fn from_envelope(envelope: &RawRequestEnvelope) -> Option<Self> {
match &envelope.request {
RawRequest::UserTurn { .. } | RawRequest::InsertSystemMessage { .. } => Some(Self {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 0,
request_id: envelope.request_id.clone(),
request: envelope.request.clone(),
}),
_ => None,
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct RawEventEnvelope {
pub protocol_version: u32,
#[serde(default)]
pub sequence: u64,
pub request_id: String,
#[serde(flatten)]
pub event: RawEvent,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum RawEvent {
AgentStart {
label: Option<String>,
depth: usize,
},
AgentFinish {
label: Option<String>,
depth: usize,
stop_reason: Option<String>,
},
AssistantTextDelta {
label: Option<String>,
depth: usize,
text: String,
},
ThinkingDelta {
label: Option<String>,
depth: usize,
text: String,
},
Info {
label: Option<String>,
depth: usize,
message: String,
},
Error {
label: Option<String>,
depth: usize,
message: String,
},
ToolUseStart {
label: Option<String>,
depth: usize,
name: String,
tool_use_id: String,
},
ToolInputDelta {
label: Option<String>,
depth: usize,
partial_json: String,
},
ToolUseEnd {
label: Option<String>,
depth: usize,
},
ToolResultStart {
label: Option<String>,
depth: usize,
tool_use_id: String,
is_error: bool,
},
ToolResultTextDelta {
label: Option<String>,
depth: usize,
text: String,
},
ToolResultEnd {
label: Option<String>,
depth: usize,
},
ResponseFinish {
label: Option<String>,
depth: usize,
},
Interrupted {
label: Option<String>,
depth: usize,
},
ToolOutput {
tool_name: String,
tool_use_id: String,
stream: String,
#[serde(skip_serializing_if = "Option::is_none")]
text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
data_b64: Option<String>,
},
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct RawPrompt {
pub protocol_version: u32,
#[serde(default)]
pub sequence: u64,
pub request_id: String,
pub prompt_id: String,
pub kind: String,
pub message: String,
pub choices: Vec<String>,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct RawPromptAck {
pub protocol_version: u32,
#[serde(default)]
pub sequence: u64,
pub request_id: String,
pub response_request_id: String,
pub prompt_id: String,
pub response: String,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct RawResultEnvelope {
pub protocol_version: u32,
#[serde(default)]
pub sequence: u64,
pub request_id: String,
pub ok: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<RawServerError>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RawServerError {
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
pub message: String,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ToolOutputEvent {
pub request_id: String,
pub tool_name: String,
pub tool_use_id: String,
pub stream: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_b64: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct UsageReportEvent {
pub token_line: String,
pub usage_line: String,
}
pub trait ToolOutputObserver: Send + Sync {
fn on_tool_output(&self, event: &ToolOutputEvent);
}
pub trait UsageReportObserver: Send + Sync {
fn on_usage_report(&self, event: &UsageReportEvent);
}
pub struct ToolOutputObserverRegistration {
previous: Option<Arc<dyn ToolOutputObserver>>,
}
pub struct UsageReportObserverRegistration {
previous: Option<Arc<dyn UsageReportObserver>>,
}
impl Drop for ToolOutputObserverRegistration {
fn drop(&mut self) {
set_active_tool_output_observer(self.previous.take());
}
}
impl Drop for UsageReportObserverRegistration {
fn drop(&mut self) {
set_active_usage_report_observer(self.previous.take());
}
}
pub fn install_tool_output_observer(
observer: Option<Arc<dyn ToolOutputObserver>>,
) -> ToolOutputObserverRegistration {
let previous = set_active_tool_output_observer(observer);
ToolOutputObserverRegistration { previous }
}
pub fn install_usage_report_observer(
observer: Option<Arc<dyn UsageReportObserver>>,
) -> UsageReportObserverRegistration {
let previous = set_active_usage_report_observer(observer);
UsageReportObserverRegistration { previous }
}
pub(crate) fn notify_tool_output_observer(event: &ToolOutputEvent) {
let observer = active_tool_output_observer()
.lock()
.expect("tool output observer lock poisoned")
.clone();
if let Some(observer) = observer {
observer.on_tool_output(event);
}
}
pub(crate) fn notify_usage_report_observer(event: &UsageReportEvent) -> bool {
let observer = active_usage_report_observer()
.lock()
.expect("usage report observer lock poisoned")
.clone();
if let Some(observer) = observer {
observer.on_usage_report(event);
true
} else {
false
}
}
pub(crate) fn has_active_tool_output_observer() -> bool {
active_tool_output_observer()
.lock()
.expect("tool output observer lock poisoned")
.is_some()
}
fn active_tool_output_observer() -> &'static StdMutex<Option<Arc<dyn ToolOutputObserver>>> {
static ACTIVE: OnceLock<StdMutex<Option<Arc<dyn ToolOutputObserver>>>> = OnceLock::new();
ACTIVE.get_or_init(|| StdMutex::new(None))
}
fn active_usage_report_observer() -> &'static StdMutex<Option<Arc<dyn UsageReportObserver>>> {
static ACTIVE: OnceLock<StdMutex<Option<Arc<dyn UsageReportObserver>>>> = OnceLock::new();
ACTIVE.get_or_init(|| StdMutex::new(None))
}
fn set_active_tool_output_observer(
observer: Option<Arc<dyn ToolOutputObserver>>,
) -> Option<Arc<dyn ToolOutputObserver>> {
let mut slot = active_tool_output_observer()
.lock()
.expect("tool output observer lock poisoned");
std::mem::replace(&mut *slot, observer)
}
fn set_active_usage_report_observer(
observer: Option<Arc<dyn UsageReportObserver>>,
) -> Option<Arc<dyn UsageReportObserver>> {
let mut slot = active_usage_report_observer()
.lock()
.expect("usage report observer lock poisoned");
std::mem::replace(&mut *slot, observer)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_envelope_user_turn_roundtrip() {
let envelope = RawRequestEnvelope {
protocol_version: RAW_PROTOCOL_VERSION,
request_id: "r-1".to_string(),
request: RawRequest::UserTurn {
text: "hello".to_string(),
},
};
let json = serde_json::to_string(&envelope).unwrap();
let decoded: RawRequestEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, envelope);
}
#[test]
fn request_envelope_prompt_response_roundtrip() {
let envelope = RawRequestEnvelope {
protocol_version: RAW_PROTOCOL_VERSION,
request_id: "r-2".to_string(),
request: RawRequest::PromptResponse {
prompt_id: "prompt-1".to_string(),
response: "yes".to_string(),
},
};
let json = serde_json::to_string(&envelope).unwrap();
let decoded: RawRequestEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, envelope);
}
#[test]
fn request_envelope_shutdown_roundtrip() {
let envelope = RawRequestEnvelope {
protocol_version: RAW_PROTOCOL_VERSION,
request_id: "r-3".to_string(),
request: RawRequest::Shutdown,
};
let json = serde_json::to_string(&envelope).unwrap();
let decoded: RawRequestEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, envelope);
}
#[test]
fn request_tag_uses_snake_case() {
let envelope = RawRequestEnvelope {
protocol_version: RAW_PROTOCOL_VERSION,
request_id: "r-7".to_string(),
request: RawRequest::ShowAgent,
};
let json = serde_json::to_string(&envelope).unwrap();
assert!(json.contains(r#""op":"show_agent""#), "json was: {json}");
}
#[test]
fn request_save_transcript_roundtrip() {
let envelope = RawRequestEnvelope {
protocol_version: RAW_PROTOCOL_VERSION,
request_id: "r-8".to_string(),
request: RawRequest::SaveTranscript {
path: "/tmp/out.json".to_string(),
},
};
let json = serde_json::to_string(&envelope).unwrap();
let decoded: RawRequestEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, envelope);
}
#[test]
fn request_set_spend_roundtrip() {
let envelope = RawRequestEnvelope {
protocol_version: RAW_PROTOCOL_VERSION,
request_id: "r-9".to_string(),
request: RawRequest::SetSpend { dollars: Some(5.0) },
};
let json = serde_json::to_string(&envelope).unwrap();
let decoded: RawRequestEnvelope = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, envelope);
}
#[test]
fn server_hello_roundtrip() {
let msg = RawServerMessage::Hello(RawHello {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 1,
session_id: "sess-1".to_string(),
session_dir: "/tmp/sess".to_string(),
workspace_root: "/workspace".to_string(),
current_agent: "default".to_string(),
model: "sonnet-4".to_string(),
resumed: false,
startup_confirmation_required: false,
sandbox_available: true,
});
let json = serde_json::to_string(&msg).unwrap();
let decoded: RawServerMessage = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, msg);
}
#[test]
fn server_replay_complete_roundtrip() {
let msg = RawServerMessage::ReplayComplete(RawReplayComplete {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 9,
});
let json = serde_json::to_string(&msg).unwrap();
let decoded: RawServerMessage = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, msg);
}
#[test]
fn server_request_user_turn_roundtrip() {
let msg = RawServerMessage::Request(RawAcceptedRequest {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 4,
request_id: "r-0".to_string(),
request: RawRequest::UserTurn {
text: "hello".to_string(),
},
});
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains(r#""type":"request""#), "got: {json}");
assert!(json.contains(r#""op":"user_turn""#), "got: {json}");
let decoded: RawServerMessage = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, msg);
}
#[test]
fn server_result_ok_roundtrip() {
let msg = RawServerMessage::Result(RawResultEnvelope {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 5,
request_id: "r-1".to_string(),
ok: true,
data: Some(serde_json::json!({"agent": "default"})),
error: None,
});
let json = serde_json::to_string(&msg).unwrap();
let decoded: RawServerMessage = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, msg);
}
#[test]
fn server_result_error_roundtrip() {
let msg = RawServerMessage::Result(RawResultEnvelope {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 6,
request_id: "r-2".to_string(),
ok: false,
data: None,
error: Some(RawServerError {
code: Some("busy".to_string()),
message: "server is busy".to_string(),
}),
});
let json = serde_json::to_string(&msg).unwrap();
let decoded: RawServerMessage = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, msg);
}
#[test]
fn server_prompt_roundtrip() {
let msg = RawServerMessage::Prompt(RawPrompt {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 7,
request_id: "r-3".to_string(),
prompt_id: "prompt-1".to_string(),
kind: "confirmation".to_string(),
message: "Continue?".to_string(),
choices: vec!["yes".to_string(), "no".to_string()],
});
let json = serde_json::to_string(&msg).unwrap();
let decoded: RawServerMessage = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, msg);
}
#[test]
fn server_prompt_ack_roundtrip() {
let msg = RawServerMessage::PromptAck(RawPromptAck {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 8,
request_id: "r-3".to_string(),
response_request_id: "r-4".to_string(),
prompt_id: "prompt-1".to_string(),
response: "yes".to_string(),
});
let json = serde_json::to_string(&msg).unwrap();
let decoded: RawServerMessage = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, msg);
}
#[test]
fn server_event_assistant_text_delta_roundtrip() {
let msg = RawServerMessage::Event(RawEventEnvelope {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 10,
request_id: "r-5".to_string(),
event: RawEvent::AssistantTextDelta {
label: Some("main".to_string()),
depth: 0,
text: "Hello world".to_string(),
},
});
let json = serde_json::to_string(&msg).unwrap();
let decoded: RawServerMessage = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, msg);
}
#[test]
fn server_event_tool_use_start_roundtrip() {
let msg = RawServerMessage::Event(RawEventEnvelope {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 11,
request_id: "r-6".to_string(),
event: RawEvent::ToolUseStart {
label: None,
depth: 1,
name: "bash".to_string(),
tool_use_id: "tu-1".to_string(),
},
});
let json = serde_json::to_string(&msg).unwrap();
let decoded: RawServerMessage = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, msg);
}
#[test]
fn server_event_tool_output_omits_none_fields() {
let msg = RawServerMessage::Event(RawEventEnvelope {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 12,
request_id: "r-7".to_string(),
event: RawEvent::ToolOutput {
tool_name: "bash".to_string(),
tool_use_id: "tu-1".to_string(),
stream: "stdout".to_string(),
text: Some("output".to_string()),
data_b64: None,
},
});
let json = serde_json::to_string(&msg).unwrap();
assert!(!json.contains("data_b64"), "None field should be omitted");
let decoded: RawServerMessage = serde_json::from_str(&json).unwrap();
assert_eq!(decoded, msg);
}
#[test]
fn server_result_omits_none_error_and_data() {
let msg = RawServerMessage::Result(RawResultEnvelope {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 13,
request_id: "r-8".to_string(),
ok: true,
data: None,
error: None,
});
let json = serde_json::to_string(&msg).unwrap();
assert!(!json.contains("data"), "None data should be omitted");
assert!(!json.contains("error"), "None error should be omitted");
}
#[test]
fn with_sequence_stamps_hello() {
let msg = RawServerMessage::Hello(RawHello {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 0,
session_id: "s".to_string(),
session_dir: "/d".to_string(),
workspace_root: "/w".to_string(),
current_agent: "a".to_string(),
model: "m".to_string(),
resumed: false,
startup_confirmation_required: false,
sandbox_available: false,
});
let stamped = msg.with_sequence(42);
match stamped {
RawServerMessage::Hello(h) => assert_eq!(h.sequence, 42),
_ => panic!("expected Hello"),
}
}
#[test]
fn with_sequence_stamps_event() {
let msg = RawServerMessage::Event(RawEventEnvelope {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 0,
request_id: "r".to_string(),
event: RawEvent::Info {
label: None,
depth: 0,
message: "info".to_string(),
},
});
let stamped = msg.with_sequence(99);
match stamped {
RawServerMessage::Event(e) => assert_eq!(e.sequence, 99),
_ => panic!("expected Event"),
}
}
#[test]
fn with_sequence_stamps_request() {
let msg = RawServerMessage::Request(RawAcceptedRequest {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 0,
request_id: "r".to_string(),
request: RawRequest::UserTurn {
text: "hello".to_string(),
},
});
let stamped = msg.with_sequence(6);
match stamped {
RawServerMessage::Request(r) => assert_eq!(r.sequence, 6),
_ => panic!("expected Request"),
}
}
#[test]
fn with_sequence_stamps_replay_complete() {
let msg = RawServerMessage::ReplayComplete(RawReplayComplete {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 0,
});
let stamped = msg.with_sequence(4);
match stamped {
RawServerMessage::ReplayComplete(replay) => assert_eq!(replay.sequence, 4),
_ => panic!("expected ReplayComplete"),
}
}
#[test]
fn with_sequence_stamps_prompt() {
let msg = RawServerMessage::Prompt(RawPrompt {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 0,
request_id: "r".to_string(),
prompt_id: "p".to_string(),
kind: "confirmation".to_string(),
message: "ok?".to_string(),
choices: vec![],
});
let stamped = msg.with_sequence(7);
match stamped {
RawServerMessage::Prompt(p) => assert_eq!(p.sequence, 7),
_ => panic!("expected Prompt"),
}
}
#[test]
fn with_sequence_stamps_prompt_ack() {
let msg = RawServerMessage::PromptAck(RawPromptAck {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 0,
request_id: "r".to_string(),
response_request_id: "rr".to_string(),
prompt_id: "p".to_string(),
response: "yes".to_string(),
});
let stamped = msg.with_sequence(8);
match stamped {
RawServerMessage::PromptAck(p) => assert_eq!(p.sequence, 8),
_ => panic!("expected PromptAck"),
}
}
#[test]
fn with_sequence_stamps_result() {
let msg = RawServerMessage::Result(RawResultEnvelope {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 0,
request_id: "r".to_string(),
ok: true,
data: None,
error: None,
});
let stamped = msg.with_sequence(3);
match stamped {
RawServerMessage::Result(r) => assert_eq!(r.sequence, 3),
_ => panic!("expected Result"),
}
}
#[test]
fn server_message_type_tags_are_snake_case() {
let hello = serde_json::to_string(&RawServerMessage::Hello(RawHello {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 0,
session_id: "s".to_string(),
session_dir: "/d".to_string(),
workspace_root: "/w".to_string(),
current_agent: "a".to_string(),
model: "m".to_string(),
resumed: false,
startup_confirmation_required: false,
sandbox_available: false,
}))
.unwrap();
assert!(hello.contains(r#""type":"hello""#), "got: {hello}");
let replay_complete =
serde_json::to_string(&RawServerMessage::ReplayComplete(RawReplayComplete {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 0,
}))
.unwrap();
assert!(
replay_complete.contains(r#""type":"replay_complete""#),
"got: {replay_complete}"
);
let request = serde_json::to_string(&RawServerMessage::Request(RawAcceptedRequest {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 0,
request_id: "r".to_string(),
request: RawRequest::UserTurn {
text: "hello".to_string(),
},
}))
.unwrap();
assert!(request.contains(r#""type":"request""#), "got: {request}");
let prompt_ack = serde_json::to_string(&RawServerMessage::PromptAck(RawPromptAck {
protocol_version: RAW_PROTOCOL_VERSION,
sequence: 0,
request_id: "r".to_string(),
response_request_id: "rr".to_string(),
prompt_id: "p".to_string(),
response: "yes".to_string(),
}))
.unwrap();
assert!(
prompt_ack.contains(r#""type":"prompt_ack""#),
"got: {prompt_ack}"
);
}
#[test]
fn event_variant_tags_are_snake_case() {
let event = serde_json::to_string(&RawEvent::AgentStart {
label: None,
depth: 0,
})
.unwrap();
assert!(event.contains(r#""event":"agent_start""#), "got: {event}");
let event = serde_json::to_string(&RawEvent::ToolResultTextDelta {
label: None,
depth: 0,
text: "t".to_string(),
})
.unwrap();
assert!(
event.contains(r#""event":"tool_result_text_delta""#),
"got: {event}"
);
}
#[test]
fn hello_sequence_defaults_to_zero_when_absent() {
let json = r#"{
"type": "hello",
"protocol_version": 2,
"session_id": "s",
"session_dir": "/d",
"workspace_root": "/w",
"current_agent": "a",
"model": "m",
"resumed": false,
"startup_confirmation_required": false,
"sandbox_available": false
}"#;
let msg: RawServerMessage = serde_json::from_str(json).unwrap();
match msg {
RawServerMessage::Hello(h) => assert_eq!(h.sequence, 0),
_ => panic!("expected Hello"),
}
}
#[test]
fn replay_complete_sequence_defaults_to_zero_when_absent() {
let json = r#"{
"type": "replay_complete",
"protocol_version": 2
}"#;
let msg: RawServerMessage = serde_json::from_str(json).unwrap();
match msg {
RawServerMessage::ReplayComplete(replay) => assert_eq!(replay.sequence, 0),
_ => panic!("expected ReplayComplete"),
}
}
#[test]
fn request_sequence_defaults_to_zero_when_absent() {
let json = r#"{
"type": "request",
"protocol_version": 2,
"request_id": "r",
"op": "user_turn",
"text": "hello"
}"#;
let msg: RawServerMessage = serde_json::from_str(json).unwrap();
match msg {
RawServerMessage::Request(request) => assert_eq!(request.sequence, 0),
_ => panic!("expected Request"),
}
}
#[test]
fn accepted_request_from_envelope_keeps_user_turn_text() {
let envelope = RawRequestEnvelope {
protocol_version: RAW_PROTOCOL_VERSION,
request_id: "turn-1".to_string(),
request: RawRequest::UserTurn {
text: "what did I ask?".to_string(),
},
};
let request = RawAcceptedRequest::from_envelope(&envelope).unwrap();
assert_eq!(request.request_id, "turn-1");
assert_eq!(
request.request,
RawRequest::UserTurn {
text: "what did I ask?".to_string(),
}
);
}
#[test]
fn accepted_request_from_envelope_keeps_inserted_system_message_text() {
let envelope = RawRequestEnvelope {
protocol_version: RAW_PROTOCOL_VERSION,
request_id: "system-1".to_string(),
request: RawRequest::InsertSystemMessage {
text: "pin this instruction".to_string(),
},
};
let request = RawAcceptedRequest::from_envelope(&envelope).unwrap();
assert_eq!(request.request_id, "system-1");
assert_eq!(
request.request,
RawRequest::InsertSystemMessage {
text: "pin this instruction".to_string(),
}
);
}
#[test]
fn accepted_request_from_envelope_skips_non_transcript_requests() {
let envelope = RawRequestEnvelope {
protocol_version: RAW_PROTOCOL_VERSION,
request_id: "stats".to_string(),
request: RawRequest::Stats,
};
assert!(RawAcceptedRequest::from_envelope(&envelope).is_none());
}
}