use super::*;
use crate::app::supervisor::LlamaSupervisor;
use crate::shared::config::{CloudSettings, ExternalSettings, ServerMode};
fn spawn_on_the_real_supervisor(config: AppConfig) -> OrchHandle {
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, evt_rx) = unbounded_channel();
let handle = tokio::spawn(run(OrchestratorDeps {
cmd_rx,
evt_tx,
storage,
config,
supervisor: Arc::new(LlamaSupervisor::default()),
default_language: crate::shared::i18n::Lang::default(),
extra_tools: Vec::new(),
}));
(dir, cmd_tx, evt_rx, handle)
}
type Facts = (Option<Arc<[String]>>, Option<u32>);
async fn engine_facts(
rx: &mut UnboundedReceiver<AppEvent>,
settled: impl Fn(&Facts) -> bool,
) -> Facts {
let (mut ready, mut fields, mut slots) = (false, None, None);
let wait = async {
loop {
match rx.recv().await.expect("the event stream") {
AppEvent::ServerStatus(statuses) => {
ready = statuses.chat == ServerStatus::Ready;
(fields, slots) = (None, None);
}
AppEvent::EngineSamplingFields(f) => fields = Some(f),
AppEvent::EngineSlots(s) => slots = Some(s),
_ => continue,
}
if let (true, Some(f), Some(s)) = (ready, &fields, &slots) {
let facts = (f.clone(), *s);
if settled(&facts) {
return facts;
}
}
}
};
let facts = tokio::time::timeout(std::time::Duration::from_secs(90), wait).await;
facts.unwrap_or_else(|_| {
panic!("the engine never settled: ready={ready} fields={fields:?} slots={slots:?}")
})
}
async fn turn(
cmd_tx: &UnboundedSender<AppCommand>,
rx: &mut UnboundedReceiver<AppEvent>,
text: &str,
) -> String {
cmd_tx.send(AppCommand::SendMessage(text.into())).unwrap();
let mut out = String::new();
let reply = async {
loop {
match rx.recv().await.expect("the event stream") {
AppEvent::Chunk { text, .. } => out.push_str(&text),
AppEvent::Error(said) => panic!("the turn failed: {said}"),
AppEvent::Finished { .. } => return,
_ => {}
}
}
};
tokio::time::timeout(std::time::Duration::from_secs(300), reply)
.await
.expect("the turn ends");
out
}
fn of_a_local_server(facts: &Facts) -> bool {
facts.0.is_none() && facts.1.is_some()
}
fn of_the_gateway(facts: &Facts) -> bool {
facts.0.is_some() && facts.1.is_none()
}
#[tokio::test]
#[ignore = "requires MINDFORK_ENGINE_URL (a llama.cpp, no key) and MINDFORK_OPENROUTER_KEY; MINDFORK_OPENROUTER_MODEL names the gateway's model"]
async fn a_chat_moves_to_the_gateway_and_back_in_one_session_live() {
let Ok(local) = std::env::var("MINDFORK_ENGINE_URL") else {
eprintln!("skip: MINDFORK_ENGINE_URL not set");
return;
};
if std::env::var("MINDFORK_OPENROUTER_KEY").map_or(true, |k| k.trim().is_empty()) {
eprintln!("skip: MINDFORK_OPENROUTER_KEY not set");
return;
}
let slug = std::env::var("MINDFORK_OPENROUTER_MODEL")
.unwrap_or_else(|_| "anthropic/claude-haiku-4.5".into());
let mut on_the_local_server = no_auto_cfg();
on_the_local_server.engine.mode = ServerMode::External;
on_the_local_server.engine.external = ExternalSettings {
url: Some(local.clone()),
..Default::default()
};
on_the_local_server.engine.openrouter = CloudSettings {
model_name: Some(slug.clone()),
api_key_env: Some("MINDFORK_OPENROUTER_KEY".into()),
..Default::default()
};
on_the_local_server.default_sampling.thinking = Some(false);
let mut through_the_gateway = on_the_local_server.clone();
through_the_gateway.engine.mode = ServerMode::OpenRouter;
let (dir, cmd_tx, mut rx, handle) = spawn_on_the_real_supervisor(on_the_local_server.clone());
let root = dir.path().to_path_buf();
let chat_id = match wait_for(&mut rx, |e| matches!(e, AppEvent::ChatActivated { .. }))
.await
.expect("a chat")
{
AppEvent::ChatActivated { id, .. } => id,
_ => unreachable!(),
};
let question = "Answer with one word: what is the capital of France?";
let (fields, slots) = engine_facts(&mut rx, of_a_local_server).await;
eprintln!("local: fields={fields:?} slots={slots:?}");
let first = turn(&cmd_tx, &mut rx, question).await;
eprintln!("local: {first:?}");
assert!(first.to_lowercase().contains("paris"), "{first:?}");
cmd_tx
.send(AppCommand::UpdateConfig(Box::new(
through_the_gateway.clone(),
)))
.unwrap();
let (fields, slots) = engine_facts(&mut rx, of_the_gateway).await;
eprintln!("gateway: fields={fields:?} slots={slots:?}");
let fields = fields.expect("the gateway publishes what the model takes");
assert!(fields.iter().any(|f| f == "temperature"), "{fields:?}");
assert!(
!fields
.iter()
.any(|f| f == "repeat_penalty" || f.starts_with("mirostat")),
"the list is in the gateway's words: {fields:?}"
);
let second = turn(&cmd_tx, &mut rx, question).await;
eprintln!("gateway: {second:?}");
assert!(second.to_lowercase().contains("paris"), "{second:?}");
cmd_tx
.send(AppCommand::UpdateConfig(Box::new(on_the_local_server)))
.unwrap();
let (fields, slots) = engine_facts(&mut rx, of_a_local_server).await;
eprintln!("local again: fields={fields:?} slots={slots:?}");
let third = turn(&cmd_tx, &mut rx, question).await;
eprintln!("local again: {third:?}");
assert!(third.to_lowercase().contains("paris"), "{third:?}");
cmd_tx.send(AppCommand::Quit).unwrap();
handle.await.unwrap();
let reopened = Storage::open(Paths::with_root(&root)).unwrap();
let chat = reopened.json().load_chat(chat_id).unwrap().unwrap();
let replies: Vec<_> = chat
.messages
.iter()
.filter(|m| m.role == MessageRole::Assistant)
.filter_map(|m| m.metadata.clone())
.collect();
for meta in &replies {
eprintln!(
"reply: mode={:?} model={:?} provider={:?} cost_nanos={:?}",
meta.mode, meta.model, meta.provider, meta.cost_nanos
);
}
assert_eq!(
replies.iter().map(|m| m.mode).collect::<Vec<_>>(),
[
ServerMode::External,
ServerMode::OpenRouter,
ServerMode::External
]
);
assert_eq!(replies[1].model.as_deref(), Some(slug.as_str()));
assert!(replies[1].provider.is_some(), "the gateway says who served");
assert!(replies[1].cost_nanos.is_some_and(|c| c > 0));
assert_eq!(
(&replies[0].provider, replies[0].cost_nanos),
(&None, None),
"a local server says neither"
);
assert_eq!((&replies[2].provider, replies[2].cost_nanos), (&None, None));
let saved = reopened.json().load_config().unwrap();
assert_eq!(saved.engine.mode, ServerMode::External);
assert_eq!(saved.engine.external.url.as_deref(), Some(local.as_str()));
assert_eq!(
saved.engine.openrouter.model_name.as_deref(),
Some(slug.as_str())
);
}
#[tokio::test]
#[ignore = "requires MINDFORK_OPENROUTER_KEY; MINDFORK_OPENROUTER_CONTINUES_MODEL and MINDFORK_OPENROUTER_RESTARTS_MODEL name the two arms"]
async fn continue_in_the_mode_follows_the_route_table_live() {
if std::env::var("MINDFORK_OPENROUTER_KEY").map_or(true, |k| k.trim().is_empty()) {
eprintln!("skip: MINDFORK_OPENROUTER_KEY not set");
return;
}
let arm = |var: &str, measured: &str| {
std::env::var(var)
.ok()
.filter(|m| !m.trim().is_empty())
.unwrap_or_else(|| measured.to_string())
};
for (slug, continues) in [
(
arm(
"MINDFORK_OPENROUTER_CONTINUES_MODEL",
"anthropic/claude-haiku-4.5",
),
true,
),
(
arm(
"MINDFORK_OPENROUTER_RESTARTS_MODEL",
"google/gemma-4-31b-it",
),
false,
),
] {
let mut cfg = no_auto_cfg();
cfg.engine.mode = ServerMode::OpenRouter;
cfg.engine.openrouter = CloudSettings {
model_name: Some(slug.clone()),
api_key_env: Some("MINDFORK_OPENROUTER_KEY".into()),
..Default::default()
};
cfg.default_sampling.max_tokens = Some(4);
cfg.default_sampling.reasoning_budget = Some(0);
let (_dir, cmd_tx, mut rx, handle) = spawn_on_the_real_supervisor(cfg);
engine_facts(&mut rx, of_the_gateway).await;
super::live::cut_then_continue(&cmd_tx, &mut rx, &slug, continues).await;
cmd_tx.send(AppCommand::Quit).unwrap();
handle.await.unwrap();
}
}
#[tokio::test]
#[ignore = "requires network access to openrouter.ai; no key is needed — the key under test is a wrong one"]
async fn a_refused_key_is_the_chats_status_live() {
const VAR: &str = "MINDFORK_TEST_OPENROUTER_WRONG_KEY";
unsafe { std::env::set_var(VAR, format!("sk-or-v1-{}", "0".repeat(64))) };
let mut cfg = no_auto_cfg();
cfg.engine.mode = ServerMode::OpenRouter;
cfg.engine.openrouter = CloudSettings {
model_name: Some("anthropic/claude-haiku-4.5".into()),
api_key_env: Some(VAR.into()),
..Default::default()
};
let (_dir, cmd_tx, mut rx, handle) = spawn_on_the_real_supervisor(cfg);
let refused = tokio::time::timeout(
std::time::Duration::from_secs(60),
wait_for(&mut rx, |e| {
matches!(e, AppEvent::ServerStatus(s)
if matches!(s.chat, ServerStatus::Disconnected(_)))
}),
)
.await
.expect("the key check answers")
.expect("the event stream");
let AppEvent::ServerStatus(statuses) = refused else {
unreachable!()
};
let ServerStatus::Disconnected(said) = statuses.chat else {
unreachable!()
};
eprintln!("status: {said}");
assert!(said.contains("OpenRouter"), "{said}");
cmd_tx.send(AppCommand::SendMessage("hi".into())).unwrap();
let note = wait_for(&mut rx, |e| matches!(e, AppEvent::Error(_)))
.await
.expect("the refusal");
let AppEvent::Error(text) = note else {
unreachable!()
};
eprintln!("a message sent anyway: {text}");
assert!(text.contains("OpenRouter"), "{text}");
cmd_tx.send(AppCommand::Quit).unwrap();
handle.await.unwrap();
}