262 lines
8.7 KiB
Rust
262 lines
8.7 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(),
|
|
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()],
|
|
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(),
|
|
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""#));
|
|
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:?}"),
|
|
}
|
|
}
|