253 lines
7.4 KiB
Rust
253 lines
7.4 KiB
Rust
use std::sync::{Arc, Mutex};
|
|
|
|
use ai::skills::SkillPathOrigin;
|
|
use galaxy_agent_core::{
|
|
ContentPart, MessageContent, MessageRole, StopReason, ToolCall, ToolResult, ToolResultStatus,
|
|
};
|
|
use warp_multi_agent_api::response_event::stream_finished;
|
|
|
|
use super::{
|
|
append_tool_result, build_add_reasoning, build_append_reasoning, build_tool_proposed,
|
|
map_stop_reason, saturating_i32, sync_assistant_turn,
|
|
};
|
|
|
|
#[test]
|
|
fn stop_reasons_map_to_the_existing_ui_contract() {
|
|
assert!(matches!(
|
|
map_stop_reason(StopReason::Completed),
|
|
stream_finished::Reason::Done(_)
|
|
));
|
|
assert!(matches!(
|
|
map_stop_reason(StopReason::MaxTokens),
|
|
stream_finished::Reason::MaxTokenLimit(_)
|
|
));
|
|
assert!(matches!(
|
|
map_stop_reason(StopReason::Cancelled),
|
|
stream_finished::Reason::Other(_)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn token_counts_saturate_at_the_proto_limit() {
|
|
assert_eq!(saturating_i32(u64::MAX), i32::MAX);
|
|
}
|
|
|
|
#[test]
|
|
fn reasoning_events_match_the_existing_ui_message_contract() {
|
|
let add = build_add_reasoning("task", "message", "think");
|
|
let append = build_append_reasoning("task", "message", " more");
|
|
|
|
let Some(warp_multi_agent_api::response_event::Type::ClientActions(add)) = add.r#type else {
|
|
panic!("expected client actions");
|
|
};
|
|
let Some(warp_multi_agent_api::client_action::Action::AddMessagesToTask(add)) =
|
|
&add.actions[0].action
|
|
else {
|
|
panic!("expected add-message action");
|
|
};
|
|
assert!(matches!(
|
|
add.messages[0].message.as_ref(),
|
|
Some(warp_multi_agent_api::message::Message::AgentReasoning(reasoning))
|
|
if reasoning.reasoning == "think"
|
|
));
|
|
|
|
let Some(warp_multi_agent_api::response_event::Type::ClientActions(append)) = append.r#type
|
|
else {
|
|
panic!("expected client actions");
|
|
};
|
|
let Some(warp_multi_agent_api::client_action::Action::AppendToMessageContent(append)) =
|
|
&append.actions[0].action
|
|
else {
|
|
panic!("expected append-message action");
|
|
};
|
|
assert_eq!(
|
|
append.mask.as_ref().unwrap().paths,
|
|
["agent_reasoning.reasoning"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn tool_proposal_matches_the_domain_permission_contract() {
|
|
let action = build_tool_proposed(
|
|
"task",
|
|
&ToolCall {
|
|
id: "call-1".to_string(),
|
|
name: "run_shell_command".to_string(),
|
|
arguments: serde_json::json!({
|
|
"command": "cargo test",
|
|
"is_read_only": true
|
|
}),
|
|
},
|
|
&SkillPathOrigin::Local,
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(action.id.to_string(), "call-1");
|
|
assert!(matches!(
|
|
action.action,
|
|
crate::ai::agent::AIAgentActionType::RequestCommandOutput {
|
|
command,
|
|
is_read_only: Some(true),
|
|
..
|
|
} if command == "cargo test"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn mcp_tool_proposal_routes_directly_to_the_mcp_executor_contract() {
|
|
let action = build_tool_proposed(
|
|
"task",
|
|
&ToolCall {
|
|
id: "call-mcp".to_string(),
|
|
name: "mcp__11111111-1111-4111-8111-111111111111__read_file".to_string(),
|
|
arguments: serde_json::json!({"path": "Cargo.toml"}),
|
|
},
|
|
&SkillPathOrigin::Local,
|
|
)
|
|
.unwrap();
|
|
|
|
assert!(matches!(
|
|
action.action,
|
|
crate::ai::agent::AIAgentActionType::CallMCPTool {
|
|
server_id: Some(server_id),
|
|
name,
|
|
..
|
|
} if server_id.to_string() == "11111111-1111-4111-8111-111111111111"
|
|
&& name == "read_file"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn assistant_history_is_updated_before_fast_tool_execution_can_continue() {
|
|
let messages = Arc::new(Mutex::new(Vec::new()));
|
|
let mut history_index = None;
|
|
let first_call = ToolCall {
|
|
id: "call-1".to_string(),
|
|
name: "read_files".to_string(),
|
|
arguments: serde_json::json!({"files": ["Cargo.toml"]}),
|
|
};
|
|
let second_call = ToolCall {
|
|
id: "call-2".to_string(),
|
|
name: "grep".to_string(),
|
|
arguments: serde_json::json!({"queries": ["rig"]}),
|
|
};
|
|
|
|
sync_assistant_turn(
|
|
&messages,
|
|
"",
|
|
None,
|
|
"I'll inspect both.",
|
|
std::slice::from_ref(&first_call),
|
|
&mut history_index,
|
|
);
|
|
sync_assistant_turn(
|
|
&messages,
|
|
"",
|
|
None,
|
|
"I'll inspect both.",
|
|
&[first_call, second_call],
|
|
&mut history_index,
|
|
);
|
|
|
|
let messages = messages.lock().unwrap();
|
|
assert_eq!(messages.len(), 1);
|
|
let MessageContent::MultiPart(parts) = &messages[0].content else {
|
|
panic!("expected combined assistant content");
|
|
};
|
|
assert_eq!(parts.len(), 3);
|
|
assert!(
|
|
matches!(&parts[0], galaxy_agent_core::ContentPart::Text(text) if text == "I'll inspect both.")
|
|
);
|
|
assert!(
|
|
matches!(&parts[1], galaxy_agent_core::ContentPart::ToolUse { tool_use_id, .. } if tool_use_id == "call-1")
|
|
);
|
|
assert!(
|
|
matches!(&parts[2], galaxy_agent_core::ContentPart::ToolUse { tool_use_id, .. } if tool_use_id == "call-2")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn signed_reasoning_is_persisted_before_the_tool_call() {
|
|
let messages = Arc::new(Mutex::new(Vec::new()));
|
|
let mut history_index = None;
|
|
let call = ToolCall {
|
|
id: "call-1".to_string(),
|
|
name: "read_files".to_string(),
|
|
arguments: serde_json::json!({"files": ["Cargo.toml"]}),
|
|
};
|
|
|
|
sync_assistant_turn(
|
|
&messages,
|
|
"I should inspect the manifest.",
|
|
Some("signed-reasoning"),
|
|
"",
|
|
std::slice::from_ref(&call),
|
|
&mut history_index,
|
|
);
|
|
|
|
let messages = messages.lock().unwrap();
|
|
let MessageContent::MultiPart(parts) = &messages[0].content else {
|
|
panic!("expected reasoning and tool call parts");
|
|
};
|
|
assert!(matches!(
|
|
parts.as_slice(),
|
|
[
|
|
ContentPart::Reasoning {
|
|
text,
|
|
signature: Some(signature),
|
|
},
|
|
ContentPart::ToolUse { tool_use_id, .. },
|
|
] if text == "I should inspect the manifest."
|
|
&& signature == "signed-reasoning"
|
|
&& tool_use_id == "call-1"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn rejected_tool_result_is_paired_with_the_assistant_call_in_history() {
|
|
let messages = Arc::new(Mutex::new(Vec::new()));
|
|
let mut history_index = None;
|
|
let call = ToolCall {
|
|
id: "call-unknown".to_string(),
|
|
name: "invented_tool".to_string(),
|
|
arguments: serde_json::json!({}),
|
|
};
|
|
sync_assistant_turn(
|
|
&messages,
|
|
"",
|
|
None,
|
|
"",
|
|
std::slice::from_ref(&call),
|
|
&mut history_index,
|
|
);
|
|
append_tool_result(
|
|
&messages,
|
|
ToolResult {
|
|
call_id: call.id.clone(),
|
|
content: "tool is unavailable".to_string(),
|
|
status: ToolResultStatus::Error,
|
|
},
|
|
);
|
|
|
|
let messages = messages.lock().unwrap();
|
|
assert_eq!(messages.len(), 2);
|
|
assert_eq!(messages[0].role, MessageRole::Assistant);
|
|
assert!(matches!(
|
|
&messages[0].content,
|
|
MessageContent::ToolUse {
|
|
tool_use_id,
|
|
name,
|
|
..
|
|
} if tool_use_id == "call-unknown" && name == "invented_tool"
|
|
));
|
|
assert_eq!(messages[1].role, MessageRole::User);
|
|
assert!(matches!(
|
|
&messages[1].content,
|
|
MessageContent::ToolResult {
|
|
tool_use_id,
|
|
content,
|
|
is_error: true,
|
|
} if tool_use_id == "call-unknown" && content == "tool is unavailable"
|
|
));
|
|
}
|