1#![deny(
4 clippy::unwrap_used,
5 clippy::expect_used,
6 clippy::panic,
7 clippy::unreachable,
8 clippy::todo,
9 clippy::unimplemented,
10 clippy::cast_possible_truncation,
11 clippy::cast_sign_loss,
12 clippy::cast_possible_wrap,
13 clippy::as_conversions,
14 clippy::arithmetic_side_effects,
15 clippy::integer_division,
16 clippy::modulo_arithmetic,
17 clippy::float_arithmetic,
18 clippy::allow_attributes,
19 clippy::allow_attributes_without_reason,
20 clippy::unwrap_in_result,
21 clippy::panic_in_result_fn,
22 clippy::let_underscore_must_use,
23 clippy::clone_on_ref_ptr,
24 clippy::dbg_macro,
25 clippy::exit,
26 clippy::indexing_slicing,
27 clippy::string_slice,
28 clippy::str_to_string,
29 clippy::mem_forget,
30 clippy::match_wildcard_for_single_variants,
31 clippy::wildcard_enum_match_arm,
32 clippy::wildcard_imports,
33 clippy::unseparated_literal_suffix,
34 clippy::single_char_lifetime_names,
35 clippy::undocumented_unsafe_blocks,
36 clippy::multiple_unsafe_ops_per_block,
37 clippy::missing_assert_message,
38 clippy::shadow_same,
39 clippy::shadow_reuse,
40 clippy::shadow_unrelated,
41 clippy::else_if_without_else,
42 clippy::impl_trait_in_params,
43 unsafe_code,
44 elided_lifetimes_in_paths,
45 unused_qualifications
46)]
47#![deny(
48 clippy::print_stdout,
49 clippy::print_stderr,
50 missing_debug_implementations,
51 unreachable_pub
52)]
53
54pub mod client_msg;
55pub mod error;
56pub mod merge;
57pub mod server_msg;
58pub mod tool_display;
59pub mod types;
60
61pub const SWP_V1: u32 = 1;
63
64pub const MAX_WIRE_MESSAGE_SIZE: usize = 128 * 1024 * 1024;
74
75#[cfg(test)]
76mod tests {
77 use serde_json::json;
78
79 use crate::client_msg::*;
80 use crate::error::*;
81 use crate::server_msg::*;
82 use crate::types::*;
83 use crate::{MAX_WIRE_MESSAGE_SIZE, SWP_V1};
84
85 fn round_trip<T: serde::Serialize + serde::de::DeserializeOwned + std::fmt::Debug>(
87 val: &T,
88 ) -> (serde_json::Value, T) {
89 let json = serde_json::to_value(val).expect("serialize");
90 let back: T = serde_json::from_value(json.clone()).expect("deserialize");
91 (json, back)
92 }
93
94 fn field<'val>(value: &'val serde_json::Value, key: &str) -> &'val serde_json::Value {
95 value.get(key).expect("expected JSON field")
96 }
97
98 fn item<T>(items: &[T], index: usize) -> &T {
99 items.get(index).expect("expected item")
100 }
101
102 #[test]
105 fn protocol_version_constant() {
106 assert_eq!(SWP_V1, 1);
107 }
108
109 #[test]
110 fn wire_message_size_constant() {
111 assert_eq!(MAX_WIRE_MESSAGE_SIZE, 128 * 1024 * 1024);
112 }
113
114 #[test]
117 fn client_hello_round_trip() {
118 let msg = ClientMessage::Hello(ClientHello {
119 client_type: "tui".into(),
120 client_name: "shore-tui".into(),
121 capabilities: vec!["streaming".into()],
122 character: None,
123 });
124 let (json, _back) = round_trip(&msg);
125 assert_eq!(field(&json, "type"), "hello");
126 assert_eq!(field(&json, "client_type"), "tui");
127 }
128
129 #[test]
130 fn client_message_round_trip() {
131 let msg = ClientMessage::Message(ClientMessageBody {
132 rid: Some("msg_01".into()),
133 text: "Hello world".into(),
134 stream: true,
135 images: vec![],
136 image_data: vec![],
137 absence_seconds: None,
138 overrides: None,
139 });
140 let (json, _back) = round_trip(&msg);
141 assert_eq!(field(&json, "type"), "message");
142 assert_eq!(field(&json, "text"), "Hello world");
143 assert_eq!(field(&json, "stream"), true);
144 }
145
146 #[test]
147 fn client_regen_round_trip() {
148 let msg = ClientMessage::Regen(Regen {
149 rid: Some("regen_01".into()),
150 stream: true,
151 guidance: None,
152 });
153 let (json, _back) = round_trip(&msg);
154 assert_eq!(field(&json, "type"), "regen");
155 }
156
157 #[test]
158 fn client_command_round_trip() {
159 let msg = ClientMessage::Command(Command {
160 rid: Some("cmd_01".into()),
161 name: "switch_character".into(),
162 args: json!({"name": "alice"}),
163 });
164 let (json, _back) = round_trip(&msg);
165 assert_eq!(field(&json, "type"), "command");
166 assert_eq!(field(&json, "name"), "switch_character");
167 assert_eq!(field(field(&json, "args"), "name"), "alice");
168 }
169
170 #[test]
173 fn server_hello_round_trip() {
174 let msg = ServerMessage::Hello(ServerHello {
175 v: SWP_V1,
176 server_name: "shore-daemon".into(),
177 characters: vec![CharacterInfo::new("alice")],
178 });
179 let (json, _back) = round_trip(&msg);
180 assert_eq!(field(&json, "type"), "hello");
181 assert_eq!(field(&json, "v"), 1);
182 }
183
184 #[test]
185 fn server_history_round_trip() {
186 let msg = ServerMessage::History(History {
187 rid: None,
188 messages: vec![Message {
189 msg_id: "m1".into(),
190 origin: None,
191 role: Role::User,
192 content: "hi".into(),
193 images: vec![],
194 content_blocks: vec![],
195 alt_index: None,
196 alt_count: None,
197 alternatives: vec![],
198 provider_key: None,
199 model: None,
200 timestamp: "2026-01-01T00:00:00Z".into(),
201 }],
202 active_start: 0,
203 config: json!({}),
204 selected_character: Some("alice".into()),
205 revision: 7,
206 });
207 let (json, _back) = round_trip(&msg);
208 assert_eq!(field(&json, "type"), "history");
209 let messages = field(&json, "messages").as_array().expect("messages array");
210 assert_eq!(field(item(messages, 0), "role"), "user");
211 assert_eq!(field(&json, "selected_character"), "alice");
212 assert_eq!(field(&json, "revision"), 7);
213 }
214
215 #[test]
216 fn server_request_history_round_trip() {
217 let msg = ServerMessage::History(History {
218 rid: Some("cmd_switch_01".into()),
219 messages: vec![],
220 active_start: 0,
221 config: json!({}),
222 selected_character: Some("alice".into()),
223 revision: 8,
224 });
225 let (json, _back) = round_trip(&msg);
226 assert_eq!(field(&json, "type"), "history");
227 assert_eq!(field(&json, "rid"), "cmd_switch_01");
228 assert_eq!(field(&json, "revision"), 8);
229 }
230
231 #[test]
232 fn server_shutdown_round_trip() {
233 let msg = ServerMessage::Shutdown(Shutdown {});
234 let (json, _back) = round_trip(&msg);
235 assert_eq!(field(&json, "type"), "shutdown");
236 }
237
238 #[test]
239 fn server_ping_round_trip() {
240 let msg = ServerMessage::Ping(Ping {});
241 let (json, _back) = round_trip(&msg);
242 assert_eq!(field(&json, "type"), "ping");
243 }
244
245 #[test]
246 fn server_command_output_round_trip() {
247 let msg = ServerMessage::CommandOutput(CommandOutput {
248 rid: Some("cmd_01".into()),
249 name: "status".into(),
250 data: json!({"ok": true}),
251 });
252 let (json, _back) = round_trip(&msg);
253 assert_eq!(field(&json, "type"), "command_output");
254 assert_eq!(field(&json, "rid"), "cmd_01");
255 assert_eq!(field(&json, "name"), "status");
256 }
257
258 #[test]
259 fn server_error_round_trip() {
260 let msg = ServerMessage::Error(Error {
261 rid: Some("msg_01".into()),
262 code: ErrorCode::Busy,
263 message: "engine busy".into(),
264 });
265 let (json, _back) = round_trip(&msg);
266 assert_eq!(field(&json, "type"), "error");
267 assert_eq!(field(&json, "rid"), "msg_01");
268 assert_eq!(field(&json, "code"), "busy");
269 }
270
271 #[test]
272 fn server_stream_start_round_trip() {
273 let msg = ServerMessage::StreamStart(StreamStart {
274 subagent: None,
275 rid: Some("msg_01".into()),
276 regen: false,
277 });
278 let (json, _back) = round_trip(&msg);
279 assert_eq!(field(&json, "type"), "stream_start");
280 assert_eq!(field(&json, "rid"), "msg_01");
281 assert_eq!(field(&json, "regen"), false);
282 }
283
284 #[test]
285 fn server_stream_chunk_round_trip() {
286 let msg = ServerMessage::StreamChunk(StreamChunk {
287 subagent: None,
288 rid: Some("msg_01".into()),
289 text: "partial".into(),
290 content_type: "text".into(),
291 });
292 let (json, _back) = round_trip(&msg);
293 assert_eq!(field(&json, "type"), "stream_chunk");
294 assert_eq!(field(&json, "rid"), "msg_01");
295 assert_eq!(field(&json, "content_type"), "text");
296 }
297
298 #[test]
299 fn server_stream_chunk_thinking() {
300 let msg = ServerMessage::StreamChunk(StreamChunk {
301 subagent: None,
302 rid: Some("msg_01".into()),
303 text: "hmm...".into(),
304 content_type: "thinking".into(),
305 });
306 let (json, _back) = round_trip(&msg);
307 assert_eq!(field(&json, "rid"), "msg_01");
308 assert_eq!(field(&json, "content_type"), "thinking");
309 }
310
311 #[test]
312 fn server_stream_end_round_trip() {
313 let msg = ServerMessage::StreamEnd(StreamEnd {
314 subagent: None,
315 rid: Some("msg_01".into()),
316 msg_id: None,
317 revision: None,
318 content: "full response".into(),
319 metadata: StreamMetadata {
320 tokens: TokenCounts {
321 input: 1234,
322 output: 567,
323 cache_read: 890,
324 cache_write: 0,
325 },
326 timing: TimingInfo {
327 total_ms: 2340,
328 ttft_ms: 450,
329 },
330 model: "claude-haiku-4-5-20251001".into(),
331 },
332 finish_reason: "end_turn".into(),
333 is_final: true,
334 });
335 let (json, _back) = round_trip(&msg);
336 assert_eq!(field(&json, "type"), "stream_end");
337 assert_eq!(field(&json, "rid"), "msg_01");
338 assert!(json.get("msg_id").is_none());
339 assert!(json.get("revision").is_none());
340 let metadata = field(&json, "metadata");
341 let tokens = field(metadata, "tokens");
342 let timing = field(metadata, "timing");
343 assert_eq!(field(tokens, "input"), 1234);
344 assert_eq!(field(tokens, "cache_read"), 890);
345 assert_eq!(field(timing, "total_ms"), 2340);
346 assert_eq!(field(timing, "ttft_ms"), 450);
347 assert_eq!(field(metadata, "model"), "claude-haiku-4-5-20251001");
348 }
349
350 #[test]
351 fn server_phase_round_trip() {
352 for phase_val in &["thinking", "text_generation", "tool_use"] {
353 let msg = ServerMessage::Phase(Phase {
354 rid: Some("msg_01".into()),
355 phase: phase_val.to_string(),
356 model: Some("test-model".into()),
357 });
358 let (json, _back) = round_trip(&msg);
359 assert_eq!(field(&json, "type"), "phase");
360 assert_eq!(field(&json, "rid"), "msg_01");
361 assert_eq!(field(&json, "phase"), *phase_val);
362 }
363 }
364
365 #[test]
366 fn server_new_message_round_trip() {
367 let msg = ServerMessage::NewMessage(NewMessage {
368 revision: 3,
369 character: Some("Alice".into()),
370 message: Message {
371 msg_id: "m2".into(),
372 origin: Some(MessageOrigin::Autonomous),
373 role: Role::Assistant,
374 content: "autonomous msg".into(),
375 images: vec![],
376 content_blocks: vec![],
377 alt_index: None,
378 alt_count: None,
379 alternatives: vec![],
380 provider_key: None,
381 model: None,
382 timestamp: "2026-01-01T00:00:01Z".into(),
383 },
384 });
385 let (json, _back) = round_trip(&msg);
386 assert_eq!(field(&json, "type"), "new_message");
387 assert_eq!(field(&json, "character"), "Alice");
388 assert_eq!(field(&json, "origin"), "autonomous");
389 assert_eq!(field(&json, "msg_id"), "m2");
390 assert_eq!(field(&json, "revision"), 3);
391 }
392
393 #[test]
394 fn server_tool_call_round_trip() {
395 let msg = ServerMessage::ToolCall(ToolCall {
396 subagent: None,
397 rid: Some("msg_01".into()),
398 tool_id: "t1".into(),
399 tool_name: "search".into(),
400 input: json!({"query": "rust serde"}),
401 });
402 let (json, _back) = round_trip(&msg);
403 assert_eq!(field(&json, "type"), "tool_call");
404 assert_eq!(field(&json, "rid"), "msg_01");
405 let input = field(&json, "input");
406 assert_eq!(field(input, "query"), "rust serde");
407 assert!(input.is_object());
409 }
410
411 #[test]
412 fn server_tool_result_round_trip() {
413 let msg = ServerMessage::ToolResult(ToolResult {
414 subagent: None,
415 rid: Some("msg_01".into()),
416 tool_id: "t1".into(),
417 tool_name: "search".into(),
418 output: "found 5 results".into(),
419 is_error: false,
420 });
421 let (json, _back) = round_trip(&msg);
422 assert_eq!(field(&json, "type"), "tool_result");
423 assert_eq!(field(&json, "rid"), "msg_01");
424 }
425
426 #[test]
427 fn server_send_image_round_trip() {
428 let msg = ServerMessage::SendImage(SendImage {
429 subagent: None,
430 rid: Some("msg_01".into()),
431 path: "/tmp/img.png".into(),
432 caption: Some("generated chart".into()),
433 data: None,
434 });
435 let (json, _back) = round_trip(&msg);
436 assert_eq!(field(&json, "type"), "send_image");
437 assert_eq!(field(&json, "rid"), "msg_01");
438 assert_eq!(field(&json, "path"), "/tmp/img.png");
439 assert_eq!(field(&json, "caption"), "generated chart");
440 }
441
442 #[test]
443 fn server_cache_warning_round_trip() {
444 let msg = ServerMessage::CacheWarning(CacheWarning {
445 expected_tokens: 5000,
446 message: "cache miss".into(),
447 });
448 let (json, _back) = round_trip(&msg);
449 assert_eq!(field(&json, "type"), "cache_warning");
450 assert_eq!(field(&json, "expected_tokens"), 5000);
451 }
452
453 #[test]
454 fn server_usage_warning_round_trip() {
455 let msg = ServerMessage::UsageWarning(UsageWarning {
456 rid: Some("msg_01".into()),
457 budget: "daily total".into(),
458 message: "Usage budget \"daily total\" reached 80% ($8.00/$10.00).".into(),
459 current_cost: 8.0,
460 cost_limit: 10.0,
461 percent_used: 0.8,
462 crossed_warn_at: vec![0.8],
463 period: "day".into(),
464 period_start: "2026-05-18T00:00:00Z".into(),
465 reset_at: "2026-05-19T00:00:00Z".into(),
466 reset_at_display: "2026-05-19 10:00 AM".into(),
467 });
468 let (json, _back) = round_trip(&msg);
469 assert_eq!(field(&json, "type"), "usage_warning");
470 assert_eq!(field(&json, "rid"), "msg_01");
471 assert_eq!(field(&json, "budget"), "daily total");
472 }
473
474 #[test]
477 fn message_with_all_fields() {
478 let msg = Message {
479 msg_id: "m3".into(),
480 origin: None,
481 role: Role::Assistant,
482 content: "response".into(),
483 images: vec![ImageRef {
484 path: "/img/a.png".into(),
485 caption: Some("photo".into()),
486 data: None,
487 }],
488 content_blocks: vec![],
489 alt_index: Some(0),
490 alt_count: Some(1),
491 alternatives: vec![MessageAlternative {
492 content: "response".into(),
493 images: vec![],
494 content_blocks: vec![ContentBlock::Text {
495 text: "response".into(),
496 }],
497 timestamp: "2026-01-01T00:00:00Z".into(),
498 provider_key: Some("openrouter".into()),
499 model: Some("anthropic/claude-opus-4.6".into()),
500 }],
501 timestamp: "2026-01-01T00:00:00Z".into(),
502 provider_key: Some("anthropic".into()),
503 model: Some("claude-opus-4-6".into()),
504 };
505 let (json, back) = round_trip(&msg);
506 assert_eq!(field(&json, "alt_index"), 0);
507 assert_eq!(field(&json, "alt_count"), 1);
508 assert_eq!(field(&json, "model"), "claude-opus-4-6");
509 let alternatives = field(&json, "alternatives")
510 .as_array()
511 .expect("alternatives array");
512 let images = field(&json, "images").as_array().expect("images array");
513 assert_eq!(field(item(alternatives, 0), "content"), "response");
514 assert_eq!(
515 field(item(alternatives, 0), "model"),
516 "anthropic/claude-opus-4.6"
517 );
518 assert_eq!(field(item(images, 0), "path"), "/img/a.png");
519 assert_eq!(back.alt_index, Some(0));
520 assert_eq!(back.alt_count, Some(1));
521 assert_eq!(back.alternatives.len(), 1);
522 assert_eq!(back.model.as_deref(), Some("claude-opus-4-6"));
523 }
524
525 #[test]
526 fn message_without_alts_omits_fields() {
527 let msg = Message {
528 msg_id: "m4".into(),
529 origin: None,
530 role: Role::User,
531 content: "hi".into(),
532 images: vec![],
533 content_blocks: vec![],
534 alt_index: None,
535 alt_count: None,
536 alternatives: vec![],
537 provider_key: None,
538 model: None,
539 timestamp: "2026-01-01T00:00:00Z".into(),
540 };
541 let json = serde_json::to_value(&msg).unwrap();
542 assert!(json.get("alt_index").is_none());
543 assert!(json.get("alt_count").is_none());
544 assert!(json.get("alternatives").is_none());
545 assert!(json.get("provider_key").is_none());
546 assert!(json.get("model").is_none());
547 }
548
549 #[test]
550 fn stream_metadata_nested_structure() {
551 let meta = StreamMetadata {
552 tokens: TokenCounts {
553 input: 100,
554 output: 50,
555 cache_read: 0,
556 cache_write: 0,
557 },
558 timing: TimingInfo {
559 total_ms: 1000,
560 ttft_ms: 200,
561 },
562 model: "test".into(),
563 };
564 let (json, _back) = round_trip(&meta);
565 let tokens = field(&json, "tokens");
566 let timing = field(&json, "timing");
567 assert!(tokens.is_object());
568 assert!(timing.is_object());
569 assert_eq!(field(tokens, "input"), 100);
570 assert_eq!(field(timing, "ttft_ms"), 200);
571 }
572
573 #[test]
574 fn error_code_all_variants() {
575 let codes = [
576 ErrorCode::ProtocolError,
577 ErrorCode::InvalidRequest,
578 ErrorCode::NotFound,
579 ErrorCode::Busy,
580 ErrorCode::ProviderError,
581 ErrorCode::Timeout,
582 ErrorCode::InternalError,
583 ];
584 let expected = [
585 "protocol_error",
586 "invalid_request",
587 "not_found",
588 "busy",
589 "provider_error",
590 "timeout",
591 "internal_error",
592 ];
593 for (code, exp) in codes.iter().zip(expected.iter()) {
594 let json = serde_json::to_value(code).unwrap();
595 assert_eq!(json.as_str().unwrap(), *exp);
596 }
597 }
598
599 #[test]
600 fn character_info_round_trip() {
601 let info = CharacterInfo::new("alice");
602 let (json, back) = round_trip(&info);
603 assert!(json.get("avatar").is_none());
604 assert_eq!(back.name, "alice");
605 assert_eq!(back.avatar, None);
606 }
607
608 #[test]
609 fn character_info_avatar_round_trip() {
610 let info = CharacterInfo {
611 name: "alice".into(),
612 avatar: Some(CharacterAvatar {
613 mime_type: "image/png".into(),
614 data: "AQID".into(),
615 }),
616 };
617 let (json, back) = round_trip(&info);
618 assert_eq!(field(field(&json, "avatar"), "mime_type"), "image/png");
619 assert_eq!(back.avatar.unwrap().data, "AQID");
620 }
621
622 #[test]
623 fn role_serialization() {
624 assert_eq!(serde_json::to_value(Role::User).unwrap(), "user");
625 assert_eq!(serde_json::to_value(Role::Assistant).unwrap(), "assistant");
626 assert_eq!(serde_json::to_value(Role::System).unwrap(), "system");
627 }
628
629 #[test]
632 fn content_block_text_round_trip() {
633 let block = ContentBlock::Text {
634 text: "hello world".into(),
635 };
636 let json = serde_json::to_value(&block).unwrap();
637 assert_eq!(field(&json, "type"), "text");
638 assert_eq!(field(&json, "text"), "hello world");
639 let back: ContentBlock = serde_json::from_value(json).unwrap();
640 assert_eq!(back, block);
641 }
642
643 #[test]
644 fn content_block_thinking_round_trip() {
645 let block = ContentBlock::Thinking {
646 thinking: "Let me consider...".into(),
647 signature: None,
648 };
649 let json = serde_json::to_value(&block).unwrap();
650 assert_eq!(field(&json, "type"), "thinking");
651 assert_eq!(field(&json, "thinking"), "Let me consider...");
652 let back: ContentBlock = serde_json::from_value(json).unwrap();
653 assert_eq!(back, block);
654 }
655
656 #[test]
657 fn content_block_thinking_with_signature_round_trip() {
658 let block = ContentBlock::Thinking {
659 thinking: "Let me consider...".into(),
660 signature: Some("sig_abc123".into()),
661 };
662 let json = serde_json::to_value(&block).unwrap();
663 assert_eq!(field(&json, "type"), "thinking");
664 assert_eq!(field(&json, "thinking"), "Let me consider...");
665 assert_eq!(field(&json, "signature"), "sig_abc123");
666 let back: ContentBlock = serde_json::from_value(json).unwrap();
667 assert_eq!(back, block);
668 }
669
670 #[test]
671 fn content_block_redacted_thinking_round_trip() {
672 let block = ContentBlock::RedactedThinking {
673 data: "opaque_data_abc".into(),
674 };
675 let json = serde_json::to_value(&block).unwrap();
676 assert_eq!(field(&json, "type"), "redacted_thinking");
677 assert_eq!(field(&json, "data"), "opaque_data_abc");
678 let back: ContentBlock = serde_json::from_value(json).unwrap();
679 assert_eq!(back, block);
680 }
681
682 #[test]
683 fn content_block_thinking_without_signature_compat() {
684 let json = json!({"type": "thinking", "thinking": "old block"});
686 let block: ContentBlock = serde_json::from_value(json).unwrap();
687 let ContentBlock::Thinking {
688 thinking,
689 signature,
690 } = block
691 else {
692 panic!("Expected Thinking");
693 };
694 assert_eq!(thinking, "old block");
695 assert!(signature.is_none());
696 }
697
698 #[test]
699 fn content_block_tool_use_round_trip() {
700 let block = ContentBlock::ToolUse {
701 id: "tu_123".into(),
702 name: "check_time".into(),
703 input: json!({"timezone": "UTC"}),
704 };
705 let json = serde_json::to_value(&block).unwrap();
706 assert_eq!(field(&json, "type"), "tool_use");
707 assert_eq!(field(&json, "id"), "tu_123");
708 assert_eq!(field(&json, "name"), "check_time");
709 assert_eq!(field(field(&json, "input"), "timezone"), "UTC");
710 let back: ContentBlock = serde_json::from_value(json).unwrap();
711 assert_eq!(back, block);
712 }
713
714 #[test]
715 fn content_block_tool_result_round_trip() {
716 let block = ContentBlock::ToolResult {
717 tool_use_id: "tu_123".into(),
718 content: "2026-03-27T12:00:00Z".into(),
719 is_error: false,
720 };
721 let json = serde_json::to_value(&block).unwrap();
722 assert_eq!(field(&json, "type"), "tool_result");
723 assert_eq!(field(&json, "tool_use_id"), "tu_123");
724 assert_eq!(field(&json, "content"), "2026-03-27T12:00:00Z");
725 let back: ContentBlock = serde_json::from_value(json).unwrap();
727 assert_eq!(back, block);
728 }
729
730 #[test]
731 fn content_block_tool_result_with_error() {
732 let block = ContentBlock::ToolResult {
733 tool_use_id: "tu_456".into(),
734 content: "Tool not found".into(),
735 is_error: true,
736 };
737 let json = serde_json::to_value(&block).unwrap();
738 assert_eq!(field(&json, "is_error"), true);
739 let back: ContentBlock = serde_json::from_value(json).unwrap();
740 assert_eq!(back, block);
741 }
742
743 #[test]
744 fn content_block_tool_result_is_error_defaults_false() {
745 let json = json!({"type": "tool_result", "tool_use_id": "tu_1", "content": "ok"});
747 let block: ContentBlock = serde_json::from_value(json).unwrap();
748 let ContentBlock::ToolResult { is_error, .. } = block else {
749 panic!("Expected ToolResult");
750 };
751 assert!(!is_error);
752 }
753
754 #[test]
755 fn message_with_content_blocks_round_trip() {
756 let msg = Message {
757 msg_id: "m_test".into(),
758 origin: None,
759 role: Role::Assistant,
760 content: "The time is noon.".into(),
761 images: vec![],
762 content_blocks: vec![
763 ContentBlock::Thinking {
764 thinking: "User wants the time.".into(),
765 signature: None,
766 },
767 ContentBlock::ToolUse {
768 id: "tu_1".into(),
769 name: "check_time".into(),
770 input: json!({}),
771 },
772 ContentBlock::Text {
773 text: "The time is noon.".into(),
774 },
775 ],
776 alt_index: None,
777 alt_count: None,
778 alternatives: vec![],
779 provider_key: None,
780 model: None,
781 timestamp: "2026-01-01T00:00:00Z".into(),
782 };
783 let json = serde_json::to_value(&msg).unwrap();
784 let blocks = field(&json, "content_blocks").as_array().unwrap();
786 assert_eq!(blocks.len(), 3);
787 assert_eq!(field(item(blocks, 0), "type"), "thinking");
788 assert_eq!(field(item(blocks, 1), "type"), "tool_use");
789 assert_eq!(field(item(blocks, 2), "type"), "text");
790 let back: Message = serde_json::from_value(json).unwrap();
792 assert_eq!(back.content_blocks.len(), 3);
793 assert_eq!(back.content_blocks, msg.content_blocks);
794 }
795
796 #[test]
797 fn message_always_includes_content_blocks() {
798 let msg = Message {
799 msg_id: "m_old".into(),
800 origin: None,
801 role: Role::User,
802 content: "hello".into(),
803 images: vec![],
804 content_blocks: vec![],
805 alt_index: None,
806 alt_count: None,
807 alternatives: vec![],
808 provider_key: None,
809 model: None,
810 timestamp: "2026-01-01T00:00:00Z".into(),
811 };
812 let json = serde_json::to_value(&msg).unwrap();
813 assert!(
814 json.get("content_blocks").is_some(),
815 "content_blocks should always be serialized"
816 );
817 }
818
819 #[test]
820 fn old_message_json_without_content_blocks_deserializes() {
821 let json = json!({
823 "msg_id": "m_legacy",
824 "role": "assistant",
825 "content": "old message",
826 "timestamp": "2025-01-01T00:00:00Z"
827 });
828 let msg: Message = serde_json::from_value(json).unwrap();
829 assert!(msg.content_blocks.is_empty());
830 assert_eq!(msg.content, "old message");
831 }
832}