use picobot::protocol::{HistoryMessage, SessionSummary, WsInbound, WsOutbound}; use picobot::providers::{ChatCompletionRequest, Message, ToolCall}; use picobot::work::{TaskItem, TaskPlan}; /// Test that message with special characters is properly escaped #[test] fn test_message_special_characters() { let msg = Message::user("Hello \"world\"\nNew line\tTab"); let json = serde_json::to_string(&msg).unwrap(); let deserialized: Message = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.role, "user"); assert_eq!(deserialized.content.len(), 1); let encoded = serde_json::to_string(&deserialized.content).unwrap(); assert!(encoded.contains("Hello \\\"world\\\"\\nNew line\\tTab")); } /// Test that multi-line system prompt is preserved #[test] fn test_multiline_system_prompt() { let messages = [ Message::system( "You are a helpful assistant.\n\nFollow these rules:\n1. Be kind\n2. Be accurate", ), Message::user("Hi"), ]; let json = serde_json::to_string(&messages[0]).unwrap(); assert!(json.contains("helpful assistant")); assert!(json.contains("rules")); assert!(json.contains("1. Be kind")); } /// Test ChatCompletionRequest serialization (without model field) #[test] fn test_chat_request_serialization() { let request = ChatCompletionRequest { messages: vec![Message::system("You are helpful"), Message::user("Hello")], temperature: Some(0.7), max_tokens: Some(100), tools: None, }; let json = serde_json::to_string(&request).unwrap(); // Verify structure assert!(json.contains(r#""role":"system""#)); assert!(json.contains(r#""role":"user""#)); assert!(json.contains("You are helpful")); assert!(json.contains("Hello")); assert!(json.contains(r#""temperature":0.7"#)); assert!(json.contains(r#""max_tokens":100"#)); } #[test] fn test_session_inbound_serialization() { let msg = WsInbound::CreateSession { title: Some("demo".to_string()), }; let json = serde_json::to_string(&msg).unwrap(); assert!(json.contains(r#""type":"create_session""#)); assert!(json.contains(r#""title":"demo""#)); let decoded: WsInbound = serde_json::from_str(&json).unwrap(); match decoded { WsInbound::CreateSession { title } => { assert_eq!(title.as_deref(), Some("demo")); } other => panic!("unexpected decoded variant: {:?}", other), } } #[test] fn test_session_list_outbound_serialization() { let msg = WsOutbound::SessionList { sessions: vec![SessionSummary { session_id: "session-1".to_string(), title: "demo".to_string(), channel_name: "cli".to_string(), chat_id: "session-1".to_string(), message_count: 2, last_active_at: 123, archived_at: None, }], current_session_id: Some("session-1".to_string()), }; let json = serde_json::to_string(&msg).unwrap(); assert!(json.contains(r#""type":"session_list""#)); assert!(json.contains(r#""session_id":"session-1""#)); assert!(json.contains(r#""message_count":2"#)); let decoded: WsOutbound = serde_json::from_str(&json).unwrap(); match decoded { WsOutbound::SessionList { sessions, current_session_id, } => { assert_eq!(sessions.len(), 1); assert_eq!(sessions[0].title, "demo"); assert_eq!(current_session_id.as_deref(), Some("session-1")); } other => panic!("unexpected decoded variant: {:?}", other), } } #[test] fn test_clear_history_with_session_id_serialization() { let msg = WsInbound::ClearHistory { chat_id: None, session_id: Some("session-1".to_string()), }; let json = serde_json::to_string(&msg).unwrap(); assert!(json.contains(r#""type":"clear_history""#)); assert!(json.contains(r#""session_id":"session-1""#)); } #[test] fn test_bounded_session_history_protocol() { let inbound = WsInbound::GetSessionHistory { session_id: "cli_chat:client:dialog".to_string(), limit: Some(1000), }; let json = serde_json::to_string(&inbound).unwrap(); assert!(json.contains(r#""type":"get_session_history""#)); assert!(json.contains(r#""limit":1000"#)); let outbound = WsOutbound::SessionHistory { session_id: "cli_chat:client:dialog".to_string(), messages: vec![HistoryMessage { id: "m1".to_string(), seq: 1, role: "user".to_string(), content: "你好".to_string(), created_at: 123, tool_call_id: None, tool_name: None, tool_calls: None, }], }; let decoded: WsOutbound = serde_json::from_str(&serde_json::to_string(&outbound).unwrap()).unwrap(); match decoded { WsOutbound::SessionHistory { messages, .. } => { assert_eq!(messages.len(), 1); assert_eq!(messages[0].content, "你好"); } other => panic!("unexpected decoded variant: {other:?}"), } } #[test] fn test_session_history_preserves_tool_call_metadata() { let outbound = WsOutbound::SessionHistory { session_id: "cli_chat:client:dialog".to_string(), messages: vec![HistoryMessage { id: "m-tool".to_string(), seq: 2, role: "assistant".to_string(), content: String::new(), created_at: 124, tool_call_id: None, tool_name: None, tool_calls: Some(vec![ToolCall { id: "call-1".to_string(), name: "read_file".to_string(), arguments: serde_json::json!({ "path": "README.md" }), }]), }], }; let json = serde_json::to_string(&outbound).unwrap(); assert!(json.contains(r#""tool_calls""#)); assert!(json.contains(r#""read_file""#)); let decoded: WsOutbound = serde_json::from_str(&json).unwrap(); match decoded { WsOutbound::SessionHistory { messages, .. } => { let calls = messages[0].tool_calls.as_ref().unwrap(); assert_eq!(calls[0].name, "read_file"); assert_eq!(calls[0].arguments["path"], "README.md"); } other => panic!("unexpected decoded variant: {other:?}"), } } #[test] fn test_session_plan_protocol_is_structured_and_versioned() { let inbound = WsInbound::GetSessionPlan { session_id: "cli_chat:client:dialog".to_string(), }; assert!( serde_json::to_string(&inbound) .unwrap() .contains(r#""type":"get_session_plan""#) ); let plan = TaskPlan { id: "plan-1".to_string(), session_id: "cli_chat:client:dialog".to_string(), objective: "实现 Todo".to_string(), status: "active".to_string(), version: 3, created_at: 1, updated_at: 2, closed_at: None, items: vec![TaskItem { id: "T1".to_string(), ordinal: 1, title: "实现协议".to_string(), status: "in_progress".to_string(), executor_kind: Some("sub_agent".to_string()), execution_id: Some("run-1".to_string()), result_summary: None, error: None, version: 2, updated_at: 2, }], }; let outbound = WsOutbound::PlanUpdated { session_id: plan.session_id.clone(), reason: "item_assigned".to_string(), changed_item_ids: vec!["T1".to_string()], plan: Some(plan), }; let decoded: WsOutbound = serde_json::from_str(&serde_json::to_string(&outbound).unwrap()).unwrap(); match decoded { WsOutbound::PlanUpdated { plan, reason, .. } => { let plan = plan.unwrap(); assert_eq!(plan.version, 3); assert_eq!(plan.items[0].execution_id.as_deref(), Some("run-1")); assert_eq!(reason, "item_assigned"); } other => panic!("unexpected decoded variant: {other:?}"), } }