use crate::config::profile::with_home_override;
use crate::tui::onboarding::{OnboardingStep, OnboardingWizard, WizardMode};
use std::path::PathBuf;
fn temp_home() -> (tempfile::TempDir, PathBuf) {
let dir = tempfile::tempdir().expect("tempdir");
let opencrabs = dir.path().join(".opencrabs");
std::fs::create_dir_all(&opencrabs).expect("create .opencrabs");
(dir, opencrabs)
}
#[allow(clippy::field_reassign_with_default)]
fn wizard_at_provider_auth() -> OnboardingWizard {
let mut w = OnboardingWizard::default();
w.mode = WizardMode::Advanced;
w.step = OnboardingStep::ProviderAuth;
w.ps.api_key_input = "sk-test-key".to_string();
w
}
#[test]
fn leaving_provider_auth_persists_the_provider_section() {
let (_temp, home) = temp_home();
with_home_override(home.clone(), || {
let mut w = wizard_at_provider_auth();
w.next_step(); assert_eq!(w.step, OnboardingStep::Channels, "transition succeeds");
assert!(w.error_message.is_none());
let config =
std::fs::read_to_string(home.join("config.toml")).expect("config written by step save");
let doc: toml_edit::DocumentMut = config
.parse()
.expect("step-saved config.toml must be valid TOML");
assert_eq!(
doc["providers"]["anthropic"]["enabled"].as_bool(),
Some(true),
"selected provider is enabled on disk before the Channels step renders"
);
let keys =
std::fs::read_to_string(home.join("keys.toml")).expect("keys written by step save");
assert!(
keys.contains("sk-test-key"),
"the API key lands in keys.toml during step save, not just at the end"
);
});
}
#[test]
fn leaving_channels_persists_channel_toggles() {
let (_temp, home) = temp_home();
with_home_override(home.clone(), || {
let mut w = wizard_at_provider_auth();
w.next_step(); assert_eq!(w.step, OnboardingStep::Channels);
w.next_step(); assert_eq!(w.step, OnboardingStep::VoiceSetup);
assert!(w.error_message.is_none());
let config =
std::fs::read_to_string(home.join("config.toml")).expect("config exists after 2 saves");
let doc: toml_edit::DocumentMut = config.parse().expect("valid TOML");
assert!(
doc["channels"]["telegram"].get("enabled").is_some(),
"telegram enabled flag committed during Channels step save"
);
assert!(
doc["channels"]["discord"].get("enabled").is_some(),
"discord enabled flag committed during Channels step save"
);
});
}
#[test]
fn abandoning_mid_wizard_keeps_confirmed_sections() {
let (_temp, home) = temp_home();
with_home_override(home.clone(), || {
{
let mut w = wizard_at_provider_auth();
w.next_step(); assert_eq!(w.step, OnboardingStep::Channels);
w.next_step(); assert_eq!(w.step, OnboardingStep::VoiceSetup);
}
let config = std::fs::read_to_string(home.join("config.toml"))
.expect("both step-scoped saves survive the drop");
let doc: toml_edit::DocumentMut = config.parse().expect("valid TOML");
assert_eq!(
doc["providers"]["anthropic"]["enabled"].as_bool(),
Some(true),
"provider section survives wizard interruption"
);
assert!(
doc["channels"]["telegram"].get("enabled").is_some(),
"channel toggles survive wizard interruption"
);
});
}
#[test]
fn failed_step_save_surfaces_section_and_cause() {
let (_temp, home) = temp_home();
with_home_override(home.clone(), || {
let config_path = home.join("config.toml");
std::fs::create_dir(&config_path).expect("create directory named config.toml");
let mut w = wizard_at_provider_auth();
w.next_step();
assert_eq!(
w.step,
OnboardingStep::ProviderAuth,
"a failed save rolls the step back"
);
let err = w.error_message.expect("failure surfaces, not swallowed");
assert!(
err.contains("provider step"),
"error names the owning section: {err}"
);
assert!(
err.contains("providers."),
"error names the key that failed: {err}"
);
});
}
#[test]
fn failed_step_save_contains_the_underlying_io_cause() {
let (_temp, home) = temp_home();
with_home_override(home.clone(), || {
let config_path = home.join("config.toml");
std::fs::create_dir(&config_path).expect("config.toml as directory");
let mut w = wizard_at_provider_auth();
w.next_step();
let err = w.error_message.unwrap();
assert!(
err.contains("setting(s) could not be saved"),
"structured error from write_scoped_config: {err}"
);
});
}
#[test]
fn step_save_does_not_seed_templates() {
let (_temp, home) = temp_home();
with_home_override(home.clone(), || {
let mut w = wizard_at_provider_auth();
w.workspace_path = home.to_string_lossy().to_string();
assert!(
w.seed_templates,
"defaults to true, so this is the risky case"
);
w.next_step(); assert_eq!(w.step, OnboardingStep::Channels);
assert!(
!home.join("SOUL.md").exists(),
"step-scoped save must not seed workspace templates"
);
});
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn quick_jump_does_not_trigger_step_save() {
let (_temp, home) = temp_home();
with_home_override(home.clone(), || {
let mut w = OnboardingWizard::default();
w.quick_jump = true;
w.step = OnboardingStep::ProviderAuth;
w.ps.api_key_input = "sk-test-key".to_string();
w.next_step();
assert!(w.quick_jump_done, "quick_jump sets done flag");
assert_eq!(
w.step,
OnboardingStep::ProviderAuth,
"step does not change in quick_jump mode"
);
assert!(
!home.join("config.toml").exists(),
"no step-scoped save fired for quick_jump"
);
});
}
#[test]
fn mode_select_to_workspace_does_not_write_config() {
let (_temp, home) = temp_home();
with_home_override(home.clone(), || {
let mut w = OnboardingWizard::default();
assert_eq!(w.step, OnboardingStep::ModeSelect);
w.next_step(); assert_eq!(w.step, OnboardingStep::Workspace);
assert!(
!home.join("config.toml").exists(),
"ModeSelect and Workspace own no section, nothing written"
);
});
}