use super::support::*;
use super::*;
#[test]
fn provider_ready_stays_false_when_oauth_finishes_for_non_active_provider() {
let (sender, receiver) = bounded::<TuiEvent>(1);
let mut state = state::MissionControlState {
provider: "openai".to_string(),
provider_ready: false,
..Default::default()
};
state.open_connect_provider(Vec::new(), None);
state.start_connect_oauth(crate::providers::OPENAI_CODEX_PROVIDER.to_string());
let worker_id = 1;
state.active_worker_id = Some(worker_id);
sender
.send(TuiEvent::OAuthFinished {
worker_id,
provider_id: crate::providers::OPENAI_CODEX_PROVIDER.to_string(),
result: Ok("OpenAI Codex configured".to_string()),
})
.unwrap();
let result = drain_tui_events(&receiver, &mut state);
assert!(result.changed);
assert!(!state.provider_ready);
assert_eq!(
state.modals.connect_provider.as_ref().unwrap().stage,
state::ConnectProviderStage::Success
);
assert_eq!(state.status, "OpenAI Codex configured");
}
#[test]
fn auth_error_preserves_fast_capability_and_oauth_restores_rainbow() {
let (sender, receiver) = bounded::<TuiEvent>(2);
let mut state = state::MissionControlState {
provider: crate::providers::OPENAI_CODEX_PROVIDER.to_string(),
model: "gpt-5.5".to_string(),
provider_ready: true,
color_enabled: true,
fast_mode_enabled: true,
fast_mode_capable: true,
fast_mode_effective: true,
..Default::default()
};
sender
.send(TuiEvent::Error(
"openai-codex credential refresh failed: expired".to_string(),
))
.unwrap();
let result = drain_tui_events(&receiver, &mut state);
assert!(result.changed);
assert!(!state.provider_ready);
assert!(state.fast_mode_capable);
assert!(state.fast_mode_effective);
assert!(!state.fast_mode_rainbow_active());
state.open_connect_provider(Vec::new(), None);
state.start_connect_oauth(crate::providers::OPENAI_CODEX_PROVIDER.to_string());
let worker_id = 2;
state.active_worker_id = Some(worker_id);
sender
.send(TuiEvent::OAuthFinished {
worker_id,
provider_id: crate::providers::OPENAI_CODEX_PROVIDER.to_string(),
result: Ok("OpenAI Codex configured".to_string()),
})
.unwrap();
let result = drain_tui_events(&receiver, &mut state);
assert!(result.changed);
assert!(state.provider_ready);
assert!(state.fast_mode_capable);
assert!(state.fast_mode_effective);
assert!(state.fast_mode_rainbow_active());
}
#[test]
fn connect_provider_picker_uses_provider_specific_status_labels() {
let env = crate::test_support::env::env_lock();
let _saved_key = env.save("ANTHROPIC_API_KEY");
env.remove_var("ANTHROPIC_API_KEY");
let temp = tempfile::TempDir::new().unwrap();
let (sender, _receiver) = bounded::<TuiEvent>(4);
let mut app = test_app(&temp, sender);
let mut auth = crate::config::Auth::default();
auth.providers.insert(
crate::providers::ANTHROPIC_PROVIDER.to_string(),
crate::config::AuthProviderRecord::ApiKey {
key: "anthropic-key".to_string(),
},
);
crate::config::write_auth(&app.config.paths, &auth).unwrap();
let mut ui_state = state::MissionControlState::default();
app.open_connect_provider(&mut ui_state);
{
let picker = ui_state.modals.connect_provider.as_ref().unwrap();
assert_eq!(
picker
.entries
.iter()
.find(|entry| entry.id == crate::providers::ANTHROPIC_PROVIDER)
.map(|entry| entry.status.as_str()),
Some("ready via Anthropic API key")
);
assert_eq!(
picker
.entries
.iter()
.find(|entry| entry.id == crate::providers::OPENAI_CODEX_PROVIDER)
.map(|entry| entry.status.as_str()),
Some("missing")
);
assert_eq!(
picker
.entries
.iter()
.find(|entry| entry.id == crate::login::CUSTOM_PROVIDER_LOGIN_ID)
.map(|entry| entry.status.as_str()),
Some("configure with prompts")
);
}
crate::config::write_auth(&app.config.paths, &crate::config::Auth::default()).unwrap();
app.open_connect_provider(&mut ui_state);
let picker = ui_state.modals.connect_provider.as_ref().unwrap();
assert_eq!(
picker
.entries
.iter()
.find(|entry| entry.id == crate::providers::ANTHROPIC_PROVIDER)
.map(|entry| entry.status.as_str()),
Some("needs API key")
);
}
#[test]
fn anthropic_connect_provider_shows_setup_guidance_without_starting_worker() {
let temp = tempfile::TempDir::new().unwrap();
let (sender, receiver) = bounded::<TuiEvent>(4);
let mut app = test_app(&temp, sender);
let mut ui_state = state::MissionControlState::default();
app.start_connect_provider(crate::providers::ANTHROPIC_PROVIDER, &mut ui_state);
assert_eq!(
ui_state.status,
crate::login::anthropic_api_key_setup_guidance()
);
assert!(app.worker.is_none());
assert!(!app.active_run);
assert_eq!(
ui_state.modals.connect_provider.as_ref().unwrap().stage,
state::ConnectProviderStage::AnthropicGuidance
);
assert!(receiver.try_recv().is_err());
}
#[test]
fn unknown_connect_provider_shows_supported_provider_error() {
let temp = tempfile::TempDir::new().unwrap();
let (sender, receiver) = bounded::<TuiEvent>(4);
let mut app = test_app(&temp, sender);
let mut ui_state = state::MissionControlState::default();
app.start_connect_provider("unknown-provider", &mut ui_state);
assert!(app.worker.is_none());
assert!(!app.active_run);
assert_eq!(
ui_state.modals.connect_provider.as_ref().unwrap().stage,
state::ConnectProviderStage::Error
);
assert!(
ui_state
.status
.contains("supported providers: openai-codex, anthropic, custom-provider")
);
assert!(receiver.try_recv().is_err());
}
#[test]
fn logout_anthropic_keeps_environment_auth_in_all_tui_configs() {
let env = crate::test_support::env::env_lock();
let _saved_key = env.save("ANTHROPIC_API_KEY");
env.set_var("ANTHROPIC_API_KEY", "environment-test-key");
let temp = tempfile::TempDir::new().unwrap();
let (sender, _receiver) = bounded::<TuiEvent>(4);
let mut app = ready_anthropic_app(&temp, sender);
let mut auth = crate::config::Auth::default();
auth.providers.insert(
crate::providers::ANTHROPIC_PROVIDER.to_string(),
crate::config::AuthProviderRecord::ApiKey {
key: "stored-test-key".to_string(),
},
);
crate::config::write_auth(&app.config.paths, &auth).unwrap();
let mut ui_state = state::MissionControlState {
provider: crate::providers::ANTHROPIC_PROVIDER.to_string(),
provider_ready: true,
..Default::default()
};
app.remove_logout_provider(crate::providers::ANTHROPIC_PROVIDER, &mut ui_state);
let saved = crate::config::read_auth(&app.config.paths).unwrap();
assert!(
!saved
.providers
.contains_key(crate::providers::ANTHROPIC_PROVIDER)
);
assert_eq!(
std::env::var("ANTHROPIC_API_KEY").as_deref(),
Ok("environment-test-key")
);
assert!(ui_state.provider_ready);
assert!(matches!(
app.state.auth_state,
crate::config::AuthState::Ready {
provider,
credential: crate::config::ProviderCredential::ApiKey { key }
} if provider == crate::providers::ANTHROPIC_PROVIDER && key == "environment-test-key"
));
assert!(matches!(
app.config.auth,
Some(crate::config::ProviderCredential::ApiKey { ref key })
if key == "environment-test-key"
));
let state_config = app.state.config.as_ref().unwrap();
assert!(matches!(
state_config.auth,
Some(crate::config::ProviderCredential::ApiKey { ref key })
if key == "environment-test-key"
));
}
#[test]
fn logout_anthropic_without_environment_auth_marks_all_tui_state_missing() {
let env = crate::test_support::env::env_lock();
let _saved_key = env.save("ANTHROPIC_API_KEY");
env.remove_var("ANTHROPIC_API_KEY");
let temp = tempfile::TempDir::new().unwrap();
let (sender, _receiver) = bounded::<TuiEvent>(4);
let mut app = ready_anthropic_app(&temp, sender);
let mut auth = crate::config::Auth::default();
auth.providers.insert(
crate::providers::ANTHROPIC_PROVIDER.to_string(),
crate::config::AuthProviderRecord::ApiKey {
key: "stored-test-key".to_string(),
},
);
crate::config::write_auth(&app.config.paths, &auth).unwrap();
let mut ui_state = state::MissionControlState {
provider: crate::providers::ANTHROPIC_PROVIDER.to_string(),
provider_ready: true,
..Default::default()
};
app.remove_logout_provider(crate::providers::ANTHROPIC_PROVIDER, &mut ui_state);
assert!(!ui_state.provider_ready);
assert!(!app.state.auth_state.is_ready());
assert!(app.config.auth.is_none());
let state_config = app.state.config.as_ref().unwrap();
assert!(state_config.auth.is_none());
}
#[test]
fn provider_refresh_failure_starts_worker_and_finishes_as_worker_error() {
let temp = tempfile::TempDir::new().unwrap();
let (sender, receiver) = bounded::<TuiEvent>(4);
let mut app = test_app(&temp, sender);
let (owner, lines) = crate::herdr::HerdrOwner::new_for_test();
app.state.herdr_reporter = Some(owner.reporter());
app.state.auth_state = crate::config::AuthState::Ready {
provider: crate::providers::OPENAI_CODEX_PROVIDER.to_string(),
credential: crate::config::ProviderCredential::OAuth {
access: "expired-token".to_string(),
account_id: Some("acct".to_string()),
},
};
let mut ui_state = state::MissionControlState {
provider: crate::providers::OPENAI_CODEX_PROVIDER.to_string(),
model: "test-model".to_string(),
provider_ready: true,
..Default::default()
};
assert!(app.submit("hello".to_string(), &mut ui_state, &receiver, test_area()));
assert!(app.worker.is_some());
assert!(app.active_run);
while app
.worker
.as_ref()
.is_some_and(|worker| !worker.handle.is_finished())
{
std::thread::yield_now();
}
let result = drain_tui_events(&receiver, &mut ui_state);
assert!(result.run_finished);
assert!(app.finish_worker_if_ready(&mut ui_state));
assert!(app.worker.is_none());
assert!(!app.active_run);
assert!(!ui_state.provider_ready);
assert!(
ui_state
.status
.starts_with("openai-codex credential refresh failed:")
);
assert_eq!(
herdr_statuses(&lines),
vec![
("working".to_string(), "thinking".to_string()),
("blocked".to_string(), "needs attention".to_string()),
]
);
}
#[test]
fn missing_auth_reports_one_blocked_provider_attempt() {
let temp = tempfile::TempDir::new().unwrap();
let (sender, receiver) = bounded::<TuiEvent>(4);
let mut app = test_app(&temp, sender);
let (owner, lines) = crate::herdr::HerdrOwner::new_for_test();
app.state.herdr_reporter = Some(owner.reporter());
app.state.auth_state = crate::config::AuthState::Missing {
provider: "openai".to_string(),
};
let mut ui_state = state::MissionControlState {
provider: "openai".to_string(),
model: "test-model".to_string(),
..Default::default()
};
assert!(app.submit("hello".to_string(), &mut ui_state, &receiver, test_area()));
while app
.worker
.as_ref()
.is_some_and(|worker| !worker.handle.is_finished())
{
std::thread::yield_now();
}
let result = drain_tui_events(&receiver, &mut ui_state);
assert!(result.run_finished);
assert!(app.finish_worker_if_ready(&mut ui_state));
assert_eq!(ui_state.status, "provider not configured for 'openai'");
assert_eq!(
herdr_statuses(&lines),
vec![
("working".to_string(), "thinking".to_string()),
("blocked".to_string(), "needs attention".to_string()),
]
);
}
#[test]
fn pre_run_cancellation_reaches_tracked_provider_lifecycle_once() {
let temp = tempfile::TempDir::new().unwrap();
let (sender, receiver) = bounded::<TuiEvent>(4);
let mut app = test_app(&temp, sender);
let (owner, lines) = crate::herdr::HerdrOwner::new_for_test();
app.state.herdr_reporter = Some(owner.reporter());
app.state.auth_state = crate::config::AuthState::Missing {
provider: "openai".to_string(),
};
let gate = std::sync::Arc::new(std::sync::Barrier::new(2));
app.worker_start_gate = Some(gate.clone());
let mut ui_state = state::MissionControlState {
provider: "openai".to_string(),
model: "test-model".to_string(),
..Default::default()
};
assert!(app.submit("hello".to_string(), &mut ui_state, &receiver, test_area()));
assert!(app.cancel_active_worker(&mut ui_state).is_some());
gate.wait();
while app
.worker
.as_ref()
.is_some_and(|worker| !worker.handle.is_finished())
{
std::thread::yield_now();
}
let result = drain_tui_events(&receiver, &mut ui_state);
assert!(result.run_finished);
assert!(app.finish_worker_if_ready(&mut ui_state));
assert_eq!(
herdr_statuses(&lines),
vec![
("working".to_string(), "thinking".to_string()),
("idle".to_string(), "cancelled".to_string()),
]
);
}
#[test]
fn tui_custom_provider_connect_persists_metadata_in_worker() {
let temp = tempfile::TempDir::new().unwrap();
let (sender, receiver) = bounded::<TuiEvent>(4);
let mut app = test_app(&temp, sender);
let mut ui_state = state::MissionControlState::default();
app.start_connect_provider(crate::login::CUSTOM_PROVIDER_LOGIN_ID, &mut ui_state);
let connect = ui_state.modals.connect_provider.as_mut().unwrap();
connect.label.set_value("Local AI");
connect.base_url.set_value("http://localhost:8080/v1");
connect.api_key_env_var.set_value("LOCAL_AI_KEY");
app.submit_connect_provider_setup_step(&mut ui_state);
assert_eq!(
ui_state.modals.connect_provider.as_ref().unwrap().stage,
state::ConnectProviderStage::Saving
);
assert!(app.worker.is_some());
reap_provider_worker(&mut app, &mut ui_state, &receiver);
assert_eq!(
ui_state.modals.connect_provider.as_ref().unwrap().stage,
state::ConnectProviderStage::Success
);
let settings = crate::config::read_settings(&app.config.paths).unwrap();
let provider = settings.custom_providers.get("local-ai").unwrap();
assert_eq!(provider.label, "Local AI");
assert_eq!(provider.base_url, "http://localhost:8080/v1");
assert_eq!(provider.api_key_env_var.as_deref(), Some("LOCAL_AI_KEY"));
}
#[test]
fn tui_custom_provider_connect_reconciles_saturated_completion_once() {
let temp = tempfile::TempDir::new().unwrap();
let (sender, receiver) = bounded::<TuiEvent>(1);
sender
.send(TuiEvent::Error("queue fill".to_string()))
.unwrap();
let mut app = test_app(&temp, sender);
let provider_id = "local-ai";
let model = "model-a";
app.config.provider = Some(provider_id.to_string());
app.config.model = Some(model.to_string());
app.config.auth = Some(crate::config::ProviderCredential::NoAuth);
app.state.config = Some(app.config.clone());
app.state.model = model.to_string();
app.state.auth_state = crate::config::AuthState::Ready {
provider: provider_id.to_string(),
credential: crate::config::ProviderCredential::NoAuth,
};
let mut ui_state = state::MissionControlState {
provider: provider_id.to_string(),
model: model.to_string(),
provider_ready: false,
..Default::default()
};
app.start_connect_provider(crate::login::CUSTOM_PROVIDER_LOGIN_ID, &mut ui_state);
let connect = ui_state.modals.connect_provider.as_mut().unwrap();
connect.label.set_value("Local AI");
connect.base_url.set_value("http://localhost:8080/v1");
app.submit_connect_provider_setup_step(&mut ui_state);
assert_eq!(
ui_state.modals.connect_provider.as_ref().unwrap().stage,
state::ConnectProviderStage::Saving
);
let deadline = Instant::now() + Duration::from_secs(5);
while !app
.worker
.as_ref()
.is_some_and(|worker| worker.handle.is_finished())
{
assert!(Instant::now() < deadline, "provider worker did not finish");
thread::yield_now();
}
assert!(
app.worker
.as_ref()
.unwrap()
.outcome
.completion_delivery_failed()
);
assert!(app.finish_worker_if_ready(&mut ui_state));
let saved = crate::config::read_settings(&app.config.paths).unwrap();
let saved_provider = saved.custom_providers.get(provider_id).unwrap();
assert_eq!(saved_provider.label, "Local AI");
assert_eq!(saved_provider.base_url, "http://localhost:8080/v1");
assert_eq!(saved_provider.api_key_env_var, None);
let connect = ui_state.modals.connect_provider.as_ref().unwrap();
assert_eq!(connect.stage, state::ConnectProviderStage::Success);
assert!(!connect.runtime_refresh_pending);
assert!(ui_state.provider_ready);
assert_eq!(ui_state.provider, provider_id);
assert_eq!(
app.config.custom_providers.get(provider_id),
Some(saved_provider)
);
assert_eq!(
app.state
.config
.as_ref()
.unwrap()
.custom_providers
.get(provider_id),
Some(saved_provider)
);
let refreshed_config = app.config.clone();
let refreshed_state_config = app.state.config.clone();
app.refresh_runtime_after_custom_provider(provider_id, &mut ui_state);
assert_eq!(app.config, refreshed_config);
assert_eq!(app.state.config, refreshed_state_config);
assert!(matches!(
receiver.try_recv(),
Ok(TuiEvent::Error(message)) if message == "queue fill"
));
assert!(receiver.try_recv().is_err());
}
#[test]
fn tui_custom_provider_rejects_secret_like_env_value_without_persisting_it() {
let temp = tempfile::TempDir::new().unwrap();
let (sender, receiver) = bounded::<TuiEvent>(4);
let mut app = test_app(&temp, sender);
let mut ui_state = state::MissionControlState::default();
let fake_secret = "sk-rejected-persistence-secret";
app.start_connect_provider(crate::login::CUSTOM_PROVIDER_LOGIN_ID, &mut ui_state);
let connect = ui_state.modals.connect_provider.as_mut().unwrap();
connect.label.set_value("Rejected Provider");
connect.base_url.set_value("http://localhost:8080/v1");
connect.api_key_env_var.set_value(fake_secret);
app.submit_connect_provider_setup_step(&mut ui_state);
let connect = ui_state.modals.connect_provider.as_ref().unwrap();
assert_eq!(connect.stage, state::ConnectProviderStage::ConfigureCustom);
assert!(
connect
.error
.as_deref()
.is_some_and(|error| error.contains("looks like a secret value"))
);
assert!(!ui_state.status.contains(fake_secret));
assert!(app.worker.is_none());
assert!(!app.active_run);
assert!(
crate::config::read_settings(&app.config.paths)
.unwrap()
.custom_providers
.is_empty()
);
assert!(receiver.try_recv().is_err());
}
#[test]
fn tui_custom_provider_connect_blank_env_var_persists_no_auth_without_key_instruction() {
let temp = tempfile::TempDir::new().unwrap();
let (sender, receiver) = bounded::<TuiEvent>(4);
let mut app = test_app(&temp, sender);
let mut ui_state = state::MissionControlState::default();
app.start_connect_provider(crate::login::CUSTOM_PROVIDER_LOGIN_ID, &mut ui_state);
let connect = ui_state.modals.connect_provider.as_mut().unwrap();
connect.label.set_value("Local Llama");
connect.base_url.set_value("http://localhost:11434/v1");
app.submit_connect_provider_setup_step(&mut ui_state);
reap_provider_worker(&mut app, &mut ui_state, &receiver);
assert_eq!(
ui_state.modals.connect_provider.as_ref().unwrap().stage,
state::ConnectProviderStage::Success
);
assert!(!ui_state.status.contains("Set/export"));
let settings = crate::config::read_settings(&app.config.paths).unwrap();
let provider = settings.custom_providers.get("local-llama").unwrap();
assert_eq!(provider.api_key_env_var, None);
}
#[test]
fn tui_duplicate_custom_provider_connect_cancel_preserves_record() {
let temp = tempfile::TempDir::new().unwrap();
let (sender, _receiver) = bounded::<TuiEvent>(4);
let mut app = test_app(&temp, sender);
let mut ui_state = state::MissionControlState::default();
crate::login::configure_custom_provider(
&app.config.paths,
"local-ai",
"Old Local AI",
"http://localhost:8080/v1",
"OLD_LOCAL_AI_KEY",
)
.unwrap();
app.start_connect_provider(crate::login::CUSTOM_PROVIDER_LOGIN_ID, &mut ui_state);
let connect = ui_state.modals.connect_provider.as_mut().unwrap();
connect.label.set_value("Local AI");
connect.base_url.set_value("http://localhost:9090/v1");
connect.api_key_env_var.set_value("NEW_LOCAL_AI_KEY");
app.submit_connect_provider_setup_step(&mut ui_state);
let connect = ui_state.modals.connect_provider.as_ref().unwrap();
let replacement = connect.replacement.as_ref().unwrap();
assert_eq!(replacement.provider_id, "local-ai");
assert_eq!(replacement.existing_label, "Old Local AI");
assert_eq!(replacement.label, "Local AI");
assert_eq!(
connect.stage,
state::ConnectProviderStage::ReplacementConfirmation
);
assert!(ui_state.back_connect_provider());
ui_state.status = "custom provider replacement cancelled; edit the form or cancel".to_string();
let connect = ui_state.modals.connect_provider.as_ref().unwrap();
assert_eq!(connect.stage, state::ConnectProviderStage::ConfigureCustom);
assert!(connect.replacement.is_none());
let settings = crate::config::read_settings(&app.config.paths).unwrap();
let provider = settings.custom_providers.get("local-ai").unwrap();
assert_eq!(provider.label, "Old Local AI");
assert_eq!(provider.base_url, "http://localhost:8080/v1");
assert_eq!(
provider.api_key_env_var.as_deref(),
Some("OLD_LOCAL_AI_KEY")
);
}
#[test]
fn tui_duplicate_custom_provider_connect_confirm_replaces_record() {
let temp = tempfile::TempDir::new().unwrap();
let (sender, receiver) = bounded::<TuiEvent>(4);
let mut app = test_app(&temp, sender);
let mut ui_state = state::MissionControlState::default();
crate::login::configure_custom_provider(
&app.config.paths,
"local-ai",
"Old Local AI",
"http://localhost:8080/v1",
"OLD_LOCAL_AI_KEY",
)
.unwrap();
app.start_connect_provider(crate::login::CUSTOM_PROVIDER_LOGIN_ID, &mut ui_state);
let connect = ui_state.modals.connect_provider.as_mut().unwrap();
connect.label.set_value("Local AI");
connect.base_url.set_value("http://localhost:9090/v1");
connect.api_key_env_var.set_value("NEW_LOCAL_AI_KEY");
app.submit_connect_provider_setup_step(&mut ui_state);
app.confirm_connect_provider_replacement(&mut ui_state);
reap_provider_worker(&mut app, &mut ui_state, &receiver);
assert_eq!(
ui_state.modals.connect_provider.as_ref().unwrap().stage,
state::ConnectProviderStage::Success
);
let settings = crate::config::read_settings(&app.config.paths).unwrap();
let provider = settings.custom_providers.get("local-ai").unwrap();
assert_eq!(provider.label, "Local AI");
assert_eq!(provider.base_url, "http://localhost:9090/v1");
assert_eq!(
provider.api_key_env_var.as_deref(),
Some("NEW_LOCAL_AI_KEY")
);
}
#[test]
fn tui_active_custom_provider_reconfiguration_refreshes_runtime() {
let temp = tempfile::TempDir::new().unwrap();
let (sender, receiver) = bounded::<TuiEvent>(4);
let mut app = test_app(&temp, sender);
let provider_id = "local-ai";
let model = "model-a";
let custom = crate::config::CustomProviderConfig {
label: "Old Local AI".to_string(),
base_url: "http://localhost:8080/v1".to_string(),
fast_mode: None,
api_key_env_var: None,
models_dev_provider: Some("local-models".to_string()),
use_responses_endpoint: true,
supports_text_verbosity: false,
reasoning_protocol: crate::config::CustomReasoningProtocol::AnthropicLike,
extra_models: vec!["extra-model".to_string()],
request_headers: Default::default(),
};
crate::config::write_settings(
&app.config.paths,
&crate::config::Settings {
custom_providers: std::collections::BTreeMap::from([(
provider_id.to_string(),
custom.clone(),
)]),
selected_model: crate::config::SelectedModelSettings {
provider: Some(provider_id.to_string()),
model: Some(model.to_string()),
..Default::default()
},
..Default::default()
},
)
.unwrap();
let mut catalog_entry = crate::model_catalog::ModelCatalogEntry::new(provider_id, model);
catalog_entry.supports_reasoning = Some(true);
catalog_entry.reasoning_efforts = Some(vec![
crate::thinking::ThinkingLevel::Default,
crate::thinking::ThinkingLevel::High,
crate::thinking::ThinkingLevel::Max,
]);
crate::model_catalog::write_catalog_cache_for_configured_provider(
&app.config.paths,
provider_id,
&[catalog_entry],
)
.unwrap();
app.config.provider = Some(provider_id.to_string());
app.config.model = Some(model.to_string());
app.config.custom_providers =
std::collections::BTreeMap::from([(provider_id.to_string(), custom.clone())]);
app.config.auth = Some(crate::config::ProviderCredential::NoAuth);
app.state.config = Some(app.config.clone());
app.state.model = model.to_string();
app.state.auth_state = crate::config::AuthState::Ready {
provider: provider_id.to_string(),
credential: crate::config::ProviderCredential::NoAuth,
};
let mut ui_state = state::MissionControlState {
provider: provider_id.to_string(),
model: model.to_string(),
provider_ready: true,
..Default::default()
};
app.start_connect_provider(crate::login::CUSTOM_PROVIDER_LOGIN_ID, &mut ui_state);
let connect = ui_state.modals.connect_provider.as_mut().unwrap();
connect.label.set_value("Local AI");
connect.base_url.set_value("http://localhost:9090/v1");
app.submit_connect_provider_setup_step(&mut ui_state);
app.confirm_connect_provider_replacement(&mut ui_state);
reap_provider_worker(&mut app, &mut ui_state, &receiver);
let saved = crate::config::read_settings(&app.config.paths).unwrap();
let saved_provider = saved.custom_providers.get(provider_id).unwrap();
assert_eq!(saved_provider.base_url, "http://localhost:9090/v1");
assert_eq!(
saved_provider.models_dev_provider.as_deref(),
Some("local-models")
);
assert!(saved_provider.use_responses_endpoint);
assert_eq!(
saved_provider.reasoning_protocol,
crate::config::CustomReasoningProtocol::AnthropicLike
);
assert_eq!(saved_provider.extra_models, vec!["extra-model"]);
assert_eq!(
app.config.custom_providers.get(provider_id),
Some(saved_provider)
);
assert_eq!(
app.state
.config
.as_ref()
.unwrap()
.custom_providers
.get(provider_id),
Some(saved_provider)
);
assert!(ui_state.provider_ready);
assert_eq!(ui_state.provider, provider_id);
assert_eq!(ui_state.model, model);
for level in [
crate::thinking::ThinkingLevel::Default,
crate::thinking::ThinkingLevel::High,
crate::thinking::ThinkingLevel::Max,
] {
assert!(
ui_state.thinking_levels.contains(&level),
"missing {level:?}"
);
}
}