use super::*;
use crate::app::orchestrator::engines::{RESTART_BUDGET, Server};
use crate::shared::config::{CloudProvider, ServerMode};
use crate::shared::secrets::{ExternalSlot, SecretKey};
use crate::shared::server::ServerStatus;
#[tokio::test]
async fn bootstrap_emits_settings_snapshot() {
let (_d, cmd_tx, mut evt_rx, handle) = spawn_orch(None);
let ev = wait_for(&mut evt_rx, |e| matches!(e, AppEvent::Settings { .. }))
.await
.unwrap();
if let AppEvent::Settings {
config, profiles, ..
} = ev
{
assert_eq!(config.schema_version, AppConfig::default().schema_version);
assert_eq!(profiles.len(), 1, "the default profile is in the snapshot");
}
drop(cmd_tx);
handle.await.unwrap();
}
#[tokio::test]
async fn update_config_persists_and_reemits_settings() {
let (_d, cmd_tx, mut evt_rx, handle) = spawn_orch(None);
let root = _d.path().to_path_buf();
wait_for(&mut evt_rx, |e| matches!(e, AppEvent::Settings { .. }))
.await
.unwrap();
let config = AppConfig {
max_tool_rounds: 3,
..Default::default()
};
cmd_tx
.send(AppCommand::UpdateConfig(Box::new(config)))
.unwrap();
wait_for(
&mut evt_rx,
|e| matches!(e, AppEvent::Settings { config, .. } if config.max_tool_rounds == 3),
)
.await
.unwrap();
cmd_tx.send(AppCommand::Quit).unwrap();
handle.await.unwrap();
let reopened = Storage::open(Paths::with_root(&root)).unwrap();
assert_eq!(reopened.json().load_config().unwrap().max_tool_rounds, 3);
}
#[tokio::test]
async fn update_profile_persists_edit() {
let (_d, cmd_tx, mut evt_rx, handle) = spawn_orch(None);
let ev = wait_for(&mut evt_rx, |e| matches!(e, AppEvent::Settings { .. }))
.await
.unwrap();
let id = match ev {
AppEvent::Settings { profiles, .. } => profiles[0].id,
_ => unreachable!(),
};
cmd_tx
.send(AppCommand::UpdateProfile {
id,
edit: Box::new(ProfileEdit {
system_message: Some("новое sys".into()),
..Default::default()
}),
})
.unwrap();
wait_for(&mut evt_rx, |e| {
matches!(e, AppEvent::Settings { profiles, .. }
if profiles.iter().any(|p| p.default_system_message == "новое sys"))
})
.await
.unwrap();
cmd_tx.send(AppCommand::Quit).unwrap();
handle.await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn model_change_restarts_chat_server_debounced() {
let backend = Arc::new(MockBackend::scripted(vec![ChatChunk::Finished(
FinishReason::Stop,
)])) as Arc<dyn EngineBackend>;
let sup = Arc::new(MockSupervisor::with_backend(Some(backend)));
let dir = tempfile::tempdir().unwrap();
let storage = Arc::new(Storage::open(Paths::with_root(dir.path())).unwrap());
let (cmd_tx, cmd_rx) = unbounded_channel();
let (evt_tx, mut evt_rx) = unbounded_channel();
let handle = tokio::spawn(run(OrchestratorDeps {
cmd_rx,
evt_tx,
storage,
config: AppConfig::default(),
supervisor: sup.clone(),
default_language: crate::shared::i18n::Lang::default(),
extra_tools: Vec::new(),
}));
wait_for(&mut evt_rx, |e| matches!(e, AppEvent::Settings { .. }))
.await
.unwrap();
assert_eq!(sup.chat_call_count(), 1);
let engine1 = crate::shared::config::EngineSettings {
managed: crate::shared::config::ManagedSettings {
model_path: Some("other.gguf".into()),
..Default::default()
},
..Default::default()
};
let mut engine2 = engine1.clone();
engine2.managed.gpu_layers = 10;
cmd_tx
.send(AppCommand::UpdateConfig(Box::new(AppConfig {
engine: engine1,
..Default::default()
})))
.unwrap();
cmd_tx
.send(AppCommand::UpdateConfig(Box::new(AppConfig {
engine: engine2,
..Default::default()
})))
.unwrap();
wait_for(
&mut evt_rx,
|e| matches!(e, AppEvent::Settings { config, .. } if config.engine.managed.gpu_layers == 10),
)
.await
.unwrap();
assert_eq!(
sup.chat_call_count(),
1,
"the restart is deferred by the debounce, the config is applied right away"
);
wait_for(&mut evt_rx, |e| matches!(e, AppEvent::ServerStatus(_)))
.await
.unwrap();
assert_eq!(
sup.chat_call_count(),
2,
"two engine edits → one deferred server restart"
);
cmd_tx.send(AppCommand::Quit).unwrap();
handle.await.unwrap();
}
#[tokio::test(start_paused = true)]
async fn an_edit_and_its_undo_cost_no_restart() {
let backend = Arc::new(MockBackend::scripted(vec![ChatChunk::Finished(
FinishReason::Stop,
)])) as Arc<dyn EngineBackend>;
let sup = Arc::new(MockSupervisor::with_backend(Some(backend)));
let dir = tempfile::tempdir().unwrap();
let storage = Arc::new(Storage::open(Paths::with_root(dir.path())).unwrap());
let (cmd_tx, cmd_rx) = unbounded_channel();
let (evt_tx, mut evt_rx) = unbounded_channel();
let handle = tokio::spawn(run(OrchestratorDeps {
cmd_rx,
evt_tx,
storage,
config: AppConfig::default(),
supervisor: sup.clone(),
default_language: crate::shared::i18n::Lang::default(),
extra_tools: Vec::new(),
}));
wait_for(&mut evt_rx, |e| matches!(e, AppEvent::Settings { .. }))
.await
.unwrap();
assert_eq!(sup.chat_call_count(), 1, "bootstrap raised it once");
let edited = crate::shared::config::EngineSettings {
managed: crate::shared::config::ManagedSettings {
model_path: Some("other.gguf".into()),
..Default::default()
},
..Default::default()
};
cmd_tx
.send(AppCommand::UpdateConfig(Box::new(AppConfig {
engine: edited,
..Default::default()
})))
.unwrap();
cmd_tx
.send(AppCommand::UpdateConfig(Box::default()))
.unwrap();
wait_for(
&mut evt_rx,
|e| matches!(e, AppEvent::Settings { config, .. } if config.engine == AppConfig::default().engine),
)
.await
.unwrap();
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
assert_eq!(
sup.chat_call_count(),
1,
"the config came back to what the server is already running — nothing to do"
);
let changed = crate::shared::config::EngineSettings {
managed: crate::shared::config::ManagedSettings {
gpu_layers: 10,
..Default::default()
},
..Default::default()
};
cmd_tx
.send(AppCommand::UpdateConfig(Box::new(AppConfig {
engine: changed,
..Default::default()
})))
.unwrap();
wait_for(&mut evt_rx, |e| matches!(e, AppEvent::ServerStatus(_)))
.await
.unwrap();
assert_eq!(
sup.chat_call_count(),
2,
"exactly one restart, for the change that actually differed"
);
cmd_tx.send(AppCommand::Quit).unwrap();
handle.await.unwrap();
}
#[tokio::test]
async fn set_api_key_persists_encrypted_and_reads_back() {
if !crate::shared::secrets::scheme_available() {
return; }
let (_d, cmd_tx, mut evt_rx, handle) = spawn_orch(None);
let root = _d.path().to_path_buf();
wait_for(&mut evt_rx, |e| matches!(e, AppEvent::Settings { .. }))
.await
.unwrap();
cmd_tx
.send(AppCommand::SetSecret {
key: SecretKey::Provider(CloudProvider::OpenAi),
value: "sk-super-secret-42".into(),
})
.unwrap();
wait_for(
&mut evt_rx,
|e| matches!(e, AppEvent::Settings { secrets_present, .. } if !secrets_present.is_empty()),
)
.await
.unwrap();
cmd_tx.send(AppCommand::Quit).unwrap();
handle.await.unwrap();
let raw = std::fs::read_to_string(root.join("settings.json")).unwrap();
assert!(
!raw.contains("sk-super-secret-42"),
"the key's plaintext leaked into settings.json"
);
assert!(raw.contains("api_keys"), "the key entry wasn't saved");
let reopened = Storage::open(Paths::with_root(&root)).unwrap();
let cfg = reopened.json().load_config().unwrap();
assert_eq!(
crate::shared::secrets::stored_key(&cfg.api_keys, CloudProvider::OpenAi.key()).as_deref(),
Some("sk-super-secret-42")
);
}
#[tokio::test(start_paused = true)]
async fn set_external_key_reraises_that_server_with_the_key() {
if !crate::shared::secrets::scheme_available() {
return; }
let config = AppConfig {
engine: crate::shared::config::EngineSettings {
mode: ServerMode::External,
external: crate::shared::config::ExternalSettings {
url: Some("http://127.0.0.1:9/v1".into()),
..Default::default()
},
..Default::default()
},
..Default::default()
};
let (_d, sup, cmd_tx, mut evt_rx, handle) = spawn_orch_sup(config);
wait_for(&mut evt_rx, |e| matches!(e, AppEvent::Settings { .. }))
.await
.unwrap();
assert_eq!(sup.chat_call_count(), 1, "bootstrap raised it once");
assert_eq!(
sup.chat_keys(),
vec![None],
"nothing is stored yet, so the supervisor falls back to env"
);
cmd_tx
.send(AppCommand::SetSecret {
key: SecretKey::External(ExternalSlot::Chat),
value: "sk-gateway-1".into(),
})
.unwrap();
wait_for(&mut evt_rx, |e| {
matches!(e, AppEvent::Settings { secrets_present, .. }
if secrets_present.contains(&SecretKey::External(ExternalSlot::Chat)))
})
.await
.unwrap();
wait_for(&mut evt_rx, |e| matches!(e, AppEvent::ServerStatus(_)))
.await
.unwrap();
assert_eq!(
sup.chat_keys(),
vec![None, Some("sk-gateway-1".into())],
"the chat server came back up with the external slot's key"
);
for slot in [ExternalSlot::Tts, ExternalSlot::Embed] {
cmd_tx
.send(AppCommand::SetSecret {
key: SecretKey::External(slot),
value: format!("sk-{slot:?}-2"),
})
.unwrap();
wait_for(&mut evt_rx, |e| {
matches!(e, AppEvent::Settings { secrets_present, .. }
if secrets_present.contains(&SecretKey::External(slot)))
})
.await
.unwrap();
}
wait_for(&mut evt_rx, |e| matches!(e, AppEvent::ServerStatus(_)))
.await
.unwrap();
assert_eq!(
sup.chat_call_count(),
2,
"neither the speech nor the embedding key may restart the chat server"
);
cmd_tx.send(AppCommand::Quit).unwrap();
handle.await.unwrap();
}
#[tokio::test]
async fn set_backup_password_persists_encrypted_and_reads_back() {
if !crate::shared::secrets::scheme_available() {
return; }
let (_d, cmd_tx, mut evt_rx, handle) = spawn_orch(None);
let root = _d.path().to_path_buf();
wait_for(&mut evt_rx, |e| {
matches!(e, AppEvent::Settings { secrets_present, .. } if !secrets_present.contains(&SecretKey::BackupPassword))
})
.await
.unwrap();
cmd_tx
.send(AppCommand::SetSecret {
key: SecretKey::BackupPassword,
value: "open-sesame-42".into(),
})
.unwrap();
wait_for(&mut evt_rx, |e| {
matches!(e, AppEvent::Settings { secrets_present, .. } if secrets_present.contains(&SecretKey::BackupPassword))
})
.await
.unwrap();
cmd_tx.send(AppCommand::Quit).unwrap();
handle.await.unwrap();
let raw = std::fs::read_to_string(root.join("settings.json")).unwrap();
assert!(
!raw.contains("open-sesame-42"),
"the backup password leaked into settings.json in the clear"
);
let reopened = Storage::open(Paths::with_root(&root)).unwrap();
let cfg = reopened.json().load_config().unwrap();
assert_eq!(
crate::shared::secrets::stored_key(
&cfg.api_keys,
crate::shared::secrets::BACKUP_PASSWORD_KEY
)
.as_deref(),
Some("open-sesame-42")
);
}
#[tokio::test]
async fn update_config_preserves_stored_api_keys() {
if !crate::shared::secrets::scheme_available() {
return;
}
let (_d, cmd_tx, mut evt_rx, handle) = spawn_orch(None);
let root = _d.path().to_path_buf();
wait_for(&mut evt_rx, |e| matches!(e, AppEvent::Settings { .. }))
.await
.unwrap();
cmd_tx
.send(AppCommand::SetSecret {
key: SecretKey::Provider(CloudProvider::Claude),
value: "sk-ant-keep-me".into(),
})
.unwrap();
wait_for(
&mut evt_rx,
|e| matches!(e, AppEvent::Settings { secrets_present, .. } if !secrets_present.is_empty()),
)
.await
.unwrap();
cmd_tx
.send(AppCommand::UpdateConfig(Box::new(AppConfig {
max_tool_rounds: 5,
..Default::default()
})))
.unwrap();
wait_for(
&mut evt_rx,
|e| matches!(e, AppEvent::Settings { config, .. } if config.max_tool_rounds == 5),
)
.await
.unwrap();
cmd_tx.send(AppCommand::Quit).unwrap();
handle.await.unwrap();
let reopened = Storage::open(Paths::with_root(&root)).unwrap();
let cfg = reopened.json().load_config().unwrap();
assert_eq!(
crate::shared::secrets::stored_key(&cfg.api_keys, CloudProvider::Claude.key()).as_deref(),
Some("sk-ant-keep-me"),
"editing settings erased the saved key"
);
}
#[tokio::test]
async fn set_empty_api_key_removes_stored_entry() {
if !crate::shared::secrets::scheme_available() {
return;
}
let (_d, cmd_tx, mut evt_rx, handle) = spawn_orch(None);
wait_for(&mut evt_rx, |e| matches!(e, AppEvent::Settings { .. }))
.await
.unwrap();
cmd_tx
.send(AppCommand::SetSecret {
key: SecretKey::Provider(CloudProvider::Gemini),
value: "sk-temp".into(),
})
.unwrap();
wait_for(
&mut evt_rx,
|e| matches!(e, AppEvent::Settings { secrets_present, .. } if !secrets_present.is_empty()),
)
.await
.unwrap();
cmd_tx
.send(AppCommand::SetSecret {
key: SecretKey::Provider(CloudProvider::Gemini),
value: String::new(),
})
.unwrap();
wait_for(
&mut evt_rx,
|e| matches!(e, AppEvent::Settings { secrets_present, .. } if secrets_present.is_empty()),
)
.await
.unwrap();
cmd_tx.send(AppCommand::Quit).unwrap();
handle.await.unwrap();
}
#[tokio::test]
async fn settings_snapshot_carries_flags_not_secrets() {
if !crate::shared::secrets::scheme_available() {
return;
}
let (_d, cmd_tx, mut evt_rx, handle) = spawn_orch(None);
wait_for(&mut evt_rx, |e| matches!(e, AppEvent::Settings { .. }))
.await
.unwrap();
cmd_tx
.send(AppCommand::SetSecret {
key: SecretKey::Provider(CloudProvider::OpenAi),
value: "sk-in-snapshot-test".into(),
})
.unwrap();
let ev = wait_for(
&mut evt_rx,
|e| matches!(e, AppEvent::Settings { secrets_present, .. } if !secrets_present.is_empty()),
)
.await
.unwrap();
if let AppEvent::Settings {
config,
secrets_present,
..
} = ev
{
assert_eq!(
secrets_present,
vec![SecretKey::Provider(CloudProvider::OpenAi)]
);
assert!(
config.api_keys.is_empty(),
"the UI snapshot must not carry key entries"
);
}
cmd_tx.send(AppCommand::Quit).unwrap();
handle.await.unwrap();
}
#[tokio::test]
async fn dead_managed_server_is_relaunched_until_the_budget_runs_out() {
let (_d, mut orch) = bare_orch();
orch.config.engine.mode = ServerMode::Managed;
for round in 0..RESTART_BUDGET {
orch.engines
.set_chat_status(ServerStatus::Disconnected("process gone".into()));
orch.relaunch_dead_managed_servers();
assert!(
!matches!(
orch.engines.status_of(Server::Chat),
ServerStatus::Disconnected(_)
),
"round {round}: a dead managed server should have been relaunched"
);
}
orch.engines
.set_chat_status(ServerStatus::Disconnected("process gone".into()));
orch.relaunch_dead_managed_servers();
assert!(
matches!(
orch.engines.status_of(Server::Chat),
ServerStatus::Disconnected(_)
),
"the crash-loop guard should stop relaunching"
);
}
#[tokio::test]
async fn external_server_is_never_relaunched() {
let (_d, mut orch) = bare_orch();
orch.config.engine.mode = ServerMode::External;
orch.engines
.set_chat_status(ServerStatus::Disconnected("host down".into()));
orch.relaunch_dead_managed_servers();
assert!(
matches!(
orch.engines.status_of(Server::Chat),
ServerStatus::Disconnected(_)
),
"an external server must not be relaunched by us"
);
}
#[test]
fn a_stored_search_key_beats_the_named_environment_variable() {
use crate::shared::secrets::SearchSlot;
const VAR: &str = "MINDFORK_TEST_TAVILY_KEY_ENV";
unsafe { std::env::set_var(VAR, "from-the-environment") };
let mut cfg = crate::shared::config::AppConfig::default();
cfg.tools.web_tavily_key_env = Some(VAR.into());
assert_eq!(
super::super::web_search_keys(&cfg),
vec![(SearchSlot::Tavily, "from-the-environment".to_string())]
);
crate::shared::secrets::put_key(
&mut cfg.api_keys,
&crate::shared::secrets::SecretKey::Search(SearchSlot::Tavily).storage_name(),
"from-settings",
|| "test".to_string(),
)
.expect("the machine key scheme must be available in tests");
assert_eq!(
super::super::web_search_keys(&cfg),
vec![(SearchSlot::Tavily, "from-settings".to_string())]
);
unsafe { std::env::remove_var(VAR) };
}
#[test]
fn an_unconfigured_search_provider_yields_no_key() {
let mut cfg = crate::shared::config::AppConfig::default();
cfg.tools.web_tavily_key_env = None;
assert!(super::super::web_search_keys(&cfg).is_empty());
cfg.tools.web_tavily_key_env = Some(" ".into());
assert!(super::super::web_search_keys(&cfg).is_empty());
}
#[tokio::test]
async fn a_stored_search_key_shows_as_present() {
use crate::shared::secrets::SearchSlot;
if !crate::shared::secrets::scheme_available() {
return; }
let (_d, cmd_tx, mut evt_rx, handle) = spawn_orch(None);
wait_for(&mut evt_rx, |e| matches!(e, AppEvent::Settings { .. }))
.await
.unwrap();
for slot in SearchSlot::ALL {
cmd_tx
.send(AppCommand::SetSecret {
key: SecretKey::Search(slot),
value: format!("key-for-{slot:?}"),
})
.unwrap();
wait_for(&mut evt_rx, |e| {
matches!(e, AppEvent::Settings { secrets_present, .. }
if secrets_present.contains(&SecretKey::Search(slot)))
})
.await
.unwrap_or_else(|| panic!("{slot:?} never showed as present"));
}
cmd_tx.send(AppCommand::Quit).unwrap();
handle.await.unwrap();
}