Skip to main content

aether_project/
testing.rs

1//! Shared fixtures and fake builders for exercising `aether-project` in tests.
2//!
3//! Compiled for the crate's own tests and for any consumer that enables the
4//! `testing` feature. Prefer these over hand-rolling per-suite fixtures.
5
6use std::fs;
7use std::path::{Path, PathBuf};
8
9use tempfile::TempDir;
10
11use crate::{AgentConfig, PromptSource, SKILL_FILENAME};
12use aether_core::agent_spec::{AgentSpec, AgentSpecExposure};
13use llm::{ModelSettings, ProviderConnectionOverrides};
14use mcp_utils::client::ToolFilter;
15
16/// A temporary project directory for tests that need on-disk settings, prompts, or skills.
17pub fn project() -> TestProject {
18    TestProject { root: temp_dir("project") }
19}
20
21/// A temporary user home directory for tests that exercise `AETHER_HOME` lookups.
22pub fn home() -> TestHome {
23    TestHome { root: temp_dir("home") }
24}
25
26/// A minimal user-invocable [`AgentConfig`] as it would be parsed from `settings.json`.
27pub fn settings_agent(name: &str, description: &str) -> AgentConfig {
28    AgentConfig {
29        name: name.to_string(),
30        description: description.to_string(),
31        model: "anthropic:claude-sonnet-4-5".to_string(),
32        user_invocable: true,
33        ..AgentConfig::default()
34    }
35}
36
37/// A user-invocable [`AgentConfig`] named `name` whose prompt is the file `PROMPT.md`.
38pub fn agent_config(name: &str) -> AgentConfig {
39    AgentConfig { prompts: vec![PromptSource::file("PROMPT.md")], ..settings_agent(name, &format!("{name} agent")) }
40}
41
42/// A minimal [`AgentSpec`] with the given name and exposure.
43pub fn fake_spec(name: &str, exposure: AgentSpecExposure) -> AgentSpec {
44    AgentSpec {
45        name: name.to_string(),
46        description: format!("{name} agent"),
47        model: "anthropic:claude-sonnet-4-5".to_string(),
48        reasoning_effort: None,
49        model_settings: ModelSettings::default(),
50        context_window: None,
51        prompts: vec![],
52        provider_connections: ProviderConnectionOverrides::default(),
53        mcp_config_sources: Vec::new(),
54        exposure,
55        tools: ToolFilter::default(),
56    }
57}
58
59/// A temporary project directory whose files are set up fluently.
60pub struct TestProject {
61    root: TempDir,
62}
63
64impl TestProject {
65    /// The project root path.
66    pub fn root(&self) -> &Path {
67        self.root.path()
68    }
69
70    /// Writes `content` to `path` relative to the project root, creating parent directories.
71    pub fn file(self, path: &str, content: &str) -> Self {
72        self.write(path, content);
73        self
74    }
75
76    /// Writes `content` to `<name>/SKILL.md`, creating the skill directory.
77    pub fn skill(self, name: &str, content: &str) -> Self {
78        self.file(&format!("{name}/{SKILL_FILENAME}"), content)
79    }
80
81    /// Writes a file after the fixture exists, e.g. to exercise a state transition.
82    pub fn write(&self, path: &str, content: &str) {
83        write_file(self.root.path(), path, content);
84    }
85}
86
87/// A temporary home directory whose `.aether` contents are set up fluently.
88pub struct TestHome {
89    root: TempDir,
90}
91
92impl TestHome {
93    /// The `.aether` directory inside this home.
94    pub fn aether(&self) -> PathBuf {
95        self.root.path().join(".aether")
96    }
97
98    /// The home root path.
99    pub fn root(&self) -> &Path {
100        self.root.path()
101    }
102
103    /// Writes `content` to `path` relative to the home root, creating parent directories.
104    pub fn file(self, path: &str, content: &str) -> Self {
105        write_file(self.root.path(), path, content);
106        self
107    }
108
109    /// Writes user settings to `.aether/settings.json`.
110    pub fn settings(self, json: &str) -> Self {
111        self.file(".aether/settings.json", json)
112    }
113}
114
115fn temp_dir(label: &str) -> TempDir {
116    TempDir::new().unwrap_or_else(|error| panic!("failed to create temp {label} dir: {error}"))
117}
118
119fn write_file(root: &Path, path: &str, content: &str) {
120    let full_path = root.join(path);
121    if let Some(parent) = full_path.parent() {
122        fs::create_dir_all(parent).expect("failed to create fixture parent directories");
123    }
124
125    fs::write(full_path, content).expect("failed to write fixture file");
126}