generated from nhcarrigan/template
914 lines
29 KiB
Rust
914 lines
29 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct UsageInfo {
|
|
pub input_tokens: u64,
|
|
pub output_tokens: u64,
|
|
#[serde(default)]
|
|
pub cache_creation_input_tokens: Option<u64>,
|
|
#[serde(default)]
|
|
pub cache_read_input_tokens: Option<u64>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum CharacterState {
|
|
#[default]
|
|
Idle,
|
|
Thinking,
|
|
Typing,
|
|
Searching,
|
|
Coding,
|
|
Mcp,
|
|
Permission,
|
|
Success,
|
|
Error,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ConnectionStatus {
|
|
#[default]
|
|
Disconnected,
|
|
Connecting,
|
|
Connected,
|
|
Error,
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TerminalLine {
|
|
pub id: String,
|
|
#[serde(rename = "type")]
|
|
pub line_type: String,
|
|
pub content: String,
|
|
pub timestamp: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub tool_name: Option<String>,
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PermissionRequest {
|
|
pub id: String,
|
|
pub tool: String,
|
|
pub description: String,
|
|
pub input: serde_json::Value,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PermissionDenial {
|
|
pub tool_name: String,
|
|
pub tool_use_id: String,
|
|
pub tool_input: serde_json::Value,
|
|
}
|
|
|
|
/// Rate limit information from a `rate_limit_event` message.
|
|
/// All fields are optional to ensure forward-compatibility as the Claude CLI evolves.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
|
pub struct RateLimitInfo {
|
|
#[serde(default)]
|
|
pub requests_limit: Option<u64>,
|
|
#[serde(default)]
|
|
pub requests_remaining: Option<u64>,
|
|
#[serde(default)]
|
|
pub requests_reset: Option<String>,
|
|
#[serde(default)]
|
|
pub tokens_limit: Option<u64>,
|
|
#[serde(default)]
|
|
pub tokens_remaining: Option<u64>,
|
|
#[serde(default)]
|
|
pub tokens_reset: Option<String>,
|
|
#[serde(default)]
|
|
pub retry_after_ms: Option<u64>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "type")]
|
|
pub enum ClaudeMessage {
|
|
#[serde(rename = "system")]
|
|
System {
|
|
subtype: String,
|
|
#[serde(default)]
|
|
session_id: Option<String>,
|
|
#[serde(default)]
|
|
cwd: Option<String>,
|
|
#[serde(default)]
|
|
tools: Option<Vec<String>>,
|
|
},
|
|
#[serde(rename = "assistant")]
|
|
Assistant {
|
|
message: AssistantMessageContent,
|
|
#[serde(default)]
|
|
parent_tool_use_id: Option<String>,
|
|
},
|
|
#[serde(rename = "user")]
|
|
User { message: UserMessageContent },
|
|
#[serde(rename = "stream_event")]
|
|
StreamEvent { event: StreamEventData },
|
|
#[serde(rename = "result")]
|
|
Result {
|
|
subtype: String,
|
|
#[serde(default)]
|
|
result: Option<String>,
|
|
#[serde(default)]
|
|
duration_ms: Option<u64>,
|
|
#[serde(default)]
|
|
num_turns: Option<u32>,
|
|
#[serde(default)]
|
|
permission_denials: Option<Vec<PermissionDenial>>,
|
|
#[serde(default)]
|
|
usage: Option<UsageInfo>,
|
|
},
|
|
#[serde(rename = "rate_limit_event")]
|
|
RateLimitEvent {
|
|
#[serde(default)]
|
|
rate_limit_info: RateLimitInfo,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AssistantMessageContent {
|
|
pub content: Vec<ContentBlock>,
|
|
#[serde(default)]
|
|
pub model: Option<String>,
|
|
#[serde(default)]
|
|
pub stop_reason: Option<String>,
|
|
#[serde(default)]
|
|
pub usage: Option<UsageInfo>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct UserMessageContent {
|
|
pub content: Vec<ContentBlock>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "type")]
|
|
pub enum ContentBlock {
|
|
#[serde(rename = "text")]
|
|
Text { text: String },
|
|
#[serde(rename = "thinking")]
|
|
Thinking { thinking: String },
|
|
#[serde(rename = "tool_use")]
|
|
ToolUse {
|
|
id: String,
|
|
name: String,
|
|
input: serde_json::Value,
|
|
},
|
|
#[serde(rename = "tool_result")]
|
|
ToolResult {
|
|
tool_use_id: String,
|
|
content: serde_json::Value,
|
|
#[serde(default)]
|
|
is_error: Option<bool>,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct StreamEventData {
|
|
#[serde(rename = "type")]
|
|
pub event_type: String,
|
|
#[serde(default)]
|
|
pub index: Option<u32>,
|
|
#[serde(default)]
|
|
pub content_block: Option<ContentBlockStart>,
|
|
#[serde(default)]
|
|
pub delta: Option<DeltaContent>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ContentBlockStart {
|
|
#[serde(rename = "type")]
|
|
pub block_type: String,
|
|
#[serde(default)]
|
|
pub id: Option<String>,
|
|
#[serde(default)]
|
|
pub name: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DeltaContent {
|
|
#[serde(rename = "type")]
|
|
pub delta_type: String,
|
|
#[serde(default)]
|
|
pub text: Option<String>,
|
|
#[serde(default)]
|
|
pub thinking: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct StateChangeEvent {
|
|
pub state: CharacterState,
|
|
pub tool_name: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub conversation_id: Option<String>,
|
|
}
|
|
|
|
/// Cost information for a message
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MessageCost {
|
|
pub input_tokens: u64,
|
|
pub output_tokens: u64,
|
|
pub cost_usd: f64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct OutputEvent {
|
|
pub line_type: String,
|
|
pub content: String,
|
|
pub tool_name: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub conversation_id: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub cost: Option<MessageCost>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub parent_tool_use_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PermissionPromptEventItem {
|
|
pub id: String,
|
|
pub tool_name: String,
|
|
pub tool_input: serde_json::Value,
|
|
pub description: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PermissionPromptEvent {
|
|
pub permissions: Vec<PermissionPromptEventItem>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub conversation_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ConnectionEvent {
|
|
pub status: ConnectionStatus,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub conversation_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct SessionEvent {
|
|
pub session_id: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub conversation_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct WorkingDirectoryEvent {
|
|
pub directory: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub conversation_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct QuestionOption {
|
|
pub label: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct UserQuestionEvent {
|
|
pub id: String,
|
|
pub question: String,
|
|
pub header: Option<String>,
|
|
pub options: Vec<QuestionOption>,
|
|
pub multi_select: bool,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub conversation_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ElicitationEvent {
|
|
pub message: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub server_name: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub request_id: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub conversation_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ElicitationResultEvent {
|
|
pub action: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub request_id: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub conversation_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct StopFailureEvent {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub stop_reason: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub error_type: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub conversation_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PostCompactEvent {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub session_id: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub conversation_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CwdChangedEvent {
|
|
pub cwd: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub conversation_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct FileChangedEvent {
|
|
pub file: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub conversation_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TaskCreatedEvent {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub task_id: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub description: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub parent_tool_use_id: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub conversation_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct PermissionDeniedEvent {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub tool_name: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub reason: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub conversation_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AgentStartEvent {
|
|
pub tool_use_id: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub agent_id: Option<String>,
|
|
pub description: String,
|
|
pub subagent_type: String,
|
|
pub started_at: u64,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub conversation_id: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub parent_tool_use_id: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub model: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct WorktreeInfo {
|
|
pub name: String,
|
|
pub path: String,
|
|
pub branch: String,
|
|
pub original_repo_directory: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct WorktreeEvent {
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub conversation_id: Option<String>,
|
|
/// "create" or "remove"
|
|
pub event_type: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub worktree: Option<WorktreeInfo>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AgentEndEvent {
|
|
pub tool_use_id: String,
|
|
pub ended_at: u64,
|
|
pub is_error: bool,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub conversation_id: Option<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub duration_ms: Option<u64>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub num_turns: Option<u32>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub last_assistant_message: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TodoItem {
|
|
pub content: String,
|
|
pub status: String, // "pending", "in_progress", or "completed"
|
|
#[serde(rename = "activeForm")]
|
|
pub active_form: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TodoUpdateEvent {
|
|
pub todos: Vec<TodoItem>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub conversation_id: Option<String>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_character_state_default() {
|
|
let state = CharacterState::default();
|
|
assert_eq!(state, CharacterState::Idle);
|
|
}
|
|
|
|
#[test]
|
|
fn test_connection_status_default() {
|
|
let status = ConnectionStatus::default();
|
|
matches!(status, ConnectionStatus::Disconnected);
|
|
}
|
|
|
|
#[test]
|
|
fn test_character_state_serialization() {
|
|
let state = CharacterState::Thinking;
|
|
let serialized = serde_json::to_string(&state).unwrap();
|
|
assert_eq!(serialized, "\"thinking\"");
|
|
|
|
let deserialized: CharacterState = serde_json::from_str(&serialized).unwrap();
|
|
assert_eq!(deserialized, CharacterState::Thinking);
|
|
}
|
|
|
|
#[test]
|
|
fn test_all_character_states_serialize() {
|
|
let states = vec![
|
|
(CharacterState::Idle, "\"idle\""),
|
|
(CharacterState::Thinking, "\"thinking\""),
|
|
(CharacterState::Typing, "\"typing\""),
|
|
(CharacterState::Searching, "\"searching\""),
|
|
(CharacterState::Coding, "\"coding\""),
|
|
(CharacterState::Mcp, "\"mcp\""),
|
|
(CharacterState::Permission, "\"permission\""),
|
|
(CharacterState::Success, "\"success\""),
|
|
(CharacterState::Error, "\"error\""),
|
|
];
|
|
|
|
for (state, expected) in states {
|
|
let serialized = serde_json::to_string(&state).unwrap();
|
|
assert_eq!(serialized, expected, "Failed for state: {:?}", state);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_terminal_line_serialization() {
|
|
let line = TerminalLine {
|
|
id: "test-123".to_string(),
|
|
line_type: "assistant".to_string(),
|
|
content: "Hello, world!".to_string(),
|
|
timestamp: "2024-01-01T00:00:00Z".to_string(),
|
|
tool_name: None,
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&line).unwrap();
|
|
assert!(serialized.contains("\"type\":\"assistant\""));
|
|
assert!(serialized.contains("\"content\":\"Hello, world!\""));
|
|
assert!(!serialized.contains("tool_name"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_terminal_line_with_tool_name() {
|
|
let line = TerminalLine {
|
|
id: "test-456".to_string(),
|
|
line_type: "tool".to_string(),
|
|
content: "Reading file...".to_string(),
|
|
timestamp: "2024-01-01T00:00:00Z".to_string(),
|
|
tool_name: Some("Read".to_string()),
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&line).unwrap();
|
|
assert!(serialized.contains("\"tool_name\":\"Read\""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_content_block_text() {
|
|
let block = ContentBlock::Text {
|
|
text: "Hello!".to_string(),
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&block).unwrap();
|
|
assert!(serialized.contains("\"type\":\"text\""));
|
|
assert!(serialized.contains("\"text\":\"Hello!\""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_content_block_tool_use() {
|
|
let block = ContentBlock::ToolUse {
|
|
id: "tool-123".to_string(),
|
|
name: "Read".to_string(),
|
|
input: serde_json::json!({"file_path": "/test.txt"}),
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&block).unwrap();
|
|
assert!(serialized.contains("\"type\":\"tool_use\""));
|
|
assert!(serialized.contains("\"name\":\"Read\""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_state_change_event() {
|
|
let event = StateChangeEvent {
|
|
state: CharacterState::Coding,
|
|
tool_name: Some("Edit".to_string()),
|
|
conversation_id: None,
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert!(serialized.contains("\"state\":\"coding\""));
|
|
assert!(serialized.contains("\"tool_name\":\"Edit\""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_output_event() {
|
|
let event = OutputEvent {
|
|
line_type: "assistant".to_string(),
|
|
content: "Test output".to_string(),
|
|
tool_name: None,
|
|
conversation_id: None,
|
|
cost: None,
|
|
parent_tool_use_id: None,
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert!(serialized.contains("\"line_type\":\"assistant\""));
|
|
assert!(serialized.contains("\"content\":\"Test output\""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_output_event_with_cost() {
|
|
let event = OutputEvent {
|
|
line_type: "assistant".to_string(),
|
|
content: "Test output".to_string(),
|
|
tool_name: None,
|
|
conversation_id: Some("conv-123".to_string()),
|
|
cost: Some(MessageCost {
|
|
input_tokens: 100,
|
|
output_tokens: 50,
|
|
cost_usd: 0.005,
|
|
}),
|
|
parent_tool_use_id: None,
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert!(serialized.contains("\"cost\":"));
|
|
assert!(serialized.contains("\"input_tokens\":100"));
|
|
assert!(serialized.contains("\"output_tokens\":50"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_rate_limit_info_default() {
|
|
let info = RateLimitInfo::default();
|
|
assert!(info.requests_limit.is_none());
|
|
assert!(info.requests_remaining.is_none());
|
|
assert!(info.requests_reset.is_none());
|
|
assert!(info.tokens_limit.is_none());
|
|
assert!(info.tokens_remaining.is_none());
|
|
assert!(info.tokens_reset.is_none());
|
|
assert!(info.retry_after_ms.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn test_rate_limit_event_deserialization_empty_info() {
|
|
let json = r#"{"type":"rate_limit_event","rate_limit_info":{}}"#;
|
|
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
|
|
assert!(matches!(msg, ClaudeMessage::RateLimitEvent { .. }));
|
|
}
|
|
|
|
#[test]
|
|
fn test_rate_limit_event_deserialization_no_info() {
|
|
// rate_limit_info field is optional via #[serde(default)]
|
|
let json = r#"{"type":"rate_limit_event"}"#;
|
|
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
|
|
assert!(matches!(msg, ClaudeMessage::RateLimitEvent { .. }));
|
|
}
|
|
|
|
#[test]
|
|
fn test_rate_limit_event_deserialization_with_data() {
|
|
let json = r#"{
|
|
"type": "rate_limit_event",
|
|
"rate_limit_info": {
|
|
"requests_limit": 1000,
|
|
"requests_remaining": 0,
|
|
"requests_reset": "2024-01-01T00:01:00Z",
|
|
"tokens_limit": 50000,
|
|
"tokens_remaining": 0,
|
|
"tokens_reset": "2024-01-01T00:01:00Z",
|
|
"retry_after_ms": 60000
|
|
}
|
|
}"#;
|
|
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
|
|
if let ClaudeMessage::RateLimitEvent { rate_limit_info } = msg {
|
|
assert_eq!(rate_limit_info.requests_limit, Some(1000));
|
|
assert_eq!(rate_limit_info.requests_remaining, Some(0));
|
|
assert_eq!(
|
|
rate_limit_info.requests_reset,
|
|
Some("2024-01-01T00:01:00Z".to_string())
|
|
);
|
|
assert_eq!(rate_limit_info.retry_after_ms, Some(60000));
|
|
} else {
|
|
panic!("Expected RateLimitEvent variant");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_rate_limit_event_ignores_unknown_fields() {
|
|
// Ensures forward-compat: unknown fields in rate_limit_info are silently ignored
|
|
let json = r#"{
|
|
"type": "rate_limit_event",
|
|
"rate_limit_info": {
|
|
"requests_remaining": 0,
|
|
"some_future_field": "some_value"
|
|
}
|
|
}"#;
|
|
let msg: ClaudeMessage = serde_json::from_str(json).unwrap();
|
|
if let ClaudeMessage::RateLimitEvent { rate_limit_info } = msg {
|
|
assert_eq!(rate_limit_info.requests_remaining, Some(0));
|
|
} else {
|
|
panic!("Expected RateLimitEvent variant");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_elicitation_event_serialization() {
|
|
let event = ElicitationEvent {
|
|
message: "Please provide the API endpoint".to_string(),
|
|
server_name: Some("my-server".to_string()),
|
|
request_id: Some("req-123".to_string()),
|
|
conversation_id: Some("conv-abc".to_string()),
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert!(serialized.contains("\"message\":\"Please provide the API endpoint\""));
|
|
assert!(serialized.contains("\"server_name\":\"my-server\""));
|
|
assert!(serialized.contains("\"request_id\":\"req-123\""));
|
|
assert!(serialized.contains("\"conversation_id\":\"conv-abc\""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_elicitation_event_omits_none_fields() {
|
|
let event = ElicitationEvent {
|
|
message: "Enter your token".to_string(),
|
|
server_name: None,
|
|
request_id: None,
|
|
conversation_id: None,
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert!(serialized.contains("\"message\":\"Enter your token\""));
|
|
assert!(!serialized.contains("server_name"));
|
|
assert!(!serialized.contains("request_id"));
|
|
assert!(!serialized.contains("conversation_id"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_elicitation_result_event_serialization() {
|
|
let event = ElicitationResultEvent {
|
|
action: "accept".to_string(),
|
|
request_id: Some("req-123".to_string()),
|
|
conversation_id: Some("conv-abc".to_string()),
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert!(serialized.contains("\"action\":\"accept\""));
|
|
assert!(serialized.contains("\"request_id\":\"req-123\""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_elicitation_result_event_cancel_omits_none_fields() {
|
|
let event = ElicitationResultEvent {
|
|
action: "cancel".to_string(),
|
|
request_id: None,
|
|
conversation_id: None,
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert!(serialized.contains("\"action\":\"cancel\""));
|
|
assert!(!serialized.contains("request_id"));
|
|
assert!(!serialized.contains("conversation_id"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_stop_failure_event_serialization() {
|
|
let event = StopFailureEvent {
|
|
stop_reason: Some("api_error".to_string()),
|
|
error_type: Some("rate_limit".to_string()),
|
|
conversation_id: Some("conv-abc".to_string()),
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert!(serialized.contains("\"stop_reason\":\"api_error\""));
|
|
assert!(serialized.contains("\"error_type\":\"rate_limit\""));
|
|
assert!(serialized.contains("\"conversation_id\":\"conv-abc\""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_stop_failure_event_omits_none_fields() {
|
|
let event = StopFailureEvent {
|
|
stop_reason: None,
|
|
error_type: None,
|
|
conversation_id: None,
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert!(!serialized.contains("stop_reason"));
|
|
assert!(!serialized.contains("error_type"));
|
|
assert!(!serialized.contains("conversation_id"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_stop_failure_event_partial_fields() {
|
|
let event = StopFailureEvent {
|
|
stop_reason: Some("api_error".to_string()),
|
|
error_type: None,
|
|
conversation_id: None,
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert!(serialized.contains("\"stop_reason\":\"api_error\""));
|
|
assert!(!serialized.contains("error_type"));
|
|
assert!(!serialized.contains("conversation_id"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_post_compact_event_serialization() {
|
|
let event = PostCompactEvent {
|
|
session_id: Some("sess-abc".to_string()),
|
|
conversation_id: Some("conv-123".to_string()),
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert!(serialized.contains("\"session_id\":\"sess-abc\""));
|
|
assert!(serialized.contains("\"conversation_id\":\"conv-123\""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_post_compact_event_omits_none_fields() {
|
|
let event = PostCompactEvent {
|
|
session_id: None,
|
|
conversation_id: None,
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert!(!serialized.contains("session_id"));
|
|
assert!(!serialized.contains("conversation_id"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_post_compact_event_partial_fields() {
|
|
let event = PostCompactEvent {
|
|
session_id: Some("sess-xyz".to_string()),
|
|
conversation_id: None,
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert!(serialized.contains("\"session_id\":\"sess-xyz\""));
|
|
assert!(!serialized.contains("conversation_id"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_cwd_changed_event_serialization() {
|
|
let event = CwdChangedEvent {
|
|
cwd: "/home/naomi/code/my-project".to_string(),
|
|
conversation_id: Some("conv-abc".to_string()),
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert!(serialized.contains("\"cwd\":\"/home/naomi/code/my-project\""));
|
|
assert!(serialized.contains("\"conversation_id\":\"conv-abc\""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_cwd_changed_event_omits_none_fields() {
|
|
let event = CwdChangedEvent {
|
|
cwd: "/tmp/workspace".to_string(),
|
|
conversation_id: None,
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert!(serialized.contains("\"cwd\":\"/tmp/workspace\""));
|
|
assert!(!serialized.contains("conversation_id"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_changed_event_serialization() {
|
|
let event = FileChangedEvent {
|
|
file: "/home/naomi/code/my-project/src/main.rs".to_string(),
|
|
conversation_id: Some("conv-abc".to_string()),
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert!(serialized.contains("\"file\":\"/home/naomi/code/my-project/src/main.rs\""));
|
|
assert!(serialized.contains("\"conversation_id\":\"conv-abc\""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_file_changed_event_omits_none_fields() {
|
|
let event = FileChangedEvent {
|
|
file: "/tmp/test.txt".to_string(),
|
|
conversation_id: None,
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert!(serialized.contains("\"file\":\"/tmp/test.txt\""));
|
|
assert!(!serialized.contains("conversation_id"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_task_created_event_serialization() {
|
|
let event = TaskCreatedEvent {
|
|
task_id: Some("task-abc123".to_string()),
|
|
description: Some("Explore the codebase".to_string()),
|
|
parent_tool_use_id: Some("toolu_xyz".to_string()),
|
|
conversation_id: Some("conv-abc".to_string()),
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert!(serialized.contains("\"task_id\":\"task-abc123\""));
|
|
assert!(serialized.contains("\"description\":\"Explore the codebase\""));
|
|
assert!(serialized.contains("\"parent_tool_use_id\":\"toolu_xyz\""));
|
|
assert!(serialized.contains("\"conversation_id\":\"conv-abc\""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_task_created_event_omits_none_fields() {
|
|
let event = TaskCreatedEvent {
|
|
task_id: None,
|
|
description: None,
|
|
parent_tool_use_id: None,
|
|
conversation_id: None,
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert_eq!(serialized, "{}");
|
|
}
|
|
|
|
#[test]
|
|
fn test_task_created_event_partial_fields() {
|
|
let event = TaskCreatedEvent {
|
|
task_id: Some("task-001".to_string()),
|
|
description: None,
|
|
parent_tool_use_id: None,
|
|
conversation_id: None,
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert!(serialized.contains("\"task_id\":\"task-001\""));
|
|
assert!(!serialized.contains("description"));
|
|
assert!(!serialized.contains("parent_tool_use_id"));
|
|
assert!(!serialized.contains("conversation_id"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_permission_denied_event_serialization() {
|
|
let event = PermissionDeniedEvent {
|
|
tool_name: Some("Bash".to_string()),
|
|
reason: Some("Tool not in allow list".to_string()),
|
|
conversation_id: Some("conv-abc".to_string()),
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert!(serialized.contains("\"tool_name\":\"Bash\""));
|
|
assert!(serialized.contains("\"reason\":\"Tool not in allow list\""));
|
|
assert!(serialized.contains("\"conversation_id\":\"conv-abc\""));
|
|
}
|
|
|
|
#[test]
|
|
fn test_permission_denied_event_omits_none_fields() {
|
|
let event = PermissionDeniedEvent {
|
|
tool_name: None,
|
|
reason: None,
|
|
conversation_id: None,
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert_eq!(serialized, "{}");
|
|
}
|
|
|
|
#[test]
|
|
fn test_permission_denied_event_partial_fields() {
|
|
let event = PermissionDeniedEvent {
|
|
tool_name: Some("Edit".to_string()),
|
|
reason: None,
|
|
conversation_id: None,
|
|
};
|
|
|
|
let serialized = serde_json::to_string(&event).unwrap();
|
|
assert!(serialized.contains("\"tool_name\":\"Edit\""));
|
|
assert!(!serialized.contains("reason"));
|
|
assert!(!serialized.contains("conversation_id"));
|
|
}
|
|
}
|