use std::fs;
use std::path::{Path, PathBuf};
use serde_json::{Value, json};
use tempfile::TempDir;
use crate::{AgentConfig, PromptSource, SKILL_FILENAME};
use aether_core::agent_spec::{AgentSpec, AgentSpecExposure};
use llm::{ModelSettings, ProviderConnectionOverrides};
use mcp_utils::client::ToolFilter;
pub const DEFAULT_MODEL: &str = "anthropic:claude-sonnet-4-5";
pub fn project() -> TestProject {
TestProject { root: temp_dir("project") }
}
pub fn home() -> TestHome {
TestHome { root: temp_dir("home") }
}
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 fake_spec(name: &str, exposure: AgentSpecExposure) -> AgentSpec {
AgentSpec {
name: name.to_string(),
description: format!("{name} agent"),
model: DEFAULT_MODEL.to_string(),
reasoning_effort: None,
model_settings: ModelSettings::default(),
context_window: None,
prompts: vec![],
provider_connections: ProviderConnectionOverrides::default(),
mcp_config_sources: Vec::new(),
exposure,
tools: ToolFilter::default(),
}
}
pub struct TestProject {
root: TempDir,
}
impl TestProject {
pub fn root(&self) -> &Path {
self.root.path()
}
pub fn file(self, path: &str, content: &str) -> Self {
self.write(path, content);
self
}
pub fn skill(self, name: &str, content: &str) -> Self {
self.file(&format!("{name}/{SKILL_FILENAME}"), content)
}
pub fn write(&self, path: &str, content: &str) {
write_file(self.root.path(), path, content);
}
}
pub struct TestHome {
root: TempDir,
}
impl TestHome {
pub fn aether(&self) -> PathBuf {
self.root.path().join(".aether")
}
pub fn root(&self) -> &Path {
self.root.path()
}
pub fn file(self, path: &str, content: &str) -> Self {
write_file(self.root.path(), path, content);
self
}
pub fn settings(self, json: &str) -> Self {
self.file(".aether/settings.json", json)
}
}
fn temp_dir(label: &str) -> TempDir {
TempDir::new().unwrap_or_else(|error| panic!("failed to create temp {label} dir: {error}"))
}
fn write_file(root: &Path, path: &str, content: &str) {
let full_path = root.join(path);
if let Some(parent) = full_path.parent() {
fs::create_dir_all(parent).expect("failed to create fixture parent directories");
}
fs::write(full_path, content).expect("failed to write fixture file");
}