PicoBot/tests/test_request_format.rs

268 lines
9.0 KiB
Rust

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(),
reasoning_content: None,
completion_status: picobot::bus::CompletionStatus::Completed,
created_at: 123,
tool_call_id: None,
tool_name: None,
tool_calls: None,
attachments: Vec::new(),
}],
};
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_user_input_accepts_upload_ids_and_old_payloads() {
let old: WsInbound =
serde_json::from_str(r#"{"type":"user_input","content":"hello"}"#).unwrap();
match old {
WsInbound::UserInput { upload_ids, .. } => assert!(upload_ids.is_empty()),
other => panic!("unexpected decoded variant: {other:?}"),
}
let message = WsInbound::UserInput {
content: "处理文件".to_string(),
upload_ids: vec!["upload-1".to_string()],
client_message_id: None,
channel: None,
chat_id: None,
sender_id: None,
};
let json = serde_json::to_string(&message).unwrap();
assert!(json.contains(r#""upload_ids":["upload-1"]"#));
}
#[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(),
reasoning_content: Some("checking".to_string()),
completion_status: picobot::bus::CompletionStatus::Completed,
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" }),
}]),
attachments: Vec::new(),
}],
};
let json = serde_json::to_string(&outbound).unwrap();
assert!(json.contains(r#""tool_calls""#));
assert!(json.contains(r#""read_file""#));
assert!(json.contains(r#""reasoning_content":"checking""#));
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:?}"),
}
}