179 lines
4.9 KiB
Rust
179 lines
4.9 KiB
Rust
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
|
|
use ai::diff_validation::ParsedDiff;
|
|
use ai::skills::{SkillPathOrigin, SkillReference};
|
|
use galaxy_agent_core::ToolCall;
|
|
|
|
use super::{action_from_tool_call, MCPToolTarget};
|
|
use crate::ai::agent::{AIAgentActionType, FileEdit};
|
|
|
|
fn call(name: &str, arguments: serde_json::Value) -> ToolCall {
|
|
ToolCall {
|
|
id: "call-1".to_string(),
|
|
name: name.to_string(),
|
|
arguments,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn shell_calls_become_domain_actions_without_a_proto_round_trip() {
|
|
let action = action_from_tool_call(
|
|
"task-1",
|
|
&call(
|
|
"run_shell_command",
|
|
serde_json::json!({
|
|
"command": "cargo test",
|
|
"is_read_only": true,
|
|
"is_risky": false
|
|
}),
|
|
),
|
|
&SkillPathOrigin::Local,
|
|
&HashMap::new(),
|
|
)
|
|
.unwrap();
|
|
|
|
assert_eq!(action.id.to_string(), "call-1");
|
|
assert_eq!(action.task_id.to_string(), "task-1");
|
|
assert!(matches!(
|
|
action.action,
|
|
AIAgentActionType::RequestCommandOutput {
|
|
command,
|
|
is_read_only: Some(true),
|
|
is_risky: Some(false),
|
|
..
|
|
} if command == "cargo test"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn edit_calls_preserve_file_edits_in_the_domain_model() {
|
|
let action = action_from_tool_call(
|
|
"task-1",
|
|
&call(
|
|
"apply_file_diffs",
|
|
serde_json::json!({
|
|
"summary": "Update greeting",
|
|
"diffs": [{
|
|
"file_path": "/tmp/greeting.txt",
|
|
"search": "hello",
|
|
"replace": "hello galaxy"
|
|
}]
|
|
}),
|
|
),
|
|
&SkillPathOrigin::Local,
|
|
&HashMap::new(),
|
|
)
|
|
.unwrap();
|
|
|
|
let AIAgentActionType::RequestFileEdits { file_edits, title } = action.action else {
|
|
panic!("expected file-edit action");
|
|
};
|
|
assert_eq!(title.as_deref(), Some("Update greeting"));
|
|
assert!(matches!(
|
|
&file_edits[0],
|
|
FileEdit::Edit(ParsedDiff::StrReplaceEdit {
|
|
file: Some(file),
|
|
search: Some(search),
|
|
replace: Some(replace),
|
|
}) if file == "/tmp/greeting.txt" && search == "hello" && replace == "hello galaxy"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn grouped_mcp_calls_keep_the_installation_uuid_and_json_input() {
|
|
let action = action_from_tool_call(
|
|
"task-1",
|
|
&call(
|
|
"mcp__11111111-1111-4111-8111-111111111111__echo",
|
|
serde_json::json!({"message": "hello"}),
|
|
),
|
|
&SkillPathOrigin::Local,
|
|
&HashMap::new(),
|
|
)
|
|
.unwrap();
|
|
|
|
assert!(matches!(
|
|
action.action,
|
|
AIAgentActionType::CallMCPTool {
|
|
server_id: Some(server_id),
|
|
name,
|
|
input,
|
|
} if server_id.to_string() == "11111111-1111-4111-8111-111111111111"
|
|
&& name == "echo"
|
|
&& input == serde_json::json!({"message": "hello"})
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn provider_safe_mcp_aliases_resolve_to_the_original_tool() {
|
|
let server_id = uuid::Uuid::parse_str("10804e3a-859e-4474-bf89-80e98d1dd086").unwrap();
|
|
let alias = "mcp__performance_analyze_insight__0123456789abcdef";
|
|
let aliases = HashMap::from([(
|
|
alias.to_string(),
|
|
MCPToolTarget {
|
|
server_id: Some(server_id),
|
|
name: "performance_analyze_insight".to_string(),
|
|
},
|
|
)]);
|
|
|
|
let action = action_from_tool_call(
|
|
"task-1",
|
|
&call(alias, serde_json::json!({"trace_id": "trace-1"})),
|
|
&SkillPathOrigin::Local,
|
|
&aliases,
|
|
)
|
|
.unwrap();
|
|
|
|
assert!(matches!(
|
|
action.action,
|
|
AIAgentActionType::CallMCPTool {
|
|
server_id: Some(actual_server_id),
|
|
name,
|
|
input,
|
|
} if actual_server_id == server_id
|
|
&& name == "performance_analyze_insight"
|
|
&& input == serde_json::json!({"trace_id": "trace-1"})
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn local_skill_paths_preserve_the_session_origin() {
|
|
let action = action_from_tool_call(
|
|
"task-1",
|
|
&call(
|
|
"read_skill",
|
|
serde_json::json!({
|
|
"skill": "/tmp/example/SKILL.md",
|
|
"reference_type": "path"
|
|
}),
|
|
),
|
|
&SkillPathOrigin::Local,
|
|
&HashMap::new(),
|
|
)
|
|
.unwrap();
|
|
|
|
assert!(matches!(
|
|
action.action,
|
|
AIAgentActionType::ReadSkill(request)
|
|
if request.skill == SkillReference::Path(
|
|
galaxy_util::local_or_remote_path::LocalOrRemotePath::Local(PathBuf::from(
|
|
"/tmp/example/SKILL.md"
|
|
))
|
|
)
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn unknown_tools_are_rejected_before_the_permission_boundary() {
|
|
let error = action_from_tool_call(
|
|
"task-1",
|
|
&call("invented_tool", serde_json::json!({})),
|
|
&SkillPathOrigin::Local,
|
|
&HashMap::new(),
|
|
)
|
|
.unwrap_err();
|
|
|
|
assert!(error.contains("unsupported Rig tool proposal"));
|
|
}
|