1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2use serde_json::Value;
3use std::fmt;
4use uuid::Uuid;
5
6use super::content_blocks::{deserialize_content_blocks, ContentBlock};
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
14pub enum SystemSubtype {
15 Init,
16 Status,
17 CompactBoundary,
18 TaskStarted,
19 TaskProgress,
20 TaskNotification,
21 Unknown(String),
23}
24
25impl SystemSubtype {
26 pub fn as_str(&self) -> &str {
27 match self {
28 Self::Init => "init",
29 Self::Status => "status",
30 Self::CompactBoundary => "compact_boundary",
31 Self::TaskStarted => "task_started",
32 Self::TaskProgress => "task_progress",
33 Self::TaskNotification => "task_notification",
34 Self::Unknown(s) => s.as_str(),
35 }
36 }
37}
38
39impl fmt::Display for SystemSubtype {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 f.write_str(self.as_str())
42 }
43}
44
45impl From<&str> for SystemSubtype {
46 fn from(s: &str) -> Self {
47 match s {
48 "init" => Self::Init,
49 "status" => Self::Status,
50 "compact_boundary" => Self::CompactBoundary,
51 "task_started" => Self::TaskStarted,
52 "task_progress" => Self::TaskProgress,
53 "task_notification" => Self::TaskNotification,
54 other => Self::Unknown(other.to_string()),
55 }
56 }
57}
58
59impl Serialize for SystemSubtype {
60 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
61 serializer.serialize_str(self.as_str())
62 }
63}
64
65impl<'de> Deserialize<'de> for SystemSubtype {
66 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
67 let s = String::deserialize(deserializer)?;
68 Ok(Self::from(s.as_str()))
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Hash)]
77pub enum MessageRole {
78 User,
79 Assistant,
80 Unknown(String),
82}
83
84impl MessageRole {
85 pub fn as_str(&self) -> &str {
86 match self {
87 Self::User => "user",
88 Self::Assistant => "assistant",
89 Self::Unknown(s) => s.as_str(),
90 }
91 }
92}
93
94impl fmt::Display for MessageRole {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.write_str(self.as_str())
97 }
98}
99
100impl From<&str> for MessageRole {
101 fn from(s: &str) -> Self {
102 match s {
103 "user" => Self::User,
104 "assistant" => Self::Assistant,
105 other => Self::Unknown(other.to_string()),
106 }
107 }
108}
109
110impl Serialize for MessageRole {
111 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
112 serializer.serialize_str(self.as_str())
113 }
114}
115
116impl<'de> Deserialize<'de> for MessageRole {
117 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
118 let s = String::deserialize(deserializer)?;
119 Ok(Self::from(s.as_str()))
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq, Hash)]
125pub enum CompactionTrigger {
126 Auto,
128 Manual,
130 Unknown(String),
132}
133
134impl CompactionTrigger {
135 pub fn as_str(&self) -> &str {
136 match self {
137 Self::Auto => "auto",
138 Self::Manual => "manual",
139 Self::Unknown(s) => s.as_str(),
140 }
141 }
142}
143
144impl fmt::Display for CompactionTrigger {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 f.write_str(self.as_str())
147 }
148}
149
150impl From<&str> for CompactionTrigger {
151 fn from(s: &str) -> Self {
152 match s {
153 "auto" => Self::Auto,
154 "manual" => Self::Manual,
155 other => Self::Unknown(other.to_string()),
156 }
157 }
158}
159
160impl Serialize for CompactionTrigger {
161 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
162 serializer.serialize_str(self.as_str())
163 }
164}
165
166impl<'de> Deserialize<'de> for CompactionTrigger {
167 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
168 let s = String::deserialize(deserializer)?;
169 Ok(Self::from(s.as_str()))
170 }
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Hash)]
175pub enum StopReason {
176 EndTurn,
178 MaxTokens,
180 ToolUse,
182 Unknown(String),
184}
185
186impl StopReason {
187 pub fn as_str(&self) -> &str {
188 match self {
189 Self::EndTurn => "end_turn",
190 Self::MaxTokens => "max_tokens",
191 Self::ToolUse => "tool_use",
192 Self::Unknown(s) => s.as_str(),
193 }
194 }
195}
196
197impl fmt::Display for StopReason {
198 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199 f.write_str(self.as_str())
200 }
201}
202
203impl From<&str> for StopReason {
204 fn from(s: &str) -> Self {
205 match s {
206 "end_turn" => Self::EndTurn,
207 "max_tokens" => Self::MaxTokens,
208 "tool_use" => Self::ToolUse,
209 other => Self::Unknown(other.to_string()),
210 }
211 }
212}
213
214impl Serialize for StopReason {
215 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
216 serializer.serialize_str(self.as_str())
217 }
218}
219
220impl<'de> Deserialize<'de> for StopReason {
221 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
222 let s = String::deserialize(deserializer)?;
223 Ok(Self::from(s.as_str()))
224 }
225}
226
227#[derive(Debug, Clone, PartialEq, Eq, Hash)]
229pub enum ApiKeySource {
230 None,
232 Unknown(String),
234}
235
236impl ApiKeySource {
237 pub fn as_str(&self) -> &str {
238 match self {
239 Self::None => "none",
240 Self::Unknown(s) => s.as_str(),
241 }
242 }
243}
244
245impl fmt::Display for ApiKeySource {
246 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
247 f.write_str(self.as_str())
248 }
249}
250
251impl From<&str> for ApiKeySource {
252 fn from(s: &str) -> Self {
253 match s {
254 "none" => Self::None,
255 other => Self::Unknown(other.to_string()),
256 }
257 }
258}
259
260impl Serialize for ApiKeySource {
261 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
262 serializer.serialize_str(self.as_str())
263 }
264}
265
266impl<'de> Deserialize<'de> for ApiKeySource {
267 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
268 let s = String::deserialize(deserializer)?;
269 Ok(Self::from(s.as_str()))
270 }
271}
272
273#[derive(Debug, Clone, PartialEq, Eq, Hash)]
275pub enum OutputStyle {
276 Default,
278 Unknown(String),
280}
281
282impl OutputStyle {
283 pub fn as_str(&self) -> &str {
284 match self {
285 Self::Default => "default",
286 Self::Unknown(s) => s.as_str(),
287 }
288 }
289}
290
291impl fmt::Display for OutputStyle {
292 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293 f.write_str(self.as_str())
294 }
295}
296
297impl From<&str> for OutputStyle {
298 fn from(s: &str) -> Self {
299 match s {
300 "default" => Self::Default,
301 other => Self::Unknown(other.to_string()),
302 }
303 }
304}
305
306impl Serialize for OutputStyle {
307 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
308 serializer.serialize_str(self.as_str())
309 }
310}
311
312impl<'de> Deserialize<'de> for OutputStyle {
313 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
314 let s = String::deserialize(deserializer)?;
315 Ok(Self::from(s.as_str()))
316 }
317}
318
319#[derive(Debug, Clone, PartialEq, Eq, Hash)]
321pub enum InitPermissionMode {
322 Default,
324 Unknown(String),
326}
327
328impl InitPermissionMode {
329 pub fn as_str(&self) -> &str {
330 match self {
331 Self::Default => "default",
332 Self::Unknown(s) => s.as_str(),
333 }
334 }
335}
336
337impl fmt::Display for InitPermissionMode {
338 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
339 f.write_str(self.as_str())
340 }
341}
342
343impl From<&str> for InitPermissionMode {
344 fn from(s: &str) -> Self {
345 match s {
346 "default" => Self::Default,
347 other => Self::Unknown(other.to_string()),
348 }
349 }
350}
351
352impl Serialize for InitPermissionMode {
353 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
354 serializer.serialize_str(self.as_str())
355 }
356}
357
358impl<'de> Deserialize<'de> for InitPermissionMode {
359 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
360 let s = String::deserialize(deserializer)?;
361 Ok(Self::from(s.as_str()))
362 }
363}
364
365#[derive(Debug, Clone, PartialEq, Eq, Hash)]
367pub enum StatusMessageStatus {
368 Compacting,
370 Unknown(String),
372}
373
374impl StatusMessageStatus {
375 pub fn as_str(&self) -> &str {
376 match self {
377 Self::Compacting => "compacting",
378 Self::Unknown(s) => s.as_str(),
379 }
380 }
381}
382
383impl fmt::Display for StatusMessageStatus {
384 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385 f.write_str(self.as_str())
386 }
387}
388
389impl From<&str> for StatusMessageStatus {
390 fn from(s: &str) -> Self {
391 match s {
392 "compacting" => Self::Compacting,
393 other => Self::Unknown(other.to_string()),
394 }
395 }
396}
397
398impl Serialize for StatusMessageStatus {
399 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
400 serializer.serialize_str(self.as_str())
401 }
402}
403
404impl<'de> Deserialize<'de> for StatusMessageStatus {
405 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
406 let s = String::deserialize(deserializer)?;
407 Ok(Self::from(s.as_str()))
408 }
409}
410
411pub(crate) fn serialize_optional_uuid<S>(
413 uuid: &Option<Uuid>,
414 serializer: S,
415) -> Result<S::Ok, S::Error>
416where
417 S: Serializer,
418{
419 match uuid {
420 Some(id) => serializer.serialize_str(&id.to_string()),
421 None => serializer.serialize_none(),
422 }
423}
424
425pub(crate) fn deserialize_optional_uuid<'de, D>(deserializer: D) -> Result<Option<Uuid>, D::Error>
427where
428 D: Deserializer<'de>,
429{
430 let opt_str: Option<String> = Option::deserialize(deserializer)?;
431 match opt_str {
432 Some(s) => Uuid::parse_str(&s)
433 .map(Some)
434 .map_err(serde::de::Error::custom),
435 None => Ok(None),
436 }
437}
438
439#[derive(Debug, Clone, Serialize, Deserialize)]
441pub struct UserMessage {
442 pub message: MessageContent,
443 #[serde(skip_serializing_if = "Option::is_none")]
444 #[serde(
445 serialize_with = "serialize_optional_uuid",
446 deserialize_with = "deserialize_optional_uuid"
447 )]
448 pub session_id: Option<Uuid>,
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct MessageContent {
454 pub role: MessageRole,
455 #[serde(deserialize_with = "deserialize_content_blocks")]
456 pub content: Vec<ContentBlock>,
457}
458
459#[derive(Debug, Clone, Serialize, Deserialize)]
461pub struct SystemMessage {
462 pub subtype: SystemSubtype,
463 #[serde(flatten)]
464 pub data: Value, }
466
467impl SystemMessage {
468 pub fn is_init(&self) -> bool {
470 self.subtype == SystemSubtype::Init
471 }
472
473 pub fn is_status(&self) -> bool {
475 self.subtype == SystemSubtype::Status
476 }
477
478 pub fn is_compact_boundary(&self) -> bool {
480 self.subtype == SystemSubtype::CompactBoundary
481 }
482
483 pub fn as_init(&self) -> Option<InitMessage> {
485 if self.subtype != SystemSubtype::Init {
486 return None;
487 }
488 serde_json::from_value(self.data.clone()).ok()
489 }
490
491 pub fn as_status(&self) -> Option<StatusMessage> {
493 if self.subtype != SystemSubtype::Status {
494 return None;
495 }
496 serde_json::from_value(self.data.clone()).ok()
497 }
498
499 pub fn as_compact_boundary(&self) -> Option<CompactBoundaryMessage> {
501 if self.subtype != SystemSubtype::CompactBoundary {
502 return None;
503 }
504 serde_json::from_value(self.data.clone()).ok()
505 }
506
507 pub fn is_task_started(&self) -> bool {
509 self.subtype == SystemSubtype::TaskStarted
510 }
511
512 pub fn is_task_progress(&self) -> bool {
514 self.subtype == SystemSubtype::TaskProgress
515 }
516
517 pub fn is_task_notification(&self) -> bool {
519 self.subtype == SystemSubtype::TaskNotification
520 }
521
522 pub fn as_task_started(&self) -> Option<TaskStartedMessage> {
524 if self.subtype != SystemSubtype::TaskStarted {
525 return None;
526 }
527 serde_json::from_value(self.data.clone()).ok()
528 }
529
530 pub fn as_task_progress(&self) -> Option<TaskProgressMessage> {
532 if self.subtype != SystemSubtype::TaskProgress {
533 return None;
534 }
535 serde_json::from_value(self.data.clone()).ok()
536 }
537
538 pub fn as_task_notification(&self) -> Option<TaskNotificationMessage> {
540 if self.subtype != SystemSubtype::TaskNotification {
541 return None;
542 }
543 serde_json::from_value(self.data.clone()).ok()
544 }
545}
546
547#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct PluginInfo {
550 pub name: String,
552 pub path: String,
554}
555
556#[derive(Debug, Clone, Serialize, Deserialize)]
558pub struct InitMessage {
559 pub session_id: String,
561 #[serde(skip_serializing_if = "Option::is_none")]
563 pub cwd: Option<String>,
564 #[serde(skip_serializing_if = "Option::is_none")]
566 pub model: Option<String>,
567 #[serde(default, skip_serializing_if = "Vec::is_empty")]
569 pub tools: Vec<String>,
570 #[serde(default, skip_serializing_if = "Vec::is_empty")]
572 pub mcp_servers: Vec<Value>,
573 #[serde(default, skip_serializing_if = "Vec::is_empty")]
575 pub slash_commands: Vec<String>,
576 #[serde(default, skip_serializing_if = "Vec::is_empty")]
578 pub agents: Vec<String>,
579 #[serde(default, skip_serializing_if = "Vec::is_empty")]
581 pub plugins: Vec<PluginInfo>,
582 #[serde(default, skip_serializing_if = "Vec::is_empty")]
584 pub skills: Vec<Value>,
585 #[serde(skip_serializing_if = "Option::is_none")]
587 pub claude_code_version: Option<String>,
588 #[serde(skip_serializing_if = "Option::is_none", rename = "apiKeySource")]
590 pub api_key_source: Option<ApiKeySource>,
591 #[serde(skip_serializing_if = "Option::is_none")]
593 pub output_style: Option<OutputStyle>,
594 #[serde(skip_serializing_if = "Option::is_none", rename = "permissionMode")]
596 pub permission_mode: Option<InitPermissionMode>,
597}
598
599#[derive(Debug, Clone, Serialize, Deserialize)]
601pub struct StatusMessage {
602 pub session_id: String,
604 pub status: Option<StatusMessageStatus>,
606 #[serde(skip_serializing_if = "Option::is_none")]
608 pub uuid: Option<String>,
609}
610
611#[derive(Debug, Clone, Serialize, Deserialize)]
613pub struct CompactBoundaryMessage {
614 pub session_id: String,
616 pub compact_metadata: CompactMetadata,
618 #[serde(skip_serializing_if = "Option::is_none")]
620 pub uuid: Option<String>,
621}
622
623#[derive(Debug, Clone, Serialize, Deserialize)]
625pub struct CompactMetadata {
626 pub pre_tokens: u64,
628 pub trigger: CompactionTrigger,
630}
631
632#[derive(Debug, Clone, Serialize, Deserialize)]
638pub struct TaskUsage {
639 pub duration_ms: u64,
641 pub tool_uses: u64,
643 pub total_tokens: u64,
645}
646
647#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
649#[serde(rename_all = "snake_case")]
650pub enum TaskType {
651 LocalAgent,
653 LocalBash,
655}
656
657#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
659#[serde(rename_all = "snake_case")]
660pub enum TaskStatus {
661 Completed,
662 Failed,
663}
664
665#[derive(Debug, Clone, Serialize, Deserialize)]
667pub struct TaskStartedMessage {
668 pub session_id: String,
669 pub task_id: String,
670 pub task_type: TaskType,
671 pub tool_use_id: String,
672 pub description: String,
673 pub uuid: String,
674}
675
676#[derive(Debug, Clone, Serialize, Deserialize)]
679pub struct TaskProgressMessage {
680 pub session_id: String,
681 pub task_id: String,
682 pub tool_use_id: String,
683 pub description: String,
684 pub last_tool_name: String,
685 pub usage: TaskUsage,
686 pub uuid: String,
687}
688
689#[derive(Debug, Clone, Serialize, Deserialize)]
692pub struct TaskNotificationMessage {
693 pub session_id: String,
694 pub task_id: String,
695 pub status: TaskStatus,
696 pub summary: String,
697 pub output_file: Option<String>,
698 #[serde(skip_serializing_if = "Option::is_none")]
699 pub tool_use_id: Option<String>,
700 #[serde(skip_serializing_if = "Option::is_none")]
701 pub usage: Option<TaskUsage>,
702 #[serde(skip_serializing_if = "Option::is_none")]
703 pub uuid: Option<String>,
704}
705
706#[derive(Debug, Clone, Serialize, Deserialize)]
708pub struct AssistantMessage {
709 pub message: AssistantMessageContent,
710 pub session_id: String,
711 #[serde(skip_serializing_if = "Option::is_none")]
712 pub uuid: Option<String>,
713 #[serde(skip_serializing_if = "Option::is_none")]
714 pub parent_tool_use_id: Option<String>,
715}
716
717#[derive(Debug, Clone, Serialize, Deserialize)]
719pub struct AssistantMessageContent {
720 pub id: String,
721 pub role: MessageRole,
722 pub model: String,
723 pub content: Vec<ContentBlock>,
724 #[serde(skip_serializing_if = "Option::is_none")]
725 pub stop_reason: Option<StopReason>,
726 #[serde(skip_serializing_if = "Option::is_none")]
727 pub stop_sequence: Option<String>,
728 #[serde(skip_serializing_if = "Option::is_none")]
729 pub usage: Option<AssistantUsage>,
730}
731
732#[derive(Debug, Clone, Serialize, Deserialize)]
734pub struct AssistantUsage {
735 #[serde(default)]
737 pub input_tokens: u32,
738
739 #[serde(default)]
741 pub output_tokens: u32,
742
743 #[serde(default)]
745 pub cache_creation_input_tokens: u32,
746
747 #[serde(default)]
749 pub cache_read_input_tokens: u32,
750
751 #[serde(skip_serializing_if = "Option::is_none")]
753 pub service_tier: Option<String>,
754
755 #[serde(skip_serializing_if = "Option::is_none")]
757 pub cache_creation: Option<CacheCreationDetails>,
758}
759
760#[derive(Debug, Clone, Serialize, Deserialize)]
762pub struct CacheCreationDetails {
763 #[serde(default)]
765 pub ephemeral_1h_input_tokens: u32,
766
767 #[serde(default)]
769 pub ephemeral_5m_input_tokens: u32,
770}
771
772#[cfg(test)]
773mod tests {
774 use crate::io::ClaudeOutput;
775
776 #[test]
777 fn test_system_message_init() {
778 let json = r#"{
779 "type": "system",
780 "subtype": "init",
781 "session_id": "test-session-123",
782 "cwd": "/home/user/project",
783 "model": "claude-sonnet-4",
784 "tools": ["Bash", "Read", "Write"],
785 "mcp_servers": [],
786 "slash_commands": ["compact", "cost", "review"],
787 "agents": ["Bash", "Explore", "Plan"],
788 "plugins": [{"name": "rust-analyzer-lsp", "path": "/home/user/.claude/plugins/rust-analyzer-lsp/1.0.0"}],
789 "skills": [],
790 "claude_code_version": "2.1.15",
791 "apiKeySource": "none",
792 "output_style": "default",
793 "permissionMode": "default"
794 }"#;
795
796 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
797 if let ClaudeOutput::System(sys) = output {
798 assert!(sys.is_init());
799 assert!(!sys.is_status());
800 assert!(!sys.is_compact_boundary());
801
802 let init = sys.as_init().expect("Should parse as init");
803 assert_eq!(init.session_id, "test-session-123");
804 assert_eq!(init.cwd, Some("/home/user/project".to_string()));
805 assert_eq!(init.model, Some("claude-sonnet-4".to_string()));
806 assert_eq!(init.tools, vec!["Bash", "Read", "Write"]);
807 assert_eq!(init.slash_commands, vec!["compact", "cost", "review"]);
808 assert_eq!(init.agents, vec!["Bash", "Explore", "Plan"]);
809 assert_eq!(init.plugins.len(), 1);
810 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
811 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
812 assert_eq!(init.api_key_source, Some(super::ApiKeySource::None));
813 assert_eq!(init.output_style, Some(super::OutputStyle::Default));
814 assert_eq!(
815 init.permission_mode,
816 Some(super::InitPermissionMode::Default)
817 );
818 } else {
819 panic!("Expected System message");
820 }
821 }
822
823 #[test]
824 fn test_system_message_init_from_real_capture() {
825 let json = include_str!("../../test_cases/tool_use_captures/tool_msg_0.json");
826 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
827 if let ClaudeOutput::System(sys) = output {
828 let init = sys.as_init().expect("Should parse real init capture");
829 assert_eq!(init.slash_commands.len(), 8);
830 assert!(init.slash_commands.contains(&"compact".to_string()));
831 assert!(init.slash_commands.contains(&"review".to_string()));
832 assert_eq!(init.agents.len(), 5);
833 assert!(init.agents.contains(&"Bash".to_string()));
834 assert!(init.agents.contains(&"Explore".to_string()));
835 assert_eq!(init.plugins.len(), 1);
836 assert_eq!(init.plugins[0].name, "rust-analyzer-lsp");
837 assert_eq!(init.claude_code_version, Some("2.1.15".to_string()));
838 } else {
839 panic!("Expected System message");
840 }
841 }
842
843 #[test]
844 fn test_system_message_status() {
845 let json = r#"{
846 "type": "system",
847 "subtype": "status",
848 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
849 "status": "compacting",
850 "uuid": "32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93"
851 }"#;
852
853 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
854 if let ClaudeOutput::System(sys) = output {
855 assert!(sys.is_status());
856 assert!(!sys.is_init());
857
858 let status = sys.as_status().expect("Should parse as status");
859 assert_eq!(status.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
860 assert_eq!(status.status, Some(super::StatusMessageStatus::Compacting));
861 assert_eq!(
862 status.uuid,
863 Some("32eb9f9d-5ef7-47ff-8fce-bbe22fe7ed93".to_string())
864 );
865 } else {
866 panic!("Expected System message");
867 }
868 }
869
870 #[test]
871 fn test_system_message_status_null() {
872 let json = r#"{
873 "type": "system",
874 "subtype": "status",
875 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
876 "status": null,
877 "uuid": "92d9637e-d00e-418e-acd2-a504e3861c6a"
878 }"#;
879
880 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
881 if let ClaudeOutput::System(sys) = output {
882 let status = sys.as_status().expect("Should parse as status");
883 assert_eq!(status.status, None);
884 } else {
885 panic!("Expected System message");
886 }
887 }
888
889 #[test]
890 fn test_system_message_task_started() {
891 let json = r#"{
892 "type": "system",
893 "subtype": "task_started",
894 "session_id": "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9",
895 "task_id": "b6daf3f",
896 "task_type": "local_bash",
897 "tool_use_id": "toolu_011rfSTFumpJZdCCfzeD7jaS",
898 "description": "Wait for CI on PR #12",
899 "uuid": "c4243261-c128-4747-b8c3-5e1c7c10eeb8"
900 }"#;
901
902 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
903 if let ClaudeOutput::System(sys) = output {
904 assert!(sys.is_task_started());
905 assert!(!sys.is_task_progress());
906 assert!(!sys.is_task_notification());
907
908 let task = sys.as_task_started().expect("Should parse as task_started");
909 assert_eq!(task.session_id, "9abbc466-dad0-4b8e-b6b0-cad5eb7a16b9");
910 assert_eq!(task.task_id, "b6daf3f");
911 assert_eq!(task.task_type, super::TaskType::LocalBash);
912 assert_eq!(task.tool_use_id, "toolu_011rfSTFumpJZdCCfzeD7jaS");
913 assert_eq!(task.description, "Wait for CI on PR #12");
914 } else {
915 panic!("Expected System message");
916 }
917 }
918
919 #[test]
920 fn test_system_message_task_started_agent() {
921 let json = r#"{
922 "type": "system",
923 "subtype": "task_started",
924 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
925 "task_id": "a4a7e0906e5fc64cc",
926 "task_type": "local_agent",
927 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
928 "description": "Explore Scene/ArrayScene duplication",
929 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
930 }"#;
931
932 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
933 if let ClaudeOutput::System(sys) = output {
934 let task = sys.as_task_started().expect("Should parse as task_started");
935 assert_eq!(task.task_type, super::TaskType::LocalAgent);
936 assert_eq!(task.task_id, "a4a7e0906e5fc64cc");
937 } else {
938 panic!("Expected System message");
939 }
940 }
941
942 #[test]
943 fn test_system_message_task_progress() {
944 let json = r#"{
945 "type": "system",
946 "subtype": "task_progress",
947 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
948 "task_id": "a4a7e0906e5fc64cc",
949 "tool_use_id": "toolu_01SFz9FwZ1cYgCSy8vRM7wep",
950 "description": "Reading src/jplephem/chebyshev.rs",
951 "last_tool_name": "Read",
952 "usage": {
953 "duration_ms": 13996,
954 "tool_uses": 9,
955 "total_tokens": 38779
956 },
957 "uuid": "85a39f5a-e4d4-47f7-9a6d-1125f1a8035f"
958 }"#;
959
960 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
961 if let ClaudeOutput::System(sys) = output {
962 assert!(sys.is_task_progress());
963 assert!(!sys.is_task_started());
964
965 let progress = sys
966 .as_task_progress()
967 .expect("Should parse as task_progress");
968 assert_eq!(progress.task_id, "a4a7e0906e5fc64cc");
969 assert_eq!(progress.description, "Reading src/jplephem/chebyshev.rs");
970 assert_eq!(progress.last_tool_name, "Read");
971 assert_eq!(progress.usage.duration_ms, 13996);
972 assert_eq!(progress.usage.tool_uses, 9);
973 assert_eq!(progress.usage.total_tokens, 38779);
974 } else {
975 panic!("Expected System message");
976 }
977 }
978
979 #[test]
980 fn test_system_message_task_notification_completed() {
981 let json = r#"{
982 "type": "system",
983 "subtype": "task_notification",
984 "session_id": "bff4f716-17c1-4255-ab7b-eea9d33824e3",
985 "task_id": "a0ba761e9dc9c316f",
986 "tool_use_id": "toolu_01Ho6XVXFLVNjTQ9YqowdBXW",
987 "status": "completed",
988 "summary": "Agent \"Write Hipparcos data source doc\" completed",
989 "output_file": "",
990 "usage": {
991 "duration_ms": 172300,
992 "tool_uses": 11,
993 "total_tokens": 42005
994 },
995 "uuid": "269f49b9-218d-4c8d-9f7e-3a5383a0c5b2"
996 }"#;
997
998 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
999 if let ClaudeOutput::System(sys) = output {
1000 assert!(sys.is_task_notification());
1001
1002 let notif = sys
1003 .as_task_notification()
1004 .expect("Should parse as task_notification");
1005 assert_eq!(notif.status, super::TaskStatus::Completed);
1006 assert_eq!(
1007 notif.summary,
1008 "Agent \"Write Hipparcos data source doc\" completed"
1009 );
1010 assert_eq!(notif.output_file, Some("".to_string()));
1011 assert_eq!(
1012 notif.tool_use_id,
1013 Some("toolu_01Ho6XVXFLVNjTQ9YqowdBXW".to_string())
1014 );
1015 let usage = notif.usage.expect("Should have usage");
1016 assert_eq!(usage.duration_ms, 172300);
1017 assert_eq!(usage.tool_uses, 11);
1018 assert_eq!(usage.total_tokens, 42005);
1019 } else {
1020 panic!("Expected System message");
1021 }
1022 }
1023
1024 #[test]
1025 fn test_system_message_task_notification_failed_no_usage() {
1026 let json = r#"{
1027 "type": "system",
1028 "subtype": "task_notification",
1029 "session_id": "ea629737-3c36-48a8-a1c4-ad761ad35784",
1030 "task_id": "b98f6a3",
1031 "status": "failed",
1032 "summary": "Background command \"Run FSM calibration\" failed with exit code 1",
1033 "output_file": "/tmp/claude-1000/tasks/b98f6a3.output"
1034 }"#;
1035
1036 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1037 if let ClaudeOutput::System(sys) = output {
1038 let notif = sys
1039 .as_task_notification()
1040 .expect("Should parse as task_notification");
1041 assert_eq!(notif.status, super::TaskStatus::Failed);
1042 assert!(notif.tool_use_id.is_none());
1043 assert!(notif.usage.is_none());
1044 assert_eq!(
1045 notif.output_file,
1046 Some("/tmp/claude-1000/tasks/b98f6a3.output".to_string())
1047 );
1048 } else {
1049 panic!("Expected System message");
1050 }
1051 }
1052
1053 #[test]
1054 fn test_system_message_compact_boundary() {
1055 let json = r#"{
1056 "type": "system",
1057 "subtype": "compact_boundary",
1058 "session_id": "879c1a88-3756-4092-aa95-0020c4ed9692",
1059 "compact_metadata": {
1060 "pre_tokens": 155285,
1061 "trigger": "auto"
1062 },
1063 "uuid": "a67780d5-74cb-48b1-9137-7a6e7cee45d7"
1064 }"#;
1065
1066 let output: ClaudeOutput = serde_json::from_str(json).unwrap();
1067 if let ClaudeOutput::System(sys) = output {
1068 assert!(sys.is_compact_boundary());
1069 assert!(!sys.is_init());
1070 assert!(!sys.is_status());
1071
1072 let compact = sys
1073 .as_compact_boundary()
1074 .expect("Should parse as compact_boundary");
1075 assert_eq!(compact.session_id, "879c1a88-3756-4092-aa95-0020c4ed9692");
1076 assert_eq!(compact.compact_metadata.pre_tokens, 155285);
1077 assert_eq!(
1078 compact.compact_metadata.trigger,
1079 super::CompactionTrigger::Auto
1080 );
1081 } else {
1082 panic!("Expected System message");
1083 }
1084 }
1085}