aether_project/
testing.rs1use std::fs;
7use std::path::{Path, PathBuf};
8
9use serde_json::{Value, json};
10use tempfile::TempDir;
11
12use crate::prompt_file::{PromptFile, PromptTriggers};
13use crate::{AgentConfig, PromptSource, SKILL_FILENAME};
14use aether_core::agent_spec::{AgentSpec, AgentSpecExposure};
15use llm::{ModelSettings, ProviderConnectionOverrides};
16use mcp_utils::client::ToolFilter;
17
18pub const DEFAULT_MODEL: &str = "anthropic:claude-sonnet-4-5";
20
21pub fn project() -> TestProject {
23 TestProject { root: temp_dir("project") }
24}
25
26pub fn home() -> TestHome {
28 TestHome { root: temp_dir("home") }
29}
30
31pub fn settings_agent(name: &str, description: &str) -> AgentConfig {
33 AgentConfig {
34 name: name.to_string(),
35 description: description.to_string(),
36 model: DEFAULT_MODEL.to_string(),
37 user_invocable: true,
38 ..AgentConfig::default()
39 }
40}
41
42pub fn agent_json(name: &str, description: &str) -> Value {
45 json!({
46 "name": name,
47 "description": description,
48 "model": DEFAULT_MODEL,
49 "userInvocable": true,
50 })
51}
52
53pub fn agent_json_with(name: &str, description: &str, extra: Value) -> Value {
56 let Value::Object(extra) = extra else {
57 panic!("extra agent fields must be a JSON object, got {extra}");
58 };
59 let mut agent = agent_json(name, description);
60 agent.as_object_mut().expect("agent_json builds an object").extend(extra);
61 agent
62}
63
64pub fn agent_config(name: &str) -> AgentConfig {
66 AgentConfig { prompts: vec![PromptSource::file("PROMPT.md")], ..settings_agent(name, &format!("{name} agent")) }
67}
68
69pub fn prompt_file(name: &str) -> PromptFileBuilder {
72 PromptFileBuilder {
73 file: PromptFile {
74 name: name.to_string(),
75 description: format!("{name} skill"),
76 body: format!("# {name}\n\nSkill body."),
77 path: PathBuf::new(),
78 user_invocable: true,
79 agent_invocable: true,
80 argument_hint: None,
81 tags: Vec::new(),
82 triggers: PromptTriggers::default(),
83 agent_authored: false,
84 helpful: 0,
85 harmful: 0,
86 },
87 }
88}
89
90pub fn fake_spec(name: &str, exposure: AgentSpecExposure) -> AgentSpec {
92 AgentSpec {
93 name: name.to_string(),
94 description: format!("{name} agent"),
95 model: DEFAULT_MODEL.to_string(),
96 reasoning_effort: None,
97 model_settings: ModelSettings::default(),
98 context_window: None,
99 prompts: vec![],
100 provider_connections: ProviderConnectionOverrides::default(),
101 mcp_config_sources: Vec::new(),
102 exposure,
103 tools: ToolFilter::default(),
104 }
105}
106
107pub struct TestProject {
109 root: TempDir,
110}
111
112impl TestProject {
113 pub fn root(&self) -> &Path {
115 self.root.path()
116 }
117
118 pub fn file(self, path: &str, content: &str) -> Self {
120 self.write(path, content);
121 self
122 }
123
124 pub fn skill(self, name: &str, content: &str) -> Self {
126 self.file(&format!("{name}/{SKILL_FILENAME}"), content)
127 }
128
129 pub fn write(&self, path: &str, content: &str) {
131 write_file(self.root.path(), path, content);
132 }
133}
134
135pub struct TestHome {
137 root: TempDir,
138}
139
140impl TestHome {
141 pub fn aether(&self) -> PathBuf {
143 self.root.path().join(".aether")
144 }
145
146 pub fn root(&self) -> &Path {
148 self.root.path()
149 }
150
151 pub fn file(self, path: &str, content: &str) -> Self {
153 write_file(self.root.path(), path, content);
154 self
155 }
156
157 pub fn settings(self, json: &str) -> Self {
159 self.file(".aether/settings.json", json)
160 }
161}
162
163pub struct PromptFileBuilder {
165 file: PromptFile,
166}
167
168impl PromptFileBuilder {
169 pub fn description(mut self, description: &str) -> Self {
171 self.file.description = description.to_string();
172 self
173 }
174
175 pub fn body(mut self, body: &str) -> Self {
177 self.file.body = body.to_string();
178 self
179 }
180
181 pub fn user_invocable(mut self, user_invocable: bool) -> Self {
183 self.file.user_invocable = user_invocable;
184 self
185 }
186
187 pub fn agent_invocable(mut self, agent_invocable: bool) -> Self {
189 self.file.agent_invocable = agent_invocable;
190 self
191 }
192
193 pub fn argument_hint(mut self, argument_hint: &str) -> Self {
195 self.file.argument_hint = Some(argument_hint.to_string());
196 self
197 }
198
199 pub fn tags(mut self, tags: &[&str]) -> Self {
201 self.file.tags = tags.iter().map(ToString::to_string).collect();
202 self
203 }
204
205 pub fn read_triggers(mut self, patterns: &[&str]) -> Self {
207 self.file.triggers = PromptTriggers::new(patterns.iter().map(ToString::to_string).collect())
208 .expect("read_triggers patterns must be valid globs");
209 self
210 }
211
212 pub fn agent_authored(mut self, agent_authored: bool) -> Self {
214 self.file.agent_authored = agent_authored;
215 self
216 }
217
218 pub fn ratings(mut self, helpful: u32, harmful: u32) -> Self {
220 self.file.helpful = helpful;
221 self.file.harmful = harmful;
222 self
223 }
224
225 pub fn build(self) -> PromptFile {
227 self.file
228 }
229}
230
231fn temp_dir(label: &str) -> TempDir {
232 TempDir::new().unwrap_or_else(|error| panic!("failed to create temp {label} dir: {error}"))
233}
234
235fn write_file(root: &Path, path: &str, content: &str) {
236 let full_path = root.join(path);
237 if let Some(parent) = full_path.parent() {
238 fs::create_dir_all(parent).expect("failed to create fixture parent directories");
239 }
240
241 fs::write(full_path, content).expect("failed to write fixture file");
242}