aether_project/
testing.rs1use std::fs;
7use std::path::{Path, PathBuf};
8
9use serde_json::{Value, json};
10use tempfile::TempDir;
11
12use crate::{AgentConfig, PromptSource, SKILL_FILENAME};
13use aether_core::agent_spec::{AgentSpec, AgentSpecExposure};
14use llm::{ModelSettings, ProviderConnectionOverrides};
15use mcp_utils::client::ToolFilter;
16
17pub const DEFAULT_MODEL: &str = "anthropic:claude-sonnet-4-5";
19
20pub fn project() -> TestProject {
22 TestProject { root: temp_dir("project") }
23}
24
25pub fn home() -> TestHome {
27 TestHome { root: temp_dir("home") }
28}
29
30pub fn settings_agent(name: &str, description: &str) -> AgentConfig {
32 AgentConfig {
33 name: name.to_string(),
34 description: description.to_string(),
35 model: DEFAULT_MODEL.to_string(),
36 user_invocable: true,
37 ..AgentConfig::default()
38 }
39}
40
41pub fn agent_json(name: &str, description: &str) -> Value {
44 json!({
45 "name": name,
46 "description": description,
47 "model": DEFAULT_MODEL,
48 "userInvocable": true,
49 })
50}
51
52pub fn agent_json_with(name: &str, description: &str, extra: Value) -> Value {
55 let Value::Object(extra) = extra else {
56 panic!("extra agent fields must be a JSON object, got {extra}");
57 };
58 let mut agent = agent_json(name, description);
59 agent.as_object_mut().expect("agent_json builds an object").extend(extra);
60 agent
61}
62
63pub fn agent_config(name: &str) -> AgentConfig {
65 AgentConfig { prompts: vec![PromptSource::file("PROMPT.md")], ..settings_agent(name, &format!("{name} agent")) }
66}
67
68pub fn fake_spec(name: &str, exposure: AgentSpecExposure) -> AgentSpec {
70 AgentSpec {
71 name: name.to_string(),
72 description: format!("{name} agent"),
73 model: DEFAULT_MODEL.to_string(),
74 reasoning_effort: None,
75 model_settings: ModelSettings::default(),
76 context_window: None,
77 prompts: vec![],
78 provider_connections: ProviderConnectionOverrides::default(),
79 mcp_config_sources: Vec::new(),
80 exposure,
81 tools: ToolFilter::default(),
82 }
83}
84
85pub struct TestProject {
87 root: TempDir,
88}
89
90impl TestProject {
91 pub fn root(&self) -> &Path {
93 self.root.path()
94 }
95
96 pub fn file(self, path: &str, content: &str) -> Self {
98 self.write(path, content);
99 self
100 }
101
102 pub fn skill(self, name: &str, content: &str) -> Self {
104 self.file(&format!("{name}/{SKILL_FILENAME}"), content)
105 }
106
107 pub fn write(&self, path: &str, content: &str) {
109 write_file(self.root.path(), path, content);
110 }
111}
112
113pub struct TestHome {
115 root: TempDir,
116}
117
118impl TestHome {
119 pub fn aether(&self) -> PathBuf {
121 self.root.path().join(".aether")
122 }
123
124 pub fn root(&self) -> &Path {
126 self.root.path()
127 }
128
129 pub fn file(self, path: &str, content: &str) -> Self {
131 write_file(self.root.path(), path, content);
132 self
133 }
134
135 pub fn settings(self, json: &str) -> Self {
137 self.file(".aether/settings.json", json)
138 }
139}
140
141fn temp_dir(label: &str) -> TempDir {
142 TempDir::new().unwrap_or_else(|error| panic!("failed to create temp {label} dir: {error}"))
143}
144
145fn write_file(root: &Path, path: &str, content: &str) {
146 let full_path = root.join(path);
147 if let Some(parent) = full_path.parent() {
148 fs::create_dir_all(parent).expect("failed to create fixture parent directories");
149 }
150
151 fs::write(full_path, content).expect("failed to write fixture file");
152}