use std::{collections::HashMap, path::Path};
use super::*;
use crate::delegate::{
adapters::{OutputFormat, Resume},
records::DelegationRegistry,
};
fn delegation_context(home: &Path) -> DelegationContext {
DelegationContext {
registry: Arc::new(DelegationRegistry::new(&scv_client::Layout::new(home))),
session: "session-1".into(),
depth: 0,
}
}
#[test]
fn uninstalled_agents_are_not_offered() {
let adapter = |command: &str| AgentAdapterConfig {
command: command.into(),
args: Vec::new(),
prompt_args: Vec::new(),
full_permission_args: None,
model_args: Vec::new(),
effort_args: Vec::new(),
model_hint: String::new(),
environment: Vec::new(),
search_dirs: Vec::new(),
output: OutputFormat::Text,
resume: Resume::Unsupported,
home: None,
transport: Transport::Process,
acp: None,
use_for: None,
model: None,
effort: None,
options_file: None,
};
let registry = builtin_registry(
ToolsConfig::default(),
SkillMap::new(),
Vec::new(),
1024,
HashMap::from([
("present".to_owned(), adapter("bash")),
(
"missing".to_owned(),
adapter("scv-test-agent-that-is-not-installed"),
),
]),
)
.unwrap();
assert_eq!(crate::offered_agents(®istry), ["present"]);
let registry = builtin_registry(
ToolsConfig::default(),
SkillMap::new(),
Vec::new(),
1024,
HashMap::from([(
"missing".to_owned(),
adapter("scv-test-agent-that-is-not-installed"),
)]),
)
.unwrap();
for tool in ["agent", "agent_wait", "agent_status", "agent_cancel"] {
assert!(registry.get(tool).is_none(), "{tool} offered");
}
assert!(registry.get("bash").is_some());
}
#[test]
fn agents_are_not_offered_at_the_delegation_depth_limit() {
let adapter = AgentAdapterConfig {
command: "bash".into(),
args: Vec::new(),
prompt_args: Vec::new(),
full_permission_args: None,
model_args: Vec::new(),
effort_args: Vec::new(),
model_hint: String::new(),
environment: Vec::new(),
search_dirs: Vec::new(),
output: OutputFormat::Text,
resume: Resume::Unsupported,
home: None,
transport: Transport::Process,
acp: None,
use_for: None,
model: None,
effort: None,
options_file: None,
};
let home = tempfile::tempdir().unwrap();
for (max_depth, offered) in [(0, false), (1, true)] {
let registry = builtin_registry(
ToolsConfig {
max_delegation_depth: max_depth,
delegation: Some(delegation_context(home.path())),
..ToolsConfig::default()
},
SkillMap::new(),
Vec::new(),
1024,
HashMap::from([("claude".to_owned(), adapter.clone())]),
)
.unwrap();
assert_eq!(registry.get("agent").is_some(), offered);
assert!(registry.get("bash").is_some());
}
for (declared, max_depth, offered) in [(1, 1, false), (1, 2, true), (5, 2, false)] {
let registry = builtin_registry(
ToolsConfig {
max_delegation_depth: max_depth,
delegation: Some(DelegationContext {
depth: declared,
..delegation_context(home.path())
}),
..ToolsConfig::default()
},
SkillMap::new(),
Vec::new(),
1024,
HashMap::from([("claude".to_owned(), adapter.clone())]),
)
.unwrap();
assert_eq!(
registry.get("agent").is_some(),
offered,
"declared {declared}, limit {max_depth}"
);
}
}
fn installed(model_args: bool) -> AgentAdapterConfig {
AgentAdapterConfig {
command: "bash".into(),
args: Vec::new(),
prompt_args: Vec::new(),
full_permission_args: None,
model_args: if model_args {
vec!["--model".into(), "{model}".into()]
} else {
Vec::new()
},
effort_args: Vec::new(),
model_hint: "Model ID.".into(),
environment: Vec::new(),
search_dirs: Vec::new(),
output: OutputFormat::Text,
resume: Resume::Unsupported,
home: None,
transport: Transport::Process,
acp: None,
use_for: None,
model: None,
effort: None,
options_file: None,
}
}
#[test]
fn one_agent_tool_offers_every_installed_agent_with_the_preferred_one_as_default() {
let agents = || {
HashMap::from([
("claude".to_owned(), installed(true)),
("dsh".to_owned(), installed(false)),
])
};
let preferring = |prefer: &[&str], max_background| {
builtin_registry(
ToolsConfig {
prefer: prefer.iter().map(|name| (*name).to_owned()).collect(),
max_background,
..ToolsConfig::default()
},
SkillMap::new(),
Vec::new(),
1024,
agents(),
)
.unwrap()
};
let registry = preferring(&["pi", "dsh"], 2);
let names: Vec<String> = registry.specs().into_iter().map(|spec| spec.name).collect();
assert_eq!(
names,
[
"agent",
"agent_cancel",
"agent_status",
"agent_wait",
"bash",
"read",
"read_skill",
"write"
]
);
assert_eq!(crate::offered_agents(®istry), ["claude", "dsh"]);
let spec = registry.get("agent").unwrap().spec();
assert_eq!(spec.parameters["required"], serde_json::json!(["prompt"]));
assert!(
spec.parameters["properties"]["agent"]["description"]
.as_str()
.unwrap()
.starts_with("Which agent runs the task. Defaults to dsh,")
);
assert!(spec.parameters["properties"]["model"].is_object());
assert!(spec.parameters["properties"]["background"].is_object());
let spec = preferring(&["pi"], 2).get("agent").unwrap().spec();
assert_eq!(
spec.parameters["required"],
serde_json::json!(["agent", "prompt"])
);
let registry = preferring(&[], 0);
assert!(registry.get("agent_status").is_none());
let spec = registry.get("agent").unwrap().spec();
assert!(spec.parameters["properties"].get("background").is_none());
}
#[test]
fn an_acp_agent_lists_the_values_its_server_offered_and_says_so_when_unknown() {
use crate::delegate::options::{self, AgentOptions, Choice};
let dir = tempfile::tempdir().unwrap();
let server = dir.path().join("claude-agent-acp");
std::fs::write(&server, "#!/bin/sh\n").unwrap();
let file = dir.path().join("state/agent-options/claude.json");
let adapter = || {
let mut adapter = installed(true);
adapter.effort_args = vec!["--effort".into(), "{effort}".into()];
adapter.acp = Some(crate::AcpAgentLaunch {
command: server.display().to_string(),
args: Vec::new(),
full_mode: None,
environment: Vec::new(),
required: false,
});
adapter.options_file = Some(file.clone());
adapter
};
let spec = || {
builtin_registry(
ToolsConfig {
max_background: 0,
..ToolsConfig::default()
},
SkillMap::new(),
Vec::new(),
1024,
HashMap::from([("claude".to_owned(), adapter())]),
)
.unwrap()
.get("agent")
.unwrap()
.spec()
.parameters
};
let unknown = spec();
let line = unknown["properties"]["agent"]["description"]
.as_str()
.unwrap();
assert!(line.contains("which SCV has not seen yet"), "{line}");
assert!(
!line.contains("Model ID"),
"the CLI's hint does not apply: {line}"
);
let mut configured = adapter();
configured.effort = Some("turbo".into());
let efforts = builtin_registry(
ToolsConfig {
max_background: 0,
..ToolsConfig::default()
},
SkillMap::new(),
Vec::new(),
1024,
HashMap::from([("claude".to_owned(), configured)]),
)
.unwrap()
.get("agent")
.unwrap()
.spec()
.parameters["properties"]["effort"]["enum"]
.clone();
assert!(
efforts.as_array().unwrap().contains(&"turbo".into()),
"{efforts}"
);
options::save(
&file,
"claude",
&server,
&AgentOptions {
model: Some(Choice {
values: vec!["default".into(), "opus[1m]".into(), "sonnet".into()],
current: Some("default".into()),
}),
effort: Some(Choice {
values: vec!["default".into(), "high".into(), "ultra".into()],
current: Some("high".into()),
}),
},
)
.unwrap();
let known = spec();
let line = known["properties"]["agent"]["description"]
.as_str()
.unwrap();
assert!(
line.contains(
"Takes model (one of: opus[1m], sonnet), effort (one of: high, ultra; its default \
is high), and session."
),
"{line}"
);
let efforts = known["properties"]["effort"]["enum"].as_array().unwrap();
assert!(efforts.contains(&"ultra".into()), "{efforts:?}");
assert!(efforts.contains(&"max".into()), "{efforts:?}");
assert!(!efforts.contains(&"default".into()), "{efforts:?}");
options::save(
&file,
"claude",
&server,
&AgentOptions {
model: None,
effort: Some(Choice {
values: vec!["low".into()],
current: None,
}),
},
)
.unwrap();
let efforts_only = spec();
let line = efforts_only["properties"]["agent"]["description"]
.as_str()
.unwrap();
assert!(
line.contains("model (its ACP server lists no model to choose, so omit model)"),
"{line}"
);
}