//! Tests for the channel messages and the usage record, against byte-exact fixtures. Do not edit. use proto::{ CallId, DataClass, DenyReason, Envelope, Epoch, ErrorCode, Hash32, LogRecord, Message, SessionId, Timestamp, ToolCall, Turn, TurnDone, TurnEvent, Usage, WireError, }; fn fixture(kind: &str, name: &str) -> String { let path = format!( "{}/tests/fixtures/{kind}/{name}", env!("CARGO_MANIFEST_DIR") ); std::fs::read_to_string(&path) .unwrap_or_else(|e| panic!("{path}: {e}")) .trim_end_matches('\n') .to_string() } fn check(name: &str, want: Envelope) { let text = fixture("wire", name); let got: Envelope = serde_json::from_str(&text).unwrap_or_else(|e| panic!("{name}: {e}")); assert_eq!(got, want, "{name}: decoded value"); assert_eq!( serde_json::to_string(&want).unwrap(), text, "{name}: encoded bytes" ); } fn env(id: u64, r#final: bool, msg: Message) -> Envelope { Envelope { v: 1, id, r#final, msg, } } fn usage() -> Usage { Usage { cache_n: 539, prompt_n: 27, predicted_n: 24, reasoning_tokens: 8, thinking_capped: false, } } #[test] fn turn() { let body = Turn { session: SessionId::new("chat-1789700000-42").unwrap(), content: "What time is it?".to_string(), resume: false, }; check("turn.json", env(3, true, Message::Turn(body))); } #[test] fn turn_events() { check( "turn_event_tool_result.json", env( 3, false, Message::TurnEvent(TurnEvent::ToolResult { name: "clock".to_string(), class: DataClass::Public, truncated: false, }), ), ); check( "turn_event_content.json", env( 3, false, Message::TurnEvent(TurnEvent::Content { text: "It is ".to_string(), }), ), ); check( "turn_event_retrying.json", env( 3, false, Message::TurnEvent(TurnEvent::Retrying { attempt: 2, after_ms: 1500, error: "the server went silent".to_string(), }), ), ); check( "turn_event_approval_pending.json", env( 3, false, Message::TurnEvent(TurnEvent::ApprovalPending { approval: 41, tool: "shell".to_string(), expires: Timestamp::parse("2026-09-18T08:20:00.000Z").unwrap(), }), ), ); check( "turn_event_tool_denied.json", env( 3, false, Message::TurnEvent(TurnEvent::ToolDenied { name: "read_file".to_string(), reason: DenyReason::NoGrant, }), ), ); } #[test] fn turn_done_and_the_new_error_codes() { check( "turn_done.json", env( 3, true, Message::TurnDone(TurnDone { content: "It is noon.".to_string(), usage: usage(), }), ), ); let body = WireError { code: ErrorCode::SessionFull, detail: "this conversation is full; start a new one".to_string(), }; check( "error_session_full.json", env(3, true, Message::Error(body)), ); let codes = [ (ErrorCode::SessionFull, "session_full"), (ErrorCode::TurnLimit, "turn_limit"), (ErrorCode::SessionBusy, "session_busy"), (ErrorCode::NoSuchSession, "no_such_session"), (ErrorCode::SessionExists, "session_exists"), (ErrorCode::Inference, "inference"), ]; for (value, text) in codes { assert_eq!( serde_json::to_string(&value).unwrap(), format!("\"{text}\"") ); } } #[test] fn every_turn_event_kind_round_trips() { let all = vec![ TurnEvent::Queued { ahead: 1 }, TurnEvent::Waiting { slot_busy: true }, TurnEvent::Progress { total: 100, cache: 50, processed: 75, }, TurnEvent::Reasoning { text: "hm".to_string(), }, TurnEvent::Content { text: "hi".to_string(), }, TurnEvent::ToolCallStarted { name: "clock".to_string(), }, TurnEvent::ToolResult { name: "clock".to_string(), class: DataClass::Secret, truncated: true, }, TurnEvent::ThinkingCapped { tokens: 4096 }, TurnEvent::Retrying { attempt: 2, after_ms: 10, error: "x".to_string(), }, TurnEvent::CacheLoss { expected: 500, got: 20, }, TurnEvent::ApprovalPending { approval: u64::MAX, tool: "http_fetch".to_string(), expires: Timestamp::parse("2026-09-18T08:20:00.000Z").unwrap(), }, TurnEvent::ToolDenied { name: "shell".to_string(), reason: DenyReason::StateUnreadable, }, ]; for event in all { let text = serde_json::to_string(&event).unwrap(); assert!(text.starts_with("{\"event\":\""), "{text}"); assert_eq!(serde_json::from_str::(&text).unwrap(), event); } assert!(serde_json::from_str::(r#"{"event":"content","text":"x","zz":1}"#).is_err()); assert!(serde_json::from_str::(r#"{"event":"dance"}"#).is_err()); } #[test] fn usage_records() { let text = fixture("records", "session_usage.jsonl"); let lines: Vec<&str> = text.lines().collect(); let want = [ LogRecord::SessionStart { time: Timestamp::parse("2026-09-18T08:05:00.000Z").unwrap(), session: SessionId::new("chat-1789700000-42").unwrap(), epoch: Epoch(0), slot: 0, baseline: Hash32::from_hex( "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", ) .unwrap(), }, LogRecord::User { time: Timestamp::parse("2026-09-18T08:05:01.000Z").unwrap(), content: "What time is it?".to_string(), }, LogRecord::Assistant { time: Timestamp::parse("2026-09-18T08:05:03.000Z").unwrap(), content: None, reasoning_content: None, tool_calls: vec![ToolCall { id: "call_a1".to_string(), name: "clock".to_string(), arguments: "{}".to_string(), }], }, LogRecord::Usage { time: Timestamp::parse("2026-09-18T08:05:03.000Z").unwrap(), cache_n: 539, prompt_n: 27, predicted_n: 24, reasoning_tokens: 8, thinking_capped: false, }, LogRecord::ToolResult { time: Timestamp::parse("2026-09-18T08:05:03.100Z").unwrap(), call: CallId(1), tool_call_id: "call_a1".to_string(), content: "2026-09-18T08:05:03.000Z".to_string(), class: DataClass::Public, untrusted: false, truncated: false, }, LogRecord::Assistant { time: Timestamp::parse("2026-09-18T08:05:05.000Z").unwrap(), content: Some("It is five past eight.".to_string()), reasoning_content: None, tool_calls: vec![], }, LogRecord::Usage { time: Timestamp::parse("2026-09-18T08:05:05.000Z").unwrap(), cache_n: 589, prompt_n: 40, predicted_n: 64, reasoning_tokens: 27, thinking_capped: true, }, ]; assert_eq!(lines.len(), want.len()); for (i, (line, want)) in lines.iter().zip(&want).enumerate() { let got: LogRecord = serde_json::from_str(line).unwrap_or_else(|e| panic!("line {}: {e}", i + 1)); assert_eq!(&got, want, "line {}", i + 1); assert_eq!( &serde_json::to_string(want).unwrap(), line, "line {}: bytes", i + 1 ); } }