#![allow(missing_docs)]
use std::collections::HashMap;
use serde_json::Value;
use vtcode::acp::permissions::{AcpPermissionPrompter, DefaultPermissionPrompter};
use vtcode::acp::reports::{
TOOL_PERMISSION_ALLOW_ALWAYS_OPTION_ID, TOOL_PERMISSION_ALLOW_OPTION_ID, TOOL_PERMISSION_CANCELLED_MESSAGE,
TOOL_PERMISSION_DENIED_MESSAGE, TOOL_PERMISSION_DENY_ALWAYS_OPTION_ID, TOOL_PERMISSION_DENY_OPTION_ID,
TOOL_PERMISSION_REQUEST_FAILURE_MESSAGE,
};
use vtcode::acp::tooling::{
SupportedTool, TOOL_LIST_FILES_MODE_ARG, TOOL_LIST_FILES_PATH_ARG, TOOL_READ_FILE_PATH_ARG, TOOL_READ_FILE_URI_ARG,
ToolDescriptor, ToolRegistryProvider,
};
#[path = "acp_fixtures.rs"]
mod acp_fixtures;
use acp_fixtures::{list_files_permission, read_file_permission};
#[derive(Clone)]
struct FakeRegistry {
descriptors: HashMap<String, ToolDescriptor>,
}
impl FakeRegistry {
fn new() -> Self {
let mut descriptors = HashMap::new();
descriptors
.insert(SupportedTool::ReadFile.function_name().to_string(), ToolDescriptor::Acp(SupportedTool::ReadFile));
descriptors.insert(
SupportedTool::ListFiles.function_name().to_string(),
ToolDescriptor::Acp(SupportedTool::ListFiles),
);
Self { descriptors }
}
fn render_read_file(&self, args: &Value) -> String {
args.get(TOOL_READ_FILE_PATH_ARG)
.or_else(|| args.get(TOOL_READ_FILE_URI_ARG))
.and_then(Value::as_str)
.map(|path| format!("Read file {path}"))
.unwrap_or_else(|| "Read file".to_string())
}
fn render_list_files(&self, args: &Value) -> String {
let scope = args
.get(TOOL_LIST_FILES_PATH_ARG)
.and_then(Value::as_str)
.filter(|value| !value.is_empty())
.map(|value| format!("{value}/"))
.unwrap_or_else(|| "workspace".to_string());
let mode = args.get(TOOL_LIST_FILES_MODE_ARG).and_then(Value::as_str).unwrap_or("list");
format!("List files in {scope} ({mode})")
}
}
impl ToolRegistryProvider for FakeRegistry {
fn registered_tools(&self) -> Vec<SupportedTool> {
self.descriptors
.values()
.filter_map(|descriptor| match descriptor {
ToolDescriptor::Acp(tool) => Some(*tool),
ToolDescriptor::Local => None,
})
.collect()
}
fn definitions_for(
&self,
_enabled_tools: &[SupportedTool],
_include_local: bool,
) -> Vec<vtcode_core::llm::provider::ToolDefinition> {
Vec::new()
}
fn render_title(&self, descriptor: ToolDescriptor, _function_name: &str, args: &Value) -> String {
match descriptor {
ToolDescriptor::Acp(SupportedTool::ReadFile) => self.render_read_file(args),
ToolDescriptor::Acp(SupportedTool::ListFiles) => self.render_list_files(args),
ToolDescriptor::Local => "Local tool".to_string(),
}
}
fn lookup(&self, function_name: &str) -> Option<ToolDescriptor> {
self.descriptors.get(function_name).copied()
}
fn has_local_tools(&self) -> bool {
false
}
}
fn prompter() -> DefaultPermissionPrompter<FakeRegistry> {
DefaultPermissionPrompter::new(FakeRegistry::new())
}
#[derive(Clone, Debug)]
#[allow(dead_code)]
struct TestOption {
option_id: String,
name: String,
kind: String,
}
fn collect_options(args: Option<&Value>, tool: SupportedTool) -> Vec<TestOption> {
let options = prompter().permission_options(tool, args);
options
.iter()
.map(|o| TestOption {
option_id: o.option_id.0.as_ref().to_string(),
name: o.name.clone(),
kind: format!("{:?}", o.kind),
})
.collect()
}
#[test]
fn permission_options_include_allow_once() {
let options = collect_options(None, SupportedTool::ReadFile);
assert!(options.iter().any(|o| o.option_id == TOOL_PERMISSION_ALLOW_OPTION_ID));
}
#[test]
fn permission_options_include_allow_always() {
let options = collect_options(None, SupportedTool::ReadFile);
assert!(options.iter().any(|o| o.option_id == TOOL_PERMISSION_ALLOW_ALWAYS_OPTION_ID));
}
#[test]
fn permission_options_include_deny_once() {
let options = collect_options(None, SupportedTool::ReadFile);
assert!(options.iter().any(|o| o.option_id == TOOL_PERMISSION_DENY_OPTION_ID));
}
#[test]
fn permission_options_include_deny_always() {
let options = collect_options(None, SupportedTool::ReadFile);
assert!(options.iter().any(|o| o.option_id == TOOL_PERMISSION_DENY_ALWAYS_OPTION_ID));
}
#[test]
fn permission_options_have_four_entries() {
let options = collect_options(None, SupportedTool::ReadFile);
assert_eq!(options.len(), 4);
}
#[test]
fn permission_options_render_read_file_action_label() {
let fixture = read_file_permission();
let options = collect_options(Some(&fixture.arguments), SupportedTool::ReadFile);
assert!(
options
.iter()
.any(|o| o.name.contains("Read file ") && o.name.starts_with("Allow"))
);
}
#[test]
fn permission_options_render_list_files_action_label() {
let fixture = list_files_permission();
let options = collect_options(Some(&fixture.arguments), SupportedTool::ListFiles);
assert!(
options
.iter()
.any(|o| o.name.contains("List files in") && o.name.starts_with("Allow"))
);
}
#[test]
fn permission_options_have_correct_option_ids() {
let options = collect_options(None, SupportedTool::ReadFile);
let option_ids: Vec<_> = options.iter().map(|o| o.option_id.as_str()).collect();
assert!(option_ids.contains(&TOOL_PERMISSION_ALLOW_OPTION_ID));
assert!(option_ids.contains(&TOOL_PERMISSION_ALLOW_ALWAYS_OPTION_ID));
assert!(option_ids.contains(&TOOL_PERMISSION_DENY_OPTION_ID));
assert!(option_ids.contains(&TOOL_PERMISSION_DENY_ALWAYS_OPTION_ID));
}
#[test]
fn action_label_uses_path_arg_for_read_file() {
let fixture = read_file_permission();
let args = &fixture.arguments;
assert!(
prompter()
.permission_options(SupportedTool::ReadFile, Some(args))
.iter()
.any(|o| o.name.contains("Read file ") && o.name.starts_with("Allow"))
);
}
#[test]
fn action_label_uses_uri_arg_fallback_for_read_file() {
let args = serde_json::json!({
"uri": "file:///path/to/document.txt"
});
assert!(
prompter()
.permission_options(SupportedTool::ReadFile, Some(&args))
.iter()
.any(|o| o.name.contains("/path/to/document.txt"))
);
}
#[test]
fn action_label_shows_workspace_root_for_empty_path() {
let args = serde_json::json!({
"path": ""
});
assert!(
prompter()
.permission_options(SupportedTool::ReadFile, Some(&args))
.iter()
.any(|o| o.name.contains("Read file"))
);
}
#[test]
fn action_label_defaults_to_tool_name_for_no_args() {
let options = collect_options(None, SupportedTool::ReadFile);
assert!(options.iter().any(|o| o.name.contains("Read file ")));
}
#[test]
fn named_permission_uses_custom_action_label() {
let action_label = "Read file src/main.rs";
let arguments = serde_json::json!({
TOOL_READ_FILE_PATH_ARG: "src/main.rs",
});
let options = prompter().permission_options(SupportedTool::ReadFile, Some(&arguments));
assert!(
options
.iter()
.any(|o| o.name.contains(action_label) && o.name.starts_with("Allow"))
);
}
#[test]
fn denied_message_contains_keyword() {
assert!(TOOL_PERMISSION_DENIED_MESSAGE.contains("denied"));
}
#[test]
fn cancelled_message_contains_keyword() {
assert!(TOOL_PERMISSION_CANCELLED_MESSAGE.contains("cancelled"));
}
#[test]
fn failure_message_contains_keyword() {
assert!(TOOL_PERMISSION_REQUEST_FAILURE_MESSAGE.contains("failed"));
}