use std::path::{Path, PathBuf};
use serde_json::{Value, json};
use crate::prompt_file::{PromptFile, PromptTriggers};
use crate::{AetherSettings, AgentConfig, McpSourceSpec, PromptSource};
use aether_core::agent_spec::{AgentSpec, AgentSpecExposure};
use aether_core::core::Prompt;
pub use utils::testing::TestWorkspace;
pub const DEFAULT_MODEL: &str = "anthropic:claude-sonnet-4-5";
pub fn mcp_json(server: &str) -> String {
json!({ "servers": { server: { "command": format!("{server}-server") } } }).to_string()
}
pub fn project() -> TestWorkspace {
TestWorkspace::new()
}
pub fn home() -> TestWorkspace {
TestWorkspace::new()
}
pub fn settings_agent(name: &str, description: &str) -> AgentConfig {
AgentConfig {
name: name.to_string(),
description: description.to_string(),
model: DEFAULT_MODEL.to_string(),
user_invocable: true,
..AgentConfig::default()
}
}
pub fn agent_json(name: &str, description: &str) -> Value {
json!({
"name": name,
"description": description,
"model": DEFAULT_MODEL,
"userInvocable": true,
})
}
pub fn agent_json_with(name: &str, description: &str, extra: Value) -> Value {
let Value::Object(extra) = extra else {
panic!("extra agent fields must be a JSON object, got {extra}");
};
let mut agent = agent_json(name, description);
agent.as_object_mut().expect("agent_json builds an object").extend(extra);
agent
}
pub fn agent_config(name: &str) -> AgentConfig {
AgentConfig { prompts: vec![PromptSource::file("PROMPT.md")], ..settings_agent(name, &format!("{name} agent")) }
}
pub fn settings_with_mcps(mcps: Vec<McpSourceSpec>) -> AetherSettings {
AetherSettings { agents: vec![AgentConfig { mcps, ..agent_config("alpha") }], ..AetherSettings::default() }
}
pub fn has_prompt_file(spec: &AgentSpec, expected: impl AsRef<Path>) -> bool {
spec.prompts.iter().any(|prompt| matches!(prompt, Prompt::File { path, .. } if path.ends_with(&expected)))
}
pub fn prompt_file(name: &str) -> PromptFileBuilder {
PromptFileBuilder {
file: PromptFile {
name: name.to_string(),
description: format!("{name} skill"),
body: format!("# {name}\n\nSkill body."),
path: PathBuf::new(),
user_invocable: true,
agent_invocable: true,
argument_hint: None,
tags: Vec::new(),
triggers: PromptTriggers::default(),
agent_authored: false,
helpful: 0,
harmful: 0,
},
}
}
pub fn fake_spec(name: &str, exposure: AgentSpecExposure) -> AgentSpec {
AgentSpec {
name: name.to_string(),
description: format!("{name} agent"),
model: DEFAULT_MODEL.to_string(),
exposure,
..AgentSpec::default()
}
}
pub struct PromptFileBuilder {
file: PromptFile,
}
impl PromptFileBuilder {
pub fn description(mut self, description: &str) -> Self {
self.file.description = description.to_string();
self
}
pub fn body(mut self, body: &str) -> Self {
self.file.body = body.to_string();
self
}
pub fn user_invocable(mut self, user_invocable: bool) -> Self {
self.file.user_invocable = user_invocable;
self
}
pub fn agent_invocable(mut self, agent_invocable: bool) -> Self {
self.file.agent_invocable = agent_invocable;
self
}
pub fn argument_hint(mut self, argument_hint: &str) -> Self {
self.file.argument_hint = Some(argument_hint.to_string());
self
}
pub fn tags(mut self, tags: &[&str]) -> Self {
self.file.tags = tags.iter().map(ToString::to_string).collect();
self
}
pub fn read_triggers(mut self, patterns: &[&str]) -> Self {
self.file.triggers = PromptTriggers::new(patterns.iter().map(ToString::to_string).collect())
.expect("read_triggers patterns must be valid globs");
self
}
pub fn agent_authored(mut self, agent_authored: bool) -> Self {
self.file.agent_authored = agent_authored;
self
}
pub fn ratings(mut self, helpful: u32, harmful: u32) -> Self {
self.file.helpful = helpful;
self.file.harmful = harmful;
self
}
pub fn build(self) -> PromptFile {
self.file
}
}