pub mod catalog;
pub mod import;
pub mod input;
pub mod plan;
pub mod render;
pub mod state;
pub mod verify;
use std::path::{Path, PathBuf};
use std::time::Duration;
use clap::Args;
use ratatui::Terminal;
use tokio::sync::mpsc;
use crate::config::Config;
use crate::tui::{EventSource, TerminalSetup};
use crossterm::event::{Event, KeyEventKind};
use state::{VerifyReply, VerifyRequest, Wizard};
use verify::ProviderVerifier;
#[derive(Args)]
pub struct SetupArgs {
#[arg(long)]
pub non_interactive: bool,
#[arg(long)]
pub no_verify: bool,
#[arg(long)]
pub anthropic_key: Option<String>,
#[arg(long)]
pub openai_key: Option<String>,
#[arg(long)]
pub google_key: Option<String>,
#[arg(long)]
pub openrouter_key: Option<String>,
#[arg(long)]
pub ollama_url: Option<String>,
#[arg(long)]
pub default_model: Option<String>,
#[arg(long)]
pub claude_code: Option<bool>,
#[arg(long)]
pub claude_code_effort: Option<String>,
#[arg(long)]
pub install_agents: bool,
}
pub type EnvLookup = Box<dyn Fn(&str) -> Option<String> + Send + Sync>;
pub struct SetupEnv {
pub config_path: PathBuf,
pub agents_dir: PathBuf,
pub roots: import::Roots,
pub env_lookup: EnvLookup,
pub opener: leviath_mcp::BrowserOpener,
}
pub fn run_non_interactive(args: &SetupArgs, env: &SetupEnv) -> anyhow::Result<()> {
let mut config = Config::load_from_path_public(&env.config_path).unwrap_or_default();
apply_flags(&mut config, args);
let agents = if args.install_agents {
crate::bundled::plan_agent_actions(&env.agents_dir)
.into_iter()
.filter(|(_, action)| action.preselect())
.map(|(agent, _)| agent)
.collect()
} else {
Vec::new()
};
let applied = plan::apply(
&plan::SetupPlan { config, agents },
&env.config_path,
&env.agents_dir,
)?;
report(&applied);
Ok(())
}
fn report(applied: &plan::Applied) {
println!("Config saved to {}", applied.config_path.display());
if !applied.agents_installed.is_empty() {
println!(
"Installed {} agent(s): {}",
applied.agents_installed.len(),
applied.agents_installed.join(", ")
);
}
for warning in &applied.warnings {
println!(" Warning: {warning}");
}
}
fn apply_flags(config: &mut Config, args: &SetupArgs) {
if let Some(ref k) = args.anthropic_key {
config.providers.anthropic_api_key = Some(k.clone());
}
if let Some(ref k) = args.openai_key {
config.providers.openai_api_key = Some(k.clone());
}
if let Some(ref k) = args.google_key {
config.providers.google_api_key = Some(k.clone());
}
if let Some(ref k) = args.openrouter_key {
config.openrouter_api_key = Some(k.clone());
}
if let Some(ref u) = args.ollama_url {
config.ollama_base_url = Some(u.clone());
}
if let Some(ref m) = args.default_model {
config.default_model = Some(m.clone());
}
if let Some(enabled) = args.claude_code {
config.providers.claude_code_enabled = enabled;
}
if let Some(ref e) = args.claude_code_effort {
config.providers.claude_code_effort = Some(e.clone());
}
retarget_default_provider(config);
}
fn configured_providers(config: &Config) -> Vec<&'static str> {
[
("anthropic", config.providers.anthropic_api_key.is_some()),
("openai", config.providers.openai_api_key.is_some()),
("google", config.providers.google_api_key.is_some()),
("openrouter", config.openrouter_api_key.is_some()),
("claude-code", config.providers.claude_code_enabled),
("ollama", config.ollama_base_url.is_some()),
]
.into_iter()
.filter(|(_, configured)| *configured)
.map(|(id, _)| id)
.collect()
}
fn retarget_default_provider(config: &mut Config) {
let configured = configured_providers(config);
if configured.contains(&config.default_provider.as_str()) {
return;
}
if let Some(first) = configured.first() {
config.default_provider = (*first).to_string();
}
}
pub fn build_wizard(env: &SetupEnv) -> Wizard {
let base = Config::load_from_path_public(&env.config_path).unwrap_or_default();
let (candidates, errors) = state::candidates_from_scans(import::scan(&env.roots));
Wizard::new(
base,
&env.env_lookup,
candidates,
errors,
&env.agents_dir,
env.opener.clone(),
)
}
pub async fn verification_loop<V: ProviderVerifier>(
verifier: V,
mut requests: mpsc::UnboundedReceiver<VerifyRequest>,
replies: mpsc::UnboundedSender<VerifyReply>,
) {
while let Some(request) = requests.recv().await {
let outcome = verifier.verify(&request.creds).await;
if replies
.send(VerifyReply {
provider_id: request.provider_id,
outcome,
})
.is_err()
{
return;
}
}
}
pub async fn run_wizard_loop<B: ratatui::backend::Backend>(
wizard: &mut Wizard,
terminal: &mut Terminal<B>,
events: &mut impl EventSource,
tick_rate: Duration,
) -> anyhow::Result<Option<plan::SetupPlan>> {
loop {
wizard.ticks += 1;
wizard.drain_verifications();
let mut area = ratatui::layout::Rect::default();
terminal
.draw(|frame| {
area = frame.area();
render::draw(frame, wizard);
})
.map_err(|e| anyhow::anyhow!("terminal draw failed: {e}"))?;
match events.poll_event(tick_rate)? {
Some(Event::Key(key))
if key.kind == KeyEventKind::Press
&& wizard.handle_key(key) == input::Action::Save =>
{
wizard.finished = true;
}
Some(Event::Mouse(mouse))
if wizard.handle_mouse(mouse, area) == input::Action::Save =>
{
wizard.finished = true;
}
_ => {}
}
if wizard.finished {
return Ok(Some(wizard.build_plan()));
}
if wizard.should_quit {
return Ok(None);
}
}
}
pub async fn execute_core<S: TerminalSetup, E: EventSource>(
wizard: &mut Wizard,
env: &SetupEnv,
setup: &mut S,
events: &mut E,
) -> anyhow::Result<()> {
setup.enable()?;
let mut terminal = setup.create_terminal()?;
let result = run_wizard_loop(wizard, &mut terminal, events, Duration::from_millis(120)).await;
setup.disable();
match result? {
Some(plan) => {
let applied = plan::apply(&plan, &env.config_path, &env.agents_dir)?;
report(&applied);
print_next_steps(&applied);
}
None => println!("Setup cancelled. Nothing was written."),
}
Ok(())
}
fn print_next_steps(applied: &plan::Applied) {
println!();
match applied.agents_installed.first() {
Some(agent) => println!("Try it: lev run {agent} --task \"...\""),
None => println!("Install an agent with `lev setup`, then `lev run <agent>`."),
}
}
pub async fn execute_with<S: TerminalSetup, E: EventSource>(
args: &SetupArgs,
env: &SetupEnv,
setup: &mut S,
events: &mut E,
is_terminal: bool,
) -> anyhow::Result<()> {
if args.non_interactive {
return run_non_interactive(args, env);
}
if !is_terminal {
anyhow::bail!(
"lev setup needs a terminal. For scripted use:\n \
lev setup --non-interactive --anthropic-key sk-ant-... --install-agents"
);
}
let mut wizard = build_wizard(env);
execute_core(&mut wizard, env, setup, events).await
}
pub fn real_agents_dir(home: Option<&Path>) -> PathBuf {
home.unwrap_or(Path::new(""))
.join(".leviath")
.join("agents")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bundled::BUNDLED_AGENTS;
use crate::tui::{TestEventSource, TestSetup, key, key_with, test_terminal};
use crossterm::event::{KeyCode, KeyModifiers};
fn args() -> SetupArgs {
SetupArgs {
non_interactive: false,
no_verify: false,
anthropic_key: None,
openai_key: None,
google_key: None,
openrouter_key: None,
ollama_url: None,
default_model: None,
claude_code: None,
claude_code_effort: None,
install_agents: false,
}
}
fn env_in(dir: &Path) -> SetupEnv {
SetupEnv {
config_path: dir.join("config.toml"),
agents_dir: dir.join("agents"),
roots: import::Roots {
home: dir.join("home"),
os_config: dir.join("os-config"),
xdg_config: dir.join("home").join(".config"),
cwd: dir.join("cwd"),
},
env_lookup: Box::new(|_| None),
opener: std::sync::Arc::new(|_| true),
}
}
#[test]
fn a_single_non_anthropic_key_becomes_the_default_provider() {
let mut config = Config::default();
assert_eq!(config.default_provider, "anthropic");
apply_flags(
&mut config,
&SetupArgs {
openrouter_key: Some("sk-or-test".to_string()),
..args()
},
);
assert_eq!(config.default_provider, "openrouter");
}
#[test]
fn a_reachable_default_provider_is_left_alone() {
let mut config = Config::default();
apply_flags(
&mut config,
&SetupArgs {
anthropic_key: Some("sk-ant-test".to_string()),
openrouter_key: Some("sk-or-test".to_string()),
..args()
},
);
assert_eq!(config.default_provider, "anthropic");
}
#[test]
fn a_deliberate_default_provider_survives() {
let mut config = Config {
default_provider: "google".to_string(),
..Config::default()
};
apply_flags(
&mut config,
&SetupArgs {
google_key: Some("AIza-test".to_string()),
openrouter_key: Some("sk-or-test".to_string()),
..args()
},
);
assert_eq!(config.default_provider, "google");
}
#[test]
fn configuring_nothing_leaves_the_default_provider_untouched() {
let mut config = Config::default();
apply_flags(&mut config, &args());
assert_eq!(config.default_provider, "anthropic");
}
#[test]
fn ollama_is_the_last_provider_considered() {
let mut config = Config::default();
apply_flags(
&mut config,
&SetupArgs {
ollama_url: Some("http://localhost:11434".to_string()),
google_key: Some("AIza-test".to_string()),
..args()
},
);
assert_eq!(config.default_provider, "google");
let mut ollama_only = Config::default();
apply_flags(
&mut ollama_only,
&SetupArgs {
ollama_url: Some("http://localhost:11434".to_string()),
..args()
},
);
assert_eq!(ollama_only.default_provider, "ollama");
}
#[test]
fn the_claude_code_transport_counts_as_a_configured_provider() {
let mut config = Config::default();
apply_flags(
&mut config,
&SetupArgs {
claude_code: Some(true),
..args()
},
);
assert_eq!(config.default_provider, "claude-code");
}
#[test]
fn flags_are_written_to_the_config() {
let dir = tempfile::tempdir().unwrap();
let env = env_in(dir.path());
let args = SetupArgs {
non_interactive: true,
anthropic_key: Some("sk-ant-x".to_string()),
openai_key: Some("sk-oai".to_string()),
google_key: Some("goog".to_string()),
openrouter_key: Some("sk-or".to_string()),
ollama_url: Some("http://box:11434".to_string()),
default_model: Some("m".to_string()),
claude_code: Some(true),
claude_code_effort: Some("xhigh".to_string()),
..args()
};
run_non_interactive(&args, &env).unwrap();
let written = Config::load_from_path_public(&env.config_path).unwrap();
assert_eq!(
written.providers.anthropic_api_key.as_deref(),
Some("sk-ant-x")
);
assert_eq!(written.providers.openai_api_key.as_deref(), Some("sk-oai"));
assert_eq!(written.providers.google_api_key.as_deref(), Some("goog"));
assert_eq!(written.openrouter_api_key.as_deref(), Some("sk-or"));
assert_eq!(written.ollama_base_url.as_deref(), Some("http://box:11434"));
assert_eq!(written.default_model.as_deref(), Some("m"));
assert!(written.providers.claude_code_enabled);
assert_eq!(
written.providers.claude_code_effort.as_deref(),
Some("xhigh")
);
}
#[test]
fn the_non_interactive_path_installs_agents_only_when_asked() {
let dir = tempfile::tempdir().unwrap();
let env = env_in(dir.path());
run_non_interactive(&args(), &env).unwrap();
assert!(!env.agents_dir.exists(), "nothing was asked for");
run_non_interactive(
&SetupArgs {
install_agents: true,
..args()
},
&env,
)
.unwrap();
assert!(
env.agents_dir.join(BUNDLED_AGENTS[0].name).exists(),
"every bundled blueprint should land"
);
run_non_interactive(
&SetupArgs {
install_agents: true,
..args()
},
&env,
)
.unwrap();
assert!(env.agents_dir.join(BUNDLED_AGENTS[0].name).exists());
}
#[test]
fn the_non_interactive_path_keeps_settings_it_was_not_given() {
let dir = tempfile::tempdir().unwrap();
let env = env_in(dir.path());
run_non_interactive(
&SetupArgs {
anthropic_key: Some("sk-ant-first".to_string()),
..args()
},
&env,
)
.unwrap();
run_non_interactive(
&SetupArgs {
openai_key: Some("sk-oai".to_string()),
..args()
},
&env,
)
.unwrap();
let written = Config::load_from_path_public(&env.config_path).unwrap();
assert_eq!(
written.providers.anthropic_api_key.as_deref(),
Some("sk-ant-first")
);
assert_eq!(written.providers.openai_api_key.as_deref(), Some("sk-oai"));
}
#[test]
fn a_config_that_cannot_be_written_is_an_error() {
let dir = tempfile::tempdir().unwrap();
let blocked = dir.path().join("not-a-dir");
std::fs::write(&blocked, "").unwrap();
let mut env = env_in(dir.path());
env.config_path = blocked.join("config.toml");
assert!(run_non_interactive(&args(), &env).is_err());
}
#[test]
fn the_wizard_reads_the_config_file_and_scans_for_harnesses() {
let dir = tempfile::tempdir().unwrap();
let env = env_in(dir.path());
std::fs::create_dir_all(&env.roots.home).unwrap();
std::fs::write(
env.roots.home.join(".claude.json"),
r#"{"mcpServers":{"fs":{"command":"npx"}}}"#,
)
.unwrap();
run_non_interactive(
&SetupArgs {
anthropic_key: Some("sk-ant-stored".to_string()),
..args()
},
&env,
)
.unwrap();
let wizard = build_wizard(&env);
assert_eq!(
wizard.base.providers.anthropic_api_key.as_deref(),
Some("sk-ant-stored")
);
assert_eq!(wizard.mcp.len(), 1);
assert_eq!(wizard.mcp[0].candidate.config.name, "fs");
}
#[test]
fn a_missing_config_file_starts_from_defaults() {
let dir = tempfile::tempdir().unwrap();
let wizard = build_wizard(&env_in(dir.path()));
assert_eq!(
wizard.base.default_provider,
Config::default().default_provider
);
}
#[tokio::test]
async fn the_verification_loop_answers_every_request_then_stops() {
let dir = tempfile::tempdir().unwrap();
let mut wizard = build_wizard(&env_in(dir.path()));
let (requests, replies) = wizard.take_verify_ends().expect("first take");
wizard.providers[0].selected = true;
wizard.providers[0].value = "sk-ant".to_string();
wizard.request_verification(0);
let handle = tokio::spawn(verification_loop(verify::SkipVerifier, requests, replies));
let sender = wizard.verify_tx.clone();
drop(sender);
for _ in 0..50 {
wizard.drain_verifications();
if !wizard.providers[0].checking {
break;
}
tokio::time::sleep(Duration::from_millis(2)).await;
}
assert!(!wizard.providers[0].checking);
assert_eq!(wizard.providers[0].outcome, verify::Outcome::Skipped);
drop(wizard);
handle.await.expect("the loop exits cleanly");
}
#[tokio::test]
async fn the_verification_loop_stops_when_nobody_is_listening() {
let (request_tx, request_rx) = mpsc::unbounded_channel();
let (reply_tx, reply_rx) = mpsc::unbounded_channel::<VerifyReply>();
request_tx
.send(VerifyRequest {
provider_id: "anthropic".to_string(),
creds: leviath_runtime::provider_creds::ProviderCreds {
name: "anthropic".to_string(),
api_key: Some("sk-ant".to_string()),
base_url: None,
model_capabilities: std::collections::HashMap::new(),
request_timeout_secs: Some(1),
rate_limit: None,
options: std::collections::HashMap::new(),
},
})
.unwrap();
drop(reply_rx);
verification_loop(verify::SkipVerifier, request_rx, reply_tx).await;
}
#[tokio::test]
async fn quitting_returns_no_plan() {
let dir = tempfile::tempdir().unwrap();
let mut wizard = build_wizard(&env_in(dir.path()));
let mut terminal = test_terminal();
let mut events = TestEventSource::new(vec![key(KeyCode::Char('q'))]);
let plan = run_wizard_loop(
&mut wizard,
&mut terminal,
&mut events,
Duration::from_millis(1),
)
.await
.unwrap();
assert!(plan.is_none());
}
#[tokio::test]
async fn saving_returns_the_plan_the_wizard_describes() {
let dir = tempfile::tempdir().unwrap();
let mut wizard = build_wizard(&env_in(dir.path()));
let mut terminal = test_terminal();
let mut events = TestEventSource::new_with_nones(vec![
None,
Some(key_with(KeyCode::Char('s'), KeyModifiers::CONTROL)),
]);
let plan = run_wizard_loop(
&mut wizard,
&mut terminal,
&mut events,
Duration::from_millis(1),
)
.await
.unwrap()
.expect("a plan was produced");
assert_eq!(plan.agents.len(), BUNDLED_AGENTS.len());
}
#[tokio::test]
async fn non_press_and_non_key_events_are_ignored() {
let dir = tempfile::tempdir().unwrap();
let mut wizard = build_wizard(&env_in(dir.path()));
let mut terminal = test_terminal();
let release = crossterm::event::Event::Key(crossterm::event::KeyEvent::new_with_kind(
KeyCode::Char('q'),
KeyModifiers::empty(),
KeyEventKind::Release,
));
let mut events = TestEventSource::new(vec![
release,
crossterm::event::Event::FocusGained,
crossterm::event::Event::Resize(80, 24),
key(KeyCode::Char('q')),
]);
let plan = run_wizard_loop(
&mut wizard,
&mut terminal,
&mut events,
Duration::from_millis(1),
)
.await
.unwrap();
assert!(plan.is_none(), "only the real press quit");
}
#[tokio::test]
async fn a_click_is_routed_with_the_window_it_was_made_in() {
let dir = tempfile::tempdir().unwrap();
let mut wizard = build_wizard(&env_in(dir.path()));
wizard.enter(state::Step::Providers);
let mut terminal = test_terminal();
let size = terminal.size().expect("the test backend has a size");
let area = ratatui::layout::Rect::new(0, 0, size.width, size.height);
let row = (0..area.height)
.find(|y| render::row_at(area, &wizard, 4, *y) == Some(1))
.expect("the second provider is on screen");
let mut events = TestEventSource::new(vec![
crossterm::event::Event::Mouse(crossterm::event::MouseEvent {
kind: crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left),
column: 4,
row,
modifiers: KeyModifiers::empty(),
}),
key_with(KeyCode::Char('s'), KeyModifiers::CONTROL),
]);
let plan = run_wizard_loop(
&mut wizard,
&mut terminal,
&mut events,
Duration::from_millis(1),
)
.await
.unwrap()
.expect("ctrl-s finished it");
assert!(
wizard.providers[1].selected,
"the click selected what it landed on"
);
assert!(!plan.agents.is_empty());
}
#[tokio::test]
async fn clicking_apply_and_finish_ends_the_wizard() {
let dir = tempfile::tempdir().unwrap();
let mut wizard = build_wizard(&env_in(dir.path()));
wizard.enter(state::Step::Review);
let mut terminal = test_terminal();
let size = terminal.size().expect("the test backend has a size");
let area = ratatui::layout::Rect::new(0, 0, size.width, size.height);
let button = wizard.nav_rows() - 1;
let row = (0..area.height)
.find(|y| render::row_at(area, &wizard, 4, *y) == Some(button))
.expect("the button is on screen");
let mut events = TestEventSource::new(vec![crossterm::event::Event::Mouse(
crossterm::event::MouseEvent {
kind: crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left),
column: 4,
row,
modifiers: KeyModifiers::empty(),
},
)]);
let plan = run_wizard_loop(
&mut wizard,
&mut terminal,
&mut events,
Duration::from_millis(1),
)
.await
.unwrap();
assert!(plan.is_some(), "the click applied the plan");
}
#[tokio::test]
async fn a_draw_failure_propagates() {
let dir = tempfile::tempdir().unwrap();
let mut wizard = build_wizard(&env_in(dir.path()));
let mut terminal =
ratatui::Terminal::new(crate::tui::TestBackendHarness::failing(80, 24)).unwrap();
let mut events = TestEventSource::new(vec![]);
let result = run_wizard_loop(
&mut wizard,
&mut terminal,
&mut events,
Duration::from_millis(1),
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn an_event_source_failure_propagates() {
let dir = tempfile::tempdir().unwrap();
let mut wizard = build_wizard(&env_in(dir.path()));
let mut terminal = test_terminal();
let mut events = TestEventSource::failing();
let result = run_wizard_loop(
&mut wizard,
&mut terminal,
&mut events,
Duration::from_millis(1),
)
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn saving_writes_the_config_and_installs_the_agents() {
let dir = tempfile::tempdir().unwrap();
let env = env_in(dir.path());
let mut wizard = build_wizard(&env);
let mut setup = TestSetup::new();
let mut events =
TestEventSource::new(vec![key_with(KeyCode::Char('s'), KeyModifiers::CONTROL)]);
execute_core(&mut wizard, &env, &mut setup, &mut events)
.await
.unwrap();
assert!(env.config_path.exists());
assert!(env.agents_dir.join(BUNDLED_AGENTS[0].name).exists());
}
#[tokio::test]
async fn quitting_writes_nothing() {
let dir = tempfile::tempdir().unwrap();
let env = env_in(dir.path());
let mut wizard = build_wizard(&env);
let mut setup = TestSetup::new();
let mut events = TestEventSource::new(vec![key(KeyCode::Char('q'))]);
execute_core(&mut wizard, &env, &mut setup, &mut events)
.await
.unwrap();
assert!(
!env.config_path.exists(),
"nothing should have been written"
);
assert!(!env.agents_dir.exists());
}
#[tokio::test]
async fn a_terminal_that_will_not_start_is_an_error() {
let dir = tempfile::tempdir().unwrap();
let env = env_in(dir.path());
let mut wizard = build_wizard(&env);
let mut events = TestEventSource::new(vec![]);
let mut enable_fails = TestSetup {
enable_should_fail: true,
create_should_fail: false,
draw_should_fail: false,
};
assert!(
execute_core(&mut wizard, &env, &mut enable_fails, &mut events)
.await
.is_err()
);
let mut create_fails = TestSetup {
enable_should_fail: false,
create_should_fail: true,
draw_should_fail: false,
};
assert!(
execute_core(&mut wizard, &env, &mut create_fails, &mut events)
.await
.is_err()
);
}
#[tokio::test]
async fn a_loop_failure_is_surfaced_after_the_terminal_is_restored() {
let dir = tempfile::tempdir().unwrap();
let env = env_in(dir.path());
let mut wizard = build_wizard(&env);
let mut setup = TestSetup::new();
let mut events = TestEventSource::failing();
let result = execute_core(&mut wizard, &env, &mut setup, &mut events).await;
assert!(result.is_err());
assert!(!env.config_path.exists());
}
#[tokio::test]
async fn a_write_failure_after_the_wizard_is_surfaced() {
let dir = tempfile::tempdir().unwrap();
let mut env = env_in(dir.path());
let blocked = dir.path().join("not-a-dir");
std::fs::write(&blocked, "").unwrap();
let mut wizard = build_wizard(&env);
env.config_path = blocked.join("config.toml");
let mut setup = TestSetup::new();
let mut events =
TestEventSource::new(vec![key_with(KeyCode::Char('s'), KeyModifiers::CONTROL)]);
let result = execute_core(&mut wizard, &env, &mut setup, &mut events).await;
assert!(result.is_err());
}
#[tokio::test]
async fn execute_with_routes_to_the_flags_path() {
let dir = tempfile::tempdir().unwrap();
let env = env_in(dir.path());
let mut setup = TestSetup::new();
let mut events = TestEventSource::new(vec![]);
execute_with(
&SetupArgs {
non_interactive: true,
anthropic_key: Some("sk-ant-x".to_string()),
..args()
},
&env,
&mut setup,
&mut events,
false,
)
.await
.unwrap();
let written = Config::load_from_path_public(&env.config_path).unwrap();
assert_eq!(
written.providers.anthropic_api_key.as_deref(),
Some("sk-ant-x")
);
}
#[tokio::test]
async fn without_a_terminal_the_wizard_refuses_and_says_what_to_run_instead() {
let dir = tempfile::tempdir().unwrap();
let env = env_in(dir.path());
let mut setup = TestSetup::new();
let mut events = TestEventSource::new(vec![]);
let error = execute_with(&args(), &env, &mut setup, &mut events, false)
.await
.expect_err("a pipe is not a terminal");
let message = error.to_string();
assert!(message.contains("needs a terminal"), "{message}");
assert!(message.contains("--non-interactive"), "{message}");
assert!(!env.config_path.exists());
}
#[tokio::test]
async fn with_a_terminal_execute_with_runs_the_wizard() {
let dir = tempfile::tempdir().unwrap();
let env = env_in(dir.path());
let mut setup = TestSetup::new();
let mut events =
TestEventSource::new(vec![key_with(KeyCode::Char('s'), KeyModifiers::CONTROL)]);
execute_with(&args(), &env, &mut setup, &mut events, true)
.await
.unwrap();
assert!(env.config_path.exists());
}
#[test]
fn the_summary_covers_agents_warnings_and_the_empty_case() {
report(&plan::Applied {
config_path: PathBuf::from("/tmp/config.toml"),
agents_installed: vec!["coder".to_string()],
warnings: vec!["could not install x".to_string()],
});
report(&plan::Applied {
config_path: PathBuf::from("/tmp/config.toml"),
agents_installed: Vec::new(),
warnings: Vec::new(),
});
}
#[test]
fn the_next_step_names_an_installed_agent_when_there_is_one() {
print_next_steps(&plan::Applied {
config_path: PathBuf::from("/tmp/config.toml"),
agents_installed: vec!["coder".to_string()],
warnings: Vec::new(),
});
print_next_steps(&plan::Applied {
config_path: PathBuf::from("/tmp/config.toml"),
agents_installed: Vec::new(),
warnings: Vec::new(),
});
}
#[test]
fn the_real_agents_directory_sits_under_the_leviath_home() {
assert_eq!(
real_agents_dir(Some(Path::new("/home/u"))),
PathBuf::from("/home/u/.leviath/agents")
);
assert_eq!(real_agents_dir(None), PathBuf::from(".leviath/agents"));
}
}