use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc::UnboundedSender;
use tokio_util::sync::CancellationToken;
use crate::shared::api::{
AnthropicClient, Embedder, EngineBackend, GeminiClient, ManagedConfig, OpenAiClient,
ResponsesClient, ServerHandle, UnavailableEmbedder, retry, wait_until_ready,
};
use crate::shared::config::{
CloudProvider, EmbedSettings, EngineSettings, ImpersonationEngineSettings, ImpersonationMode,
ManagedSettings, ServerMode,
};
use crate::shared::i18n::Locale;
use crate::shared::paths::Paths;
use crate::shared::server::ServerStatus;
const MANAGED_READY_TIMEOUT: Duration = Duration::from_secs(600);
const EXTERNAL_READY_TIMEOUT: Duration = Duration::from_secs(15);
const HEALTHY_POLL: Duration = Duration::from_secs(60);
const RECHECK_POLL: Duration = Duration::from_secs(5);
const FAILURES_TO_UNHEALTHY: u32 = 3;
pub struct ChatSetup {
pub backend: Option<Arc<dyn EngineBackend>>,
pub handle: Option<ServerHandle>,
pub status: ServerStatus,
}
pub struct EmbedSetup {
pub embedder: Arc<dyn Embedder>,
pub handle: Option<ServerHandle>,
pub status: ServerStatus,
}
pub trait ServerSupervisor: Send + Sync {
fn apply_chat(
&self,
settings: &EngineSettings,
stored_key: Option<&str>,
cancel: CancellationToken,
status_tx: UnboundedSender<ServerStatus>,
loc: &'static Locale,
) -> ChatSetup;
fn apply_embed(
&self,
settings: &EmbedSettings,
stored_key: Option<&str>,
cancel: CancellationToken,
status_tx: UnboundedSender<ServerStatus>,
loc: &'static Locale,
) -> EmbedSetup;
fn apply_impersonation(
&self,
settings: &ImpersonationEngineSettings,
stored_key: Option<&str>,
cancel: CancellationToken,
status_tx: UnboundedSender<ServerStatus>,
loc: &'static Locale,
) -> ChatSetup;
}
#[derive(Debug, Clone, Default)]
pub struct BinaryLookup {
pub llama_dir: Option<PathBuf>,
pub exe_dir: Option<PathBuf>,
}
impl BinaryLookup {
pub fn from_paths(paths: &Paths) -> Self {
Self {
llama_dir: Some(paths.llama_dir()),
exe_dir: paths.exe_dir().map(Path::to_path_buf),
}
}
fn resolve(&self, configured: Option<&str>) -> Option<PathBuf> {
crate::features::llama_setup::resolve_binary(
configured,
self.exe_dir.as_deref(),
self.llama_dir.as_deref(),
)
}
}
#[derive(Default)]
pub struct LlamaSupervisor {
lookup: BinaryLookup,
}
impl LlamaSupervisor {
pub fn new(paths: &Paths) -> Self {
Self {
lookup: BinaryLookup::from_paths(paths),
}
}
}
impl ServerSupervisor for LlamaSupervisor {
fn apply_chat(
&self,
settings: &EngineSettings,
stored_key: Option<&str>,
cancel: CancellationToken,
status_tx: UnboundedSender<ServerStatus>,
loc: &'static Locale,
) -> ChatSetup {
match settings.mode {
ServerMode::External => external_chat_setup(
settings.external.url.as_deref(),
settings.external.model_name.as_deref(),
stored_key,
settings.external.api_key_env.as_deref(),
cancel,
status_tx,
loc,
),
ServerMode::Managed => {
let cfg = managed_config(&settings.managed, &self.lookup);
managed_chat_setup(cfg, cancel, status_tx, loc)
}
ServerMode::OpenAi | ServerMode::Gemini | ServerMode::Claude | ServerMode::Grok => {
let cloud = settings.cloud().expect("cloud mode");
cloud_chat_setup(
settings.mode.cloud_provider().expect("cloud mode"),
cloud.url.as_deref(),
stored_key,
cloud.api_key_env.as_deref(),
cloud.model_name.as_deref(),
loc,
)
}
}
}
fn apply_impersonation(
&self,
settings: &ImpersonationEngineSettings,
stored_key: Option<&str>,
cancel: CancellationToken,
status_tx: UnboundedSender<ServerStatus>,
loc: &'static Locale,
) -> ChatSetup {
match settings.mode {
ImpersonationMode::Shared => not_configured(),
ImpersonationMode::External => external_chat_setup(
settings.external.url.as_deref(),
settings.external.model_name.as_deref(),
stored_key,
settings.external.api_key_env.as_deref(),
cancel,
status_tx,
loc,
),
ImpersonationMode::Managed => {
let cfg = managed_config(&settings.managed, &self.lookup);
managed_chat_setup(cfg, cancel, status_tx, loc)
}
ImpersonationMode::OpenAi
| ImpersonationMode::Gemini
| ImpersonationMode::Claude
| ImpersonationMode::Grok => {
let cloud = settings.cloud().expect("cloud mode");
cloud_chat_setup(
settings.mode.cloud_provider().expect("cloud mode"),
cloud.url.as_deref(),
stored_key,
cloud.api_key_env.as_deref(),
cloud.model_name.as_deref(),
loc,
)
}
}
}
fn apply_embed(
&self,
settings: &EmbedSettings,
stored_key: Option<&str>,
cancel: CancellationToken,
status_tx: UnboundedSender<ServerStatus>,
loc: &'static Locale,
) -> EmbedSetup {
match settings.mode {
ServerMode::External => match settings.external.url.as_deref() {
Some(url) if !url.is_empty() => {
let client = Arc::new(
OpenAiClient::new(url)
.with_api_key(
resolve_api_key(
stored_key,
settings.external.api_key_env.as_deref(),
)
.ok(),
)
.with_model(settings.external.model_name.clone()),
);
spawn_probe(
client.clone(),
EXTERNAL_READY_TIMEOUT,
None,
cancel,
status_tx,
loc,
);
EmbedSetup {
embedder: client,
handle: None,
status: ServerStatus::Connecting,
}
}
_ => unavailable_embed(),
},
ServerMode::Managed => match self.lookup.resolve(settings.managed.binary.as_deref()) {
Some(bin) => {
let m = &settings.managed;
let cfg = ManagedConfig {
binary: bin,
model_path: m.model_path.clone(),
mmproj: None,
gpu_layers: m.gpu_layers,
context_size: crate::shared::config::DEFAULT_CONTEXT_SIZE,
batch_size: None,
parallel: 1,
jinja: false, reasoning_format: None,
embeddings: true,
no_mmap: false,
flash_attn: None,
spec_type: None,
draft_model: None,
draft_gpu_layers: None,
draft_n_max: None,
draft_n_min: None,
host: "127.0.0.1".into(),
port: m.port,
extra_args: vec![],
};
if !cfg.is_runnable() {
return unavailable_embed();
}
match ServerHandle::launch(&cfg, loc) {
Ok(handle) => {
let client = Arc::new(OpenAiClient::new(handle.base_url()));
spawn_probe(
client.clone(),
MANAGED_READY_TIMEOUT,
Some(handle.exited()),
cancel,
status_tx,
loc,
);
EmbedSetup {
embedder: client,
handle: Some(handle),
status: ServerStatus::Connecting,
}
}
Err(err) => {
tracing::warn!(error = %err, "failed to launch the embedding server; RAG unavailable");
EmbedSetup {
embedder: Arc::new(UnavailableEmbedder),
handle: None,
status: ServerStatus::Disconnected(err.to_string()),
}
}
}
}
_ => unavailable_embed(),
},
ServerMode::OpenAi | ServerMode::Gemini => {
let cloud = settings.cloud().expect("cloud mode");
cloud_embed_setup(
settings.mode.cloud_provider().expect("cloud mode"),
cloud.url.as_deref(),
stored_key,
cloud.api_key_env.as_deref(),
cloud.model_name.as_deref(),
)
}
ServerMode::Claude | ServerMode::Grok => {
tracing::warn!(
mode = ?settings.mode,
"this provider has no embeddings API; set a different embedder for RAG"
);
unavailable_embed()
}
}
}
}
fn external_chat_setup(
url: Option<&str>,
model_name: Option<&str>,
stored_key: Option<&str>,
api_key_env: Option<&str>,
cancel: CancellationToken,
status_tx: UnboundedSender<ServerStatus>,
loc: &'static Locale,
) -> ChatSetup {
match url {
Some(url) if !url.is_empty() => {
let key = resolve_api_key(stored_key, api_key_env).ok();
let client = Arc::new(
OpenAiClient::new(url)
.with_api_key(key)
.with_model(model_name.map(str::to_string)),
);
spawn_probe(
client.clone(),
EXTERNAL_READY_TIMEOUT,
None,
cancel,
status_tx,
loc,
);
ChatSetup {
backend: Some(retry::RetryBackend::wrap(client)),
handle: None,
status: ServerStatus::Connecting,
}
}
_ => not_configured(),
}
}
fn managed_chat_setup(
cfg: ManagedConfig,
cancel: CancellationToken,
status_tx: UnboundedSender<ServerStatus>,
loc: &'static Locale,
) -> ChatSetup {
if !cfg.is_runnable() {
return not_configured();
}
match ServerHandle::launch(&cfg, loc) {
Ok(handle) => {
let client = Arc::new(OpenAiClient::new(handle.base_url()));
spawn_probe(
client.clone(),
MANAGED_READY_TIMEOUT,
Some(handle.exited()),
cancel,
status_tx,
loc,
);
ChatSetup {
backend: Some(client),
handle: Some(handle),
status: ServerStatus::Connecting,
}
}
Err(err) => ChatSetup {
backend: None,
handle: None,
status: ServerStatus::Disconnected(err.to_string()),
},
}
}
fn managed_config(s: &ManagedSettings, lookup: &BinaryLookup) -> ManagedConfig {
ManagedConfig {
binary: lookup.resolve(s.binary.as_deref()).unwrap_or_default(),
model_path: s.model_path.clone(),
mmproj: s.mmproj.clone(),
gpu_layers: s.gpu_layers,
context_size: s.context_size,
batch_size: s.batch_size,
parallel: s.sessions,
jinja: s.jinja,
reasoning_format: s.reasoning_format.clone(),
embeddings: false,
no_mmap: s.no_mmap,
flash_attn: s.flash_attn.as_arg().map(str::to_string),
spec_type: s.spec_type.as_arg().map(str::to_string),
draft_model: s.draft_model.clone(),
draft_gpu_layers: s.draft_gpu_layers,
draft_n_max: s.draft_n_max,
draft_n_min: s.draft_n_min,
host: s.host.clone(),
port: s.port,
extra_args: vec![],
}
}
#[derive(Debug)]
pub(crate) enum ApiKeyError {
NoName,
Missing(String),
}
pub(crate) fn resolve_api_key(
stored: Option<&str>,
api_key_env: Option<&str>,
) -> Result<String, ApiKeyError> {
if let Some(key) = stored.filter(|k| !k.is_empty()) {
return Ok(key.to_string());
}
let var = api_key_env
.filter(|v| !v.is_empty())
.ok_or(ApiKeyError::NoName)?;
std::env::var(var).map_err(|_| ApiKeyError::Missing(var.to_string()))
}
fn cloud_chat_setup(
provider: CloudProvider,
url_override: Option<&str>,
stored_key: Option<&str>,
api_key_env: Option<&str>,
model_name: Option<&str>,
loc: &'static Locale,
) -> ChatSetup {
let disconnected = |msg: String| ChatSetup {
backend: None,
handle: None,
status: ServerStatus::Disconnected(msg),
};
let Some(model) = model_name.filter(|m| !m.is_empty()) else {
return disconnected(loc.t("ui.err.server.no_model").into());
};
let key = match resolve_api_key(stored_key, api_key_env) {
Ok(k) => k,
Err(ApiKeyError::NoName) => {
return disconnected(loc.t("ui.err.server.no_api_key").into());
}
Err(ApiKeyError::Missing(var)) => {
return disconnected(loc.tf("ui.err.server.env_missing", &[("var", &var)]));
}
};
let base = url_override
.filter(|u| !u.is_empty())
.unwrap_or_else(|| provider.chat_base_url());
let backend: Arc<dyn EngineBackend> = match provider {
CloudProvider::OpenAi => Arc::new(ResponsesClient::new(base, key, model.to_string())),
CloudProvider::Gemini => Arc::new(GeminiClient::new(base, key, model.to_string())),
CloudProvider::Claude => Arc::new(AnthropicClient::new(base, key, model.to_string())),
CloudProvider::Grok => Arc::new(
OpenAiClient::new(base)
.with_api_key(Some(key))
.with_model(Some(model.to_string()))
.with_effort_none_omitted(true),
),
};
ChatSetup {
backend: Some(retry::RetryBackend::wrap(backend)),
handle: None,
status: ServerStatus::Ready,
}
}
fn cloud_embed_setup(
provider: CloudProvider,
url_override: Option<&str>,
stored_key: Option<&str>,
api_key_env: Option<&str>,
model_name: Option<&str>,
) -> EmbedSetup {
let (Some(model), Ok(key)) = (
model_name.filter(|m| !m.is_empty()),
resolve_api_key(stored_key, api_key_env),
) else {
tracing::warn!("cloud embeddings not configured (model/key); RAG unavailable");
return unavailable_embed();
};
let base = url_override
.filter(|u| !u.is_empty())
.unwrap_or_else(|| provider.base_url());
let client = OpenAiClient::new(base)
.with_api_key(Some(key))
.with_model(Some(model.to_string()));
EmbedSetup {
status: ServerStatus::Ready,
embedder: Arc::new(client),
handle: None,
}
}
fn not_configured() -> ChatSetup {
ChatSetup {
backend: None,
handle: None,
status: ServerStatus::NotConfigured,
}
}
fn unavailable_embed() -> EmbedSetup {
EmbedSetup {
embedder: Arc::new(UnavailableEmbedder),
handle: None,
status: ServerStatus::NotConfigured,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Health {
healthy: bool,
failures: u32,
}
impl Health {
fn new(healthy: bool) -> Self {
Self {
healthy,
failures: 0,
}
}
fn poll_delay(&self) -> Duration {
if self.healthy && self.failures == 0 {
HEALTHY_POLL
} else {
RECHECK_POLL
}
}
fn record(&mut self, ok: bool) -> Option<bool> {
if ok {
self.failures = 0;
return (!std::mem::replace(&mut self.healthy, true)).then_some(true);
}
self.failures += 1;
(self.healthy && self.failures >= FAILURES_TO_UNHEALTHY).then(|| {
self.healthy = false;
false
})
}
}
fn spawn_probe(
client: Arc<OpenAiClient>,
timeout: Duration,
exited: Option<CancellationToken>,
cancel: CancellationToken,
status_tx: UnboundedSender<ServerStatus>,
loc: &'static Locale,
) {
tokio::spawn(async move {
let status = tokio::select! {
biased;
_ = cancel.cancelled() => return,
res = wait_until_ready(&client, timeout, exited.clone(), loc) => match res {
Ok(()) => ServerStatus::Ready,
Err(err) => ServerStatus::Disconnected(err.to_string()),
},
};
if cancel.is_cancelled() {
return;
}
let mut health = Health::new(matches!(status, ServerStatus::Ready));
if status_tx.send(status).is_err() {
return; }
loop {
tokio::select! {
biased;
_ = cancel.cancelled() => return,
_ = wait_for_exit(&exited) => {
let _ = status_tx.send(ServerStatus::Disconnected(
loc.t("ui.err.managed.early_exit").to_string(),
));
return;
}
_ = tokio::time::sleep(health.poll_delay()) => {}
}
if cancel.is_cancelled() {
return;
}
let outcome = client.probe().await;
let reason = outcome.as_ref().err().map(|e| e.to_string());
if let Some(healthy) = health.record(outcome.is_ok()) {
let next = if healthy {
ServerStatus::Ready
} else {
ServerStatus::Disconnected(reason.unwrap_or_default())
};
if status_tx.send(next).is_err() {
return;
}
}
}
});
}
async fn wait_for_exit(exited: &Option<CancellationToken>) {
match exited {
Some(token) => token.cancelled().await,
None => std::future::pending().await,
}
}
pub struct DemoSupervisor {
backend: Arc<dyn EngineBackend>,
}
impl DemoSupervisor {
pub fn new(backend: Arc<dyn EngineBackend>) -> Self {
Self { backend }
}
}
impl ServerSupervisor for DemoSupervisor {
fn apply_chat(
&self,
_settings: &EngineSettings,
_stored_key: Option<&str>,
_cancel: CancellationToken,
_status_tx: UnboundedSender<ServerStatus>,
_loc: &'static Locale,
) -> ChatSetup {
ChatSetup {
backend: Some(self.backend.clone()),
handle: None,
status: ServerStatus::Ready,
}
}
fn apply_embed(
&self,
_settings: &EmbedSettings,
_stored_key: Option<&str>,
_cancel: CancellationToken,
_status_tx: UnboundedSender<ServerStatus>,
_loc: &'static Locale,
) -> EmbedSetup {
EmbedSetup {
embedder: Arc::new(crate::shared::api::mock::MockEmbedder::new(16)),
handle: None,
status: ServerStatus::Ready,
}
}
fn apply_impersonation(
&self,
_settings: &ImpersonationEngineSettings,
_stored_key: Option<&str>,
_cancel: CancellationToken,
_status_tx: UnboundedSender<ServerStatus>,
_loc: &'static Locale,
) -> ChatSetup {
ChatSetup {
backend: Some(self.backend.clone()),
handle: None,
status: ServerStatus::Ready,
}
}
}
#[cfg(test)]
pub struct MockSupervisor {
backend: Option<Arc<dyn EngineBackend>>,
chat_calls: std::sync::atomic::AtomicUsize,
chat_keys: std::sync::Mutex<Vec<Option<String>>>,
embed_dim: usize,
embedder: Option<Arc<dyn Embedder>>,
embed_unavailable: bool,
}
#[cfg(test)]
impl MockSupervisor {
pub fn with_backend(backend: Option<Arc<dyn EngineBackend>>) -> Self {
Self {
backend,
chat_calls: std::sync::atomic::AtomicUsize::new(0),
chat_keys: std::sync::Mutex::new(Vec::new()),
embed_dim: 16,
embedder: None,
embed_unavailable: false,
}
}
pub fn with_backend_and_embedder(
backend: Option<Arc<dyn EngineBackend>>,
embedder: Option<Arc<dyn Embedder>>,
) -> Self {
Self {
backend,
chat_calls: std::sync::atomic::AtomicUsize::new(0),
chat_keys: std::sync::Mutex::new(Vec::new()),
embed_dim: 16,
embedder,
embed_unavailable: false,
}
}
pub fn with_backend_no_embedder(backend: Option<Arc<dyn EngineBackend>>) -> Self {
Self {
embed_unavailable: true,
..Self::with_backend_and_embedder(backend, None)
}
}
pub fn chat_call_count(&self) -> usize {
self.chat_calls.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn chat_keys(&self) -> Vec<Option<String>> {
self.chat_keys.lock().unwrap().clone()
}
}
#[cfg(test)]
impl ServerSupervisor for MockSupervisor {
fn apply_chat(
&self,
_settings: &EngineSettings,
stored_key: Option<&str>,
_cancel: CancellationToken,
_status_tx: UnboundedSender<ServerStatus>,
_loc: &'static Locale,
) -> ChatSetup {
self.chat_calls
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
self.chat_keys
.lock()
.unwrap()
.push(stored_key.map(str::to_string));
let backend = self.backend.clone();
let status = if backend.is_some() {
ServerStatus::Ready
} else {
ServerStatus::NotConfigured
};
ChatSetup {
backend,
handle: None,
status,
}
}
fn apply_impersonation(
&self,
_settings: &ImpersonationEngineSettings,
_stored_key: Option<&str>,
_cancel: CancellationToken,
_status_tx: UnboundedSender<ServerStatus>,
_loc: &'static Locale,
) -> ChatSetup {
let backend = self.backend.clone();
let status = if backend.is_some() {
ServerStatus::Ready
} else {
ServerStatus::NotConfigured
};
ChatSetup {
backend,
handle: None,
status,
}
}
fn apply_embed(
&self,
_settings: &EmbedSettings,
_stored_key: Option<&str>,
_cancel: CancellationToken,
_status_tx: UnboundedSender<ServerStatus>,
_loc: &'static Locale,
) -> EmbedSetup {
if self.embed_unavailable {
return unavailable_embed();
}
let embedder = self.embedder.clone().unwrap_or_else(|| {
Arc::new(crate::shared::api::mock::MockEmbedder::new(self.embed_dim))
});
EmbedSetup {
embedder,
handle: None,
status: ServerStatus::Ready,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::shared::api::EmbedRole;
use crate::shared::config::ManagedEmbedSettings;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::sync::mpsc::unbounded_channel;
#[tokio::test]
#[ignore = "requires a downloaded build (MINDFORK_LLAMA_DIR) and a model (MINDFORK_MODEL)"]
async fn empty_binary_launches_the_downloaded_build_live() {
let (Ok(llama_dir), Ok(model)) = (
std::env::var("MINDFORK_LLAMA_DIR"),
std::env::var("MINDFORK_MODEL"),
) else {
eprintln!("skip: MINDFORK_LLAMA_DIR / MINDFORK_MODEL not set");
return;
};
let settings = EngineSettings {
mode: ServerMode::Managed,
managed: ManagedSettings {
binary: None, model_path: Some(model),
gpu_layers: 0,
context_size: 2048,
port: 18126,
..Default::default()
},
..Default::default()
};
let supervisor = LlamaSupervisor {
lookup: BinaryLookup {
llama_dir: Some(llama_dir.clone().into()),
exe_dir: None,
},
};
eprintln!(
"resolved: {:?}",
supervisor
.lookup
.resolve(None)
.expect("a build to resolve to")
);
let (tx, mut rx) = unbounded_channel();
let setup = supervisor.apply_chat(&settings, None, CancellationToken::new(), tx, ru());
assert_eq!(
setup.status,
ServerStatus::Connecting,
"an empty field with a build on disk must not read as NotConfigured"
);
let _handle = setup.handle.expect("a child process");
let status = tokio::time::timeout(Duration::from_secs(600), rx.recv())
.await
.expect("the probe should report within the readiness timeout")
.expect("the probe channel should not close");
assert_eq!(status, ServerStatus::Ready, "the resolved build must run");
}
#[test]
fn an_empty_binary_resolves_to_a_downloaded_build() {
let data = tempfile::tempdir().unwrap();
let install = data.path().join("vulkan-b10883");
std::fs::create_dir_all(&install).unwrap();
let binary = install.join(crate::features::llama_setup::server_binary_name());
std::fs::write(&binary, b"x").unwrap();
let lookup = BinaryLookup {
llama_dir: Some(data.path().to_path_buf()),
exe_dir: None,
};
let cfg = managed_config(&ManagedSettings::default(), &lookup);
assert_eq!(cfg.binary, binary);
let bare = managed_config(&ManagedSettings::default(), &BinaryLookup::default());
assert!(
bare.binary.as_os_str().is_empty(),
"no lookup, no path — `managed_chat_setup` reads that as NotConfigured"
);
}
#[test]
fn the_embedder_resolves_its_binary_the_same_way() {
let data = tempfile::tempdir().unwrap();
let install = data.path().join("cpu-b10883");
std::fs::create_dir_all(&install).unwrap();
std::fs::write(
install.join(crate::features::llama_setup::server_binary_name()),
b"x",
)
.unwrap();
let model = tempfile::NamedTempFile::new().unwrap();
let settings = EmbedSettings {
mode: ServerMode::Managed,
managed: ManagedEmbedSettings {
model_path: Some(model.path().display().to_string()),
..Default::default()
},
..Default::default()
};
let (tx, _rx) = unbounded_channel();
let without = LlamaSupervisor::default().apply_embed(
&settings,
None,
CancellationToken::new(),
tx,
ru(),
);
assert_eq!(without.status, ServerStatus::NotConfigured);
let (tx, _rx) = unbounded_channel();
let with = LlamaSupervisor {
lookup: BinaryLookup {
llama_dir: Some(data.path().to_path_buf()),
exe_dir: None,
},
}
.apply_embed(&settings, None, CancellationToken::new(), tx, ru());
assert_ne!(
with.status,
ServerStatus::NotConfigured,
"a build was found, so this is a launch attempt, not a missing setting"
);
}
#[tokio::test]
async fn managed_without_a_model_is_not_configured() {
for model in [None, Some(String::new()), Some(" ".to_string())] {
let (tx, _rx) = unbounded_channel();
let s = EngineSettings {
mode: ServerMode::Managed,
managed: ManagedSettings {
binary: Some("llama-server".into()),
model_path: model.clone(),
..Default::default()
},
..Default::default()
};
let setup =
LlamaSupervisor::default().apply_chat(&s, None, CancellationToken::new(), tx, ru());
assert!(setup.backend.is_none(), "{model:?}");
assert!(
setup.handle.is_none(),
"no process is started for {model:?}"
);
assert_eq!(setup.status, ServerStatus::NotConfigured, "{model:?}");
}
}
#[tokio::test]
async fn the_embedder_without_a_model_is_unavailable() {
let data = tempfile::tempdir().unwrap();
let install = data.path().join("cpu-b10883");
std::fs::create_dir_all(&install).unwrap();
std::fs::write(
install.join(crate::features::llama_setup::server_binary_name()),
b"x",
)
.unwrap();
let settings = EmbedSettings {
mode: ServerMode::Managed,
..Default::default()
};
let (tx, _rx) = unbounded_channel();
let setup = LlamaSupervisor {
lookup: BinaryLookup {
llama_dir: Some(data.path().to_path_buf()),
exe_dir: None,
},
}
.apply_embed(&settings, None, CancellationToken::new(), tx, ru());
assert!(setup.handle.is_none(), "no process is started");
assert_eq!(setup.status, ServerStatus::NotConfigured);
}
fn ru() -> &'static Locale {
crate::shared::i18n::locale(crate::shared::i18n::Lang::Ru)
}
async fn spawn_stub_server(healthy: bool) -> (String, Arc<AtomicBool>) {
let switch = Arc::new(AtomicBool::new(healthy));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let flag = switch.clone();
tokio::spawn(async move {
while let Ok((mut sock, _)) = listener.accept().await {
let flag = flag.clone();
tokio::spawn(async move {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let mut buf = [0u8; 1024];
let _ = sock.read(&mut buf).await;
if flag.load(Ordering::SeqCst) {
let _ = sock
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")
.await;
let _ = sock.shutdown().await;
}
});
}
});
(format!("http://{addr}/v1"), switch)
}
fn embed_setup(s: &EmbedSettings) -> EmbedSetup {
let (tx, _rx) = unbounded_channel();
LlamaSupervisor::default().apply_embed(s, None, CancellationToken::new(), tx, ru())
}
fn embed_external(url: &str) -> EmbedSettings {
EmbedSettings {
mode: ServerMode::External,
external: crate::shared::config::ExternalSettings {
url: Some(url.into()),
..Default::default()
},
..Default::default()
}
}
fn external(url: Option<&str>) -> EngineSettings {
EngineSettings {
mode: ServerMode::External,
external: crate::shared::config::ExternalSettings {
url: url.map(String::from),
..Default::default()
},
..Default::default()
}
}
#[tokio::test]
async fn external_with_url_yields_backend_connecting() {
let (tx, _rx) = unbounded_channel();
let setup = LlamaSupervisor::default().apply_chat(
&external(Some("http://127.0.0.1:9/v1")),
None,
CancellationToken::new(),
tx,
ru(),
);
assert!(setup.backend.is_some());
assert!(setup.handle.is_none());
assert_eq!(setup.status, ServerStatus::Connecting);
}
fn auth_probe_stub() -> (String, std::thread::JoinHandle<String>) {
use std::io::{Read, Write};
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let seen = std::thread::spawn(move || {
let (mut sock, _) = listener.accept().unwrap();
let mut req = Vec::new();
let mut buf = [0u8; 512];
while !req.windows(4).any(|w| w == b"\r\n\r\n") {
match sock.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => req.extend_from_slice(&buf[..n]),
}
}
sock.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
.unwrap();
String::from_utf8_lossy(&req)
.to_ascii_lowercase()
.lines()
.find(|l| l.starts_with("authorization:"))
.unwrap_or_default()
.trim()
.to_string()
});
(format!("http://{addr}/v1"), seen)
}
async fn header(seen: std::thread::JoinHandle<String>) -> String {
tokio::task::spawn_blocking(move || seen.join().unwrap())
.await
.unwrap()
}
async fn external_chat_header(stored: Option<&str>, api_key_env: Option<&str>) -> String {
let (url, seen) = auth_probe_stub();
let mut settings = external(Some(&url));
settings.external.api_key_env = api_key_env.map(String::from);
let (tx, _rx) = unbounded_channel();
let setup = LlamaSupervisor::default().apply_chat(
&settings,
stored,
CancellationToken::new(),
tx,
ru(),
);
assert!(setup.backend.is_some(), "external mode yields a backend");
header(seen).await
}
#[tokio::test]
async fn external_sends_the_stored_key_and_falls_back_to_env() {
let from_env = std::env::var("PATH").unwrap().to_ascii_lowercase();
assert_eq!(
external_chat_header(Some("sk-stored"), Some("PATH")).await,
"authorization: bearer sk-stored",
"a stored key wins over the named variable"
);
assert_eq!(
external_chat_header(None, Some("PATH")).await,
format!("authorization: bearer {from_env}"),
"with nothing stored, the named variable is read"
);
assert_eq!(
external_chat_header(None, None).await,
"",
"no key anywhere — no header, as before this feature existed"
);
}
fn read_request(sock: &mut std::net::TcpStream) -> Option<(String, String)> {
use std::io::Read;
let mut req = Vec::new();
let mut buf = [0u8; 1024];
let head_end = loop {
match sock.read(&mut buf) {
Ok(0) | Err(_) => return None,
Ok(n) => req.extend_from_slice(&buf[..n]),
}
if let Some(i) = req.windows(4).position(|w| w == b"\r\n\r\n") {
break i + 4;
}
};
let head = String::from_utf8_lossy(&req[..head_end]).to_string();
let len = content_length(&head);
while req.len() < head_end + len {
match sock.read(&mut buf) {
Ok(0) | Err(_) => break,
Ok(n) => req.extend_from_slice(&buf[..n]),
}
}
let body = String::from_utf8_lossy(&req[head_end..]).to_string();
Some((head, body))
}
fn content_length(head: &str) -> usize {
head.lines()
.find_map(|l| {
l.to_ascii_lowercase()
.strip_prefix("content-length:")
.and_then(|v| v.trim().parse().ok())
})
.unwrap_or(0)
}
fn chat_body_stub() -> (String, std::thread::JoinHandle<String>) {
use std::io::Write;
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let seen = std::thread::spawn(move || {
loop {
let Ok((mut sock, _)) = listener.accept() else {
return String::new();
};
let Some((head, body)) = read_request(&mut sock) else {
continue;
};
let _ = sock.write_all(
b"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
);
if head.starts_with("POST") {
return body;
}
}
});
(format!("http://{addr}/v1"), seen)
}
async fn external_chat_body(model_name: Option<&str>) -> String {
let (url, seen) = chat_body_stub();
let mut settings = external(Some(&url));
settings.external.model_name = model_name.map(String::from);
let (tx, _rx) = unbounded_channel();
let setup = LlamaSupervisor::default().apply_chat(
&settings,
None,
CancellationToken::new(),
tx,
ru(),
);
let backend = setup.backend.expect("external mode yields a backend");
let _ = backend
.chat_stream(
crate::shared::api::ChatRequest {
messages: vec![crate::shared::api::ApiMessage::user("hi")],
..Default::default()
},
CancellationToken::new(),
)
.await;
tokio::task::spawn_blocking(move || seen.join().unwrap())
.await
.unwrap()
}
#[tokio::test]
async fn external_chat_sends_the_configured_model() {
let body = external_chat_body(Some("qwen-3.6-27b")).await;
assert!(
body.contains(r#""model":"qwen-3.6-27b""#),
"the configured name must reach the request: {body}"
);
}
#[tokio::test]
async fn a_blank_model_field_sends_no_model_key() {
for blank in [None, Some("")] {
let body = external_chat_body(blank).await;
assert!(
!body.contains(r#""model""#),
"blank={blank:?} must send no model key: {body}"
);
}
}
#[tokio::test]
async fn external_embeddings_send_the_stored_key() {
let (url, seen) = auth_probe_stub();
let (tx, _rx) = unbounded_channel();
let setup = LlamaSupervisor::default().apply_embed(
&embed_external(&url),
Some("sk-embed"),
CancellationToken::new(),
tx,
ru(),
);
assert_eq!(setup.status, ServerStatus::Connecting);
assert_eq!(
header(seen).await,
"authorization: bearer sk-embed",
"the embedding probe must carry the slot's stored key"
);
}
#[tokio::test]
async fn superseded_probe_sends_no_status() {
let (tx, mut rx) = unbounded_channel();
let cancel = CancellationToken::new();
cancel.cancel(); let setup = external_chat_setup(
Some("http://127.0.0.1:9/v1"),
None,
None,
None,
cancel,
tx,
ru(),
);
assert_eq!(setup.status, ServerStatus::Connecting); tokio::time::sleep(Duration::from_millis(50)).await;
assert!(rx.try_recv().is_err(), "a stale probe sent a status");
}
#[tokio::test]
async fn external_without_url_is_not_configured() {
let (tx, _rx) = unbounded_channel();
let setup = LlamaSupervisor::default().apply_chat(
&external(None),
None,
CancellationToken::new(),
tx,
ru(),
);
assert!(setup.backend.is_none());
assert_eq!(setup.status, ServerStatus::NotConfigured);
}
#[tokio::test]
async fn managed_without_binary_is_not_configured() {
let (tx, _rx) = unbounded_channel();
let s = EngineSettings {
mode: ServerMode::Managed,
..Default::default()
};
let setup =
LlamaSupervisor::default().apply_chat(&s, None, CancellationToken::new(), tx, ru());
assert_eq!(setup.status, ServerStatus::NotConfigured);
}
#[tokio::test]
async fn managed_with_bogus_binary_is_disconnected() {
let model = tempfile::NamedTempFile::new().unwrap();
let (tx, _rx) = unbounded_channel();
let s = EngineSettings {
mode: ServerMode::Managed,
managed: ManagedSettings {
binary: Some("definitely-not-a-real-binary-xyz".into()),
model_path: Some(model.path().display().to_string()),
..Default::default()
},
..Default::default()
};
let setup =
LlamaSupervisor::default().apply_chat(&s, None, CancellationToken::new(), tx, ru());
assert!(setup.backend.is_none());
assert!(matches!(setup.status, ServerStatus::Disconnected(_)));
}
#[tokio::test]
async fn managed_with_missing_model_is_disconnected() {
let (tx, _rx) = unbounded_channel();
let s = EngineSettings {
mode: ServerMode::Managed,
managed: ManagedSettings {
binary: Some("llama-server".into()),
model_path: Some("no/such/model.gguf".into()),
..Default::default()
},
..Default::default()
};
let setup =
LlamaSupervisor::default().apply_chat(&s, None, CancellationToken::new(), tx, ru());
assert!(setup.backend.is_none());
match setup.status {
ServerStatus::Disconnected(msg) => assert!(msg.contains("файл модели"), "{msg}"),
other => panic!("expected Disconnected, got {other:?}"),
}
}
#[tokio::test]
async fn managed_carries_the_projector_and_reports_a_missing_one() {
const MISSING: &str = "no/such/mmproj.gguf";
let model = tempfile::NamedTempFile::new().unwrap();
let managed = ManagedSettings {
binary: Some("llama-server".into()),
model_path: Some(model.path().display().to_string()),
mmproj: Some(MISSING.into()),
..Default::default()
};
assert_eq!(
managed_config(&managed, &BinaryLookup::default())
.mmproj
.as_deref(),
Some(MISSING),
"the setting must reach the launch config"
);
let (tx, _rx) = unbounded_channel();
let s = EngineSettings {
mode: ServerMode::Managed,
managed,
..Default::default()
};
let setup =
LlamaSupervisor::default().apply_chat(&s, None, CancellationToken::new(), tx, ru());
assert!(setup.backend.is_none());
match setup.status {
ServerStatus::Disconnected(msg) => {
let expected = ru().tf("ui.err.managed.mmproj_not_found", &[("path", MISSING)]);
assert!(msg.contains(&expected), "{msg}");
}
other => panic!("expected Disconnected, got {other:?}"),
}
}
#[test]
fn resolve_api_key_reads_env_and_reports_missing() {
assert!(resolve_api_key(None, Some("PATH")).is_ok());
assert!(matches!(
resolve_api_key(None, None),
Err(ApiKeyError::NoName)
));
match resolve_api_key(None, Some("MINDFORK_DEFINITELY_UNSET_VAR_42")) {
Err(ApiKeyError::Missing(var)) => assert_eq!(var, "MINDFORK_DEFINITELY_UNSET_VAR_42"),
_ => panic!("expected ApiKeyError::Missing"),
}
}
#[test]
fn stored_key_wins_over_env_and_works_without_it() {
assert_eq!(
resolve_api_key(Some("sk-stored"), Some("PATH")).unwrap(),
"sk-stored"
);
assert_eq!(
resolve_api_key(Some("sk-stored"), None).unwrap(),
"sk-stored"
);
assert_eq!(
resolve_api_key(Some("sk-stored"), Some("MINDFORK_DEFINITELY_UNSET_VAR_42")).unwrap(),
"sk-stored"
);
assert!(resolve_api_key(Some(""), Some("PATH")).is_ok());
assert!(matches!(
resolve_api_key(Some(""), None),
Err(ApiKeyError::NoName)
));
}
#[tokio::test]
#[ignore = "requires an authenticated external server (MINDFORK_ENGINE_URL started with --api-key MINDFORK_ENGINE_KEY)"]
async fn external_authenticated_server_takes_the_stored_key_live() {
use crate::shared::api::ChatChunk;
use futures_util::StreamExt;
let (Ok(url), Ok(key)) = (
std::env::var("MINDFORK_ENGINE_URL"),
std::env::var("MINDFORK_ENGINE_KEY"),
) else {
eprintln!("skip: MINDFORK_ENGINE_URL/MINDFORK_ENGINE_KEY not set");
return;
};
let settings = external(Some(&url));
assert_eq!(settings.external.api_key_env, None);
let turn = |stored: Option<&str>| {
let (tx, _rx) = unbounded_channel();
let setup = LlamaSupervisor::default().apply_chat(
&settings,
stored,
CancellationToken::new(),
tx,
ru(),
);
let backend = setup.backend.expect("external mode yields a backend");
async move {
let req = crate::shared::api::ChatRequest {
continue_final: false,
system: None,
messages: vec![crate::shared::api::ApiMessage::user("Say OK.".to_string())],
sampling: crate::entities::sampling::SamplingConfig {
max_tokens: Some(512),
..Default::default()
},
tools: Vec::new(),
};
let mut stream = backend.chat_stream(req, CancellationToken::new()).await?;
let mut text = String::new();
while let Some(chunk) = stream.next().await {
match chunk {
ChatChunk::Text(t) => text.push_str(&t),
ChatChunk::Error { message, .. } => anyhow::bail!("stream: {message}"),
_ => {}
}
}
Ok::<String, anyhow::Error>(text)
}
};
let with_key = turn(Some(&key)).await;
eprintln!("live: stored key -> {with_key:?}");
let answer = with_key.expect("the stored key must authenticate the turn");
assert!(
!answer.trim().is_empty(),
"authenticated turn produced no text"
);
let without = turn(None).await;
eprintln!("live: no key -> {without:?}");
assert!(
without.is_err(),
"the server accepted an unauthenticated turn — it is not enforcing \
--api-key, so this smoke measured nothing"
);
}
#[tokio::test]
async fn cloud_chat_with_stored_key_and_no_env_is_ready() {
let (tx, _rx) = unbounded_channel();
let s = EngineSettings {
mode: ServerMode::OpenAi,
openai: crate::shared::config::CloudSettings {
model_name: Some("gpt-5.5".into()),
api_key_env: None, ..Default::default()
},
..Default::default()
};
let setup = LlamaSupervisor::default().apply_chat(
&s,
Some("sk-stored"),
CancellationToken::new(),
tx,
ru(),
);
assert_eq!(setup.status, ServerStatus::Ready);
assert!(setup.backend.is_some());
}
#[tokio::test]
async fn cloud_chat_without_model_is_disconnected() {
let (tx, _rx) = unbounded_channel();
let s = EngineSettings {
mode: ServerMode::OpenAi,
openai: crate::shared::config::CloudSettings {
api_key_env: Some("PATH".into()),
..Default::default()
},
..Default::default()
};
match LlamaSupervisor::default()
.apply_chat(&s, None, CancellationToken::new(), tx, ru())
.status
{
ServerStatus::Disconnected(m) => assert!(m.contains("модел"), "{m}"),
other => panic!("expected Disconnected, got {other:?}"),
}
}
#[tokio::test]
async fn cloud_chat_missing_key_env_is_disconnected() {
let (tx, _rx) = unbounded_channel();
let s = EngineSettings {
mode: ServerMode::OpenAi,
openai: crate::shared::config::CloudSettings {
model_name: Some("gpt-4o".into()),
api_key_env: Some("MINDFORK_DEFINITELY_UNSET_VAR_42".into()),
..Default::default()
},
..Default::default()
};
match LlamaSupervisor::default()
.apply_chat(&s, None, CancellationToken::new(), tx, ru())
.status
{
ServerStatus::Disconnected(m) => {
assert!(m.contains("MINDFORK_DEFINITELY_UNSET_VAR_42"), "{m}")
}
other => panic!("expected Disconnected, got {other:?}"),
}
}
#[tokio::test]
async fn cloud_chat_with_model_and_key_is_ready() {
let (tx, _rx) = unbounded_channel();
let s = EngineSettings {
mode: ServerMode::Gemini,
gemini: crate::shared::config::CloudSettings {
model_name: Some("gemini-2.5-pro".into()),
api_key_env: Some("PATH".into()),
..Default::default()
},
..Default::default()
};
let setup =
LlamaSupervisor::default().apply_chat(&s, None, CancellationToken::new(), tx, ru());
assert!(setup.backend.is_some());
assert!(setup.handle.is_none(), "the cloud has no child process");
assert_eq!(setup.status, ServerStatus::Ready);
}
#[tokio::test]
async fn cloud_chat_claude_with_model_and_key_is_ready() {
let (tx, _rx) = unbounded_channel();
let s = EngineSettings {
mode: ServerMode::Claude,
claude: crate::shared::config::CloudSettings {
model_name: Some("claude-opus-4-8".into()),
api_key_env: Some("PATH".into()),
..Default::default()
},
..Default::default()
};
let setup =
LlamaSupervisor::default().apply_chat(&s, None, CancellationToken::new(), tx, ru());
assert!(setup.backend.is_some());
assert!(setup.handle.is_none());
assert_eq!(setup.status, ServerStatus::Ready);
}
#[tokio::test]
async fn cloud_chat_grok_with_model_and_key_is_ready() {
let (tx, _rx) = unbounded_channel();
let s = EngineSettings {
mode: ServerMode::Grok,
grok: crate::shared::config::CloudSettings {
model_name: Some("grok-4.5".into()),
api_key_env: Some("PATH".into()),
..Default::default()
},
..Default::default()
};
let setup =
LlamaSupervisor::default().apply_chat(&s, None, CancellationToken::new(), tx, ru());
assert!(setup.backend.is_some());
assert!(setup.handle.is_none());
assert_eq!(setup.status, ServerStatus::Ready);
}
#[tokio::test]
async fn grok_embed_is_unavailable() {
let s = EmbedSettings {
mode: ServerMode::Grok,
grok: crate::shared::config::CloudSettings {
model_name: Some("x".into()),
api_key_env: Some("PATH".into()),
..Default::default()
},
..Default::default()
};
let err = embed_setup(&s)
.embedder
.embed(vec!["x".into()], EmbedRole::Passage)
.await
.unwrap_err();
assert!(err.to_string().contains("not configured"));
}
#[tokio::test]
async fn claude_embed_is_unavailable() {
let s = EmbedSettings {
mode: ServerMode::Claude,
claude: crate::shared::config::CloudSettings {
model_name: Some("x".into()),
api_key_env: Some("PATH".into()),
..Default::default()
},
..Default::default()
};
let err = embed_setup(&s)
.embedder
.embed(vec!["x".into()], EmbedRole::Passage)
.await
.unwrap_err();
assert!(err.to_string().contains("not configured"));
}
#[tokio::test]
async fn cloud_embed_unconfigured_is_unavailable() {
let s = EmbedSettings {
mode: ServerMode::OpenAi,
openai: crate::shared::config::CloudSettings {
api_key_env: Some("PATH".into()),
..Default::default()
},
..Default::default()
};
let setup = embed_setup(&s);
let err = setup
.embedder
.embed(vec!["x".into()], EmbedRole::Passage)
.await
.unwrap_err();
assert!(err.to_string().contains("not configured"));
}
#[test]
fn health_flips_down_only_after_a_streak_and_up_at_once() {
let mut h = Health::new(true);
for _ in 0..FAILURES_TO_UNHEALTHY - 1 {
assert_eq!(h.record(false), None, "flipped before the streak completed");
}
assert_eq!(
h.record(false),
Some(false),
"the streak should flip it down"
);
assert_eq!(
h.record(true),
Some(true),
"one success should bring it back"
);
}
#[test]
fn health_success_resets_the_streak() {
let mut h = Health::new(true);
h.record(false);
assert_eq!(h.record(true), None, "still healthy — nothing to publish");
for _ in 0..FAILURES_TO_UNHEALTHY - 1 {
assert_eq!(h.record(false), None);
}
assert_eq!(h.record(false), Some(false));
}
#[test]
fn health_publishes_only_on_a_flip() {
let mut h = Health::new(true);
assert_eq!(h.record(true), None);
let mut down = Health::new(false);
assert_eq!(down.record(false), None);
}
#[test]
fn health_polls_fast_while_anything_looks_wrong() {
assert_eq!(Health::new(true).poll_delay(), HEALTHY_POLL);
assert_eq!(Health::new(false).poll_delay(), RECHECK_POLL);
let mut pending = Health::new(true);
pending.record(false);
assert_eq!(
pending.poll_delay(),
RECHECK_POLL,
"a pending failure streak must not wait a full healthy interval"
);
}
#[tokio::test(start_paused = true)]
async fn monitor_notices_a_server_going_down_and_coming_back() {
let (url, switch) = spawn_stub_server(true).await;
let (tx, mut rx) = unbounded_channel();
LlamaSupervisor::default().apply_embed(
&embed_external(&url),
None,
CancellationToken::new(),
tx,
ru(),
);
assert_eq!(
rx.recv().await,
Some(ServerStatus::Ready),
"initial verdict"
);
switch.store(false, Ordering::SeqCst); match rx.recv().await {
Some(ServerStatus::Disconnected(_)) => {}
other => panic!("expected the monitor to notice the outage, got {other:?}"),
}
switch.store(true, Ordering::SeqCst); assert_eq!(
rx.recv().await,
Some(ServerStatus::Ready),
"the monitor should recover without a restart"
);
}
#[tokio::test(start_paused = true)]
async fn monitor_stays_quiet_while_the_server_is_steady() {
let (url, _switch) = spawn_stub_server(true).await;
let (tx, mut rx) = unbounded_channel();
LlamaSupervisor::default().apply_embed(
&embed_external(&url),
None,
CancellationToken::new(),
tx,
ru(),
);
assert_eq!(rx.recv().await, Some(ServerStatus::Ready));
tokio::time::sleep(HEALTHY_POLL * 10).await;
assert!(
rx.try_recv().is_err(),
"a steady server should publish nothing after its first verdict"
);
}
#[tokio::test]
async fn embed_external_is_connecting_not_ready() {
let setup = embed_setup(&embed_external("http://127.0.0.1:9/v1"));
assert_eq!(setup.status, ServerStatus::Connecting);
}
#[tokio::test]
async fn embed_probe_reports_ready_when_server_answers() {
let (url, _switch) = spawn_stub_server(true).await;
let (tx, mut rx) = unbounded_channel();
let setup = LlamaSupervisor::default().apply_embed(
&embed_external(&url),
None,
CancellationToken::new(),
tx,
ru(),
);
assert_eq!(setup.status, ServerStatus::Connecting);
assert_eq!(rx.recv().await, Some(ServerStatus::Ready));
}
#[tokio::test(start_paused = true)]
async fn embed_probe_reports_disconnected_when_server_is_silent() {
let (url, _switch) = spawn_stub_server(false).await;
let (tx, mut rx) = unbounded_channel();
let setup = LlamaSupervisor::default().apply_embed(
&embed_external(&url),
None,
CancellationToken::new(),
tx,
ru(),
);
assert_eq!(setup.status, ServerStatus::Connecting);
match rx.recv().await {
Some(ServerStatus::Disconnected(_)) => {}
other => panic!("expected Disconnected from the probe, got {other:?}"),
}
}
#[tokio::test(start_paused = true)]
async fn embed_superseded_probe_sends_no_status() {
let (tx, mut rx) = unbounded_channel();
let cancel = CancellationToken::new();
cancel.cancel(); let setup = LlamaSupervisor::default().apply_embed(
&embed_external("http://127.0.0.1:9/v1"),
None,
cancel,
tx,
ru(),
);
assert_eq!(setup.status, ServerStatus::Connecting);
tokio::time::sleep(Duration::from_secs(60)).await; assert!(rx.try_recv().is_err(), "a stale probe sent a status");
}
#[tokio::test]
async fn embed_managed_with_missing_model_is_disconnected() {
let s = EmbedSettings {
mode: ServerMode::Managed,
managed: crate::shared::config::ManagedEmbedSettings {
binary: Some("llama-server".into()),
model_path: Some("no/such/model.gguf".into()),
..Default::default()
},
..Default::default()
};
match embed_setup(&s).status {
ServerStatus::Disconnected(msg) => assert!(msg.contains("файл модели"), "{msg}"),
other => panic!("expected Disconnected, got {other:?}"),
}
}
#[tokio::test(start_paused = true)]
async fn cloud_embed_configured_is_ready_without_probe() {
let (tx, mut rx) = unbounded_channel();
let s = EmbedSettings {
mode: ServerMode::OpenAi,
openai: crate::shared::config::CloudSettings {
model_name: Some("text-embedding-3-small".into()),
api_key_env: Some("PATH".into()),
..Default::default()
},
..Default::default()
};
let setup =
LlamaSupervisor::default().apply_embed(&s, None, CancellationToken::new(), tx, ru());
assert_eq!(setup.status, ServerStatus::Ready);
assert!(setup.handle.is_none(), "the cloud has no child process");
tokio::time::sleep(Duration::from_secs(60)).await;
assert!(rx.try_recv().is_err(), "the cloud shouldn't be probed");
}
#[cfg(windows)]
fn pid_on_port(port: u16) -> Option<u32> {
let out = std::process::Command::new("netstat")
.args(["-ano", "-p", "TCP"])
.output()
.ok()?;
let text = String::from_utf8_lossy(&out.stdout).into_owned();
text.lines()
.filter(|l| l.contains("LISTENING") && l.contains(&format!(":{port} ")))
.find_map(|l| l.split_whitespace().last()?.parse().ok())
}
#[tokio::test]
#[cfg(windows)]
#[ignore = "requires a local llama-server binary + model (MINDFORK_LLAMA_BIN, MINDFORK_MODEL)"]
async fn managed_without_a_model_starts_nothing_e2e_live() {
let (Ok(bin), Ok(model)) = (
std::env::var("MINDFORK_LLAMA_BIN"),
std::env::var("MINDFORK_MODEL"),
) else {
eprintln!("skip: MINDFORK_LLAMA_BIN / MINDFORK_MODEL not set");
return;
};
const PORT: u16 = 18097;
let settings = |model: Option<String>| EngineSettings {
mode: ServerMode::Managed,
managed: ManagedSettings {
binary: Some(bin.clone()),
model_path: model,
port: PORT,
gpu_layers: 0,
context_size: 4096,
..Default::default()
},
..Default::default()
};
let (tx, _rx) = unbounded_channel();
let refused = LlamaSupervisor::default().apply_chat(
&settings(None),
None,
CancellationToken::new(),
tx,
ru(),
);
assert_eq!(refused.status, ServerStatus::NotConfigured);
assert!(refused.handle.is_none(), "no child is owned");
tokio::time::sleep(Duration::from_secs(2)).await;
assert_eq!(
pid_on_port(PORT),
None,
"nothing must be listening on {PORT}"
);
let (tx, mut rx) = unbounded_channel();
let started = LlamaSupervisor::default().apply_chat(
&settings(Some(model)),
None,
CancellationToken::new(),
tx,
ru(),
);
let _handle = started.handle.expect("a managed server owns its child");
let status = rx.recv().await;
println!("with a model: {status:?}");
assert_eq!(status, Some(ServerStatus::Ready), "the model loaded");
assert!(pid_on_port(PORT).is_some(), "the server is listening");
}
#[tokio::test]
#[cfg(windows)]
#[ignore = "requires a local llama-server binary + model (MINDFORK_LLAMA_BIN, MINDFORK_MODEL)"]
async fn managed_without_mmap_starts_e2e_live() {
let (Ok(bin), Ok(model)) = (
std::env::var("MINDFORK_LLAMA_BIN"),
std::env::var("MINDFORK_MODEL"),
) else {
eprintln!("skip: MINDFORK_LLAMA_BIN / MINDFORK_MODEL not set");
return;
};
const PORT: u16 = 18098;
let spelling = crate::shared::api::managed::no_mmap_spelling(std::path::Path::new(&bin));
println!("{bin} spells no-mmap as {spelling:?}");
let settings = EngineSettings {
mode: ServerMode::Managed,
managed: ManagedSettings {
binary: Some(bin),
model_path: Some(model),
port: PORT,
gpu_layers: 0,
context_size: 4096,
no_mmap: true,
..Default::default()
},
..Default::default()
};
let (tx, mut rx) = unbounded_channel();
let started = LlamaSupervisor::default().apply_chat(
&settings,
None,
CancellationToken::new(),
tx,
ru(),
);
let _handle = started
.handle
.expect("a managed server owns its child even with the box ticked");
let status = rx.recv().await;
println!("with no mmap: {status:?}");
assert_eq!(
status,
Some(ServerStatus::Ready),
"the server must come up with the setting on"
);
assert!(pid_on_port(PORT).is_some(), "and be listening");
}
#[tokio::test]
#[cfg(windows)]
#[ignore = "requires a local llama-server binary + model (MINDFORK_LLAMA_BIN, MINDFORK_EMBED_MODEL)"]
async fn managed_child_death_is_noticed_at_once() {
let (Ok(bin), Ok(model)) = (
std::env::var("MINDFORK_LLAMA_BIN"),
std::env::var("MINDFORK_EMBED_MODEL"),
) else {
eprintln!("skip: MINDFORK_LLAMA_BIN / MINDFORK_EMBED_MODEL not set");
return;
};
const PORT: u16 = 18099;
let s = EmbedSettings {
mode: ServerMode::Managed,
managed: crate::shared::config::ManagedEmbedSettings {
binary: Some(bin),
model_path: Some(model),
port: PORT,
..Default::default()
},
..Default::default()
};
let (tx, mut rx) = unbounded_channel();
let setup =
LlamaSupervisor::default().apply_embed(&s, None, CancellationToken::new(), tx, ru());
let _handle = setup.handle.expect("a managed server owns its child");
assert_eq!(rx.recv().await, Some(ServerStatus::Ready), "model loaded");
let pid = pid_on_port(PORT).expect("the server should be listening");
let killed = std::time::Instant::now();
std::process::Command::new("taskkill")
.args(["/F", "/PID", &pid.to_string()])
.output()
.expect("taskkill");
let status = rx.recv().await;
let noticed = killed.elapsed();
println!("child death noticed in {noticed:?}: {status:?}");
assert!(
matches!(status, Some(ServerStatus::Disconnected(_))),
"a dead child must be reported, got {status:?}"
);
assert!(
noticed < Duration::from_secs(10),
"should come from the exit signal, not a probe — took {noticed:?}"
);
}
#[tokio::test]
#[ignore = "requires a live embedding server (MINDFORK_EMBED_URL); takes ~70s"]
async fn monitor_does_not_flap_against_a_live_server() {
let Ok(url) = std::env::var("MINDFORK_EMBED_URL") else {
eprintln!("skip: MINDFORK_EMBED_URL not set");
return;
};
let (tx, mut rx) = unbounded_channel();
LlamaSupervisor::default().apply_embed(
&embed_external(&url),
None,
CancellationToken::new(),
tx,
ru(),
);
assert_eq!(
rx.recv().await,
Some(ServerStatus::Ready),
"initial verdict"
);
let watch = HEALTHY_POLL + Duration::from_secs(10);
println!("watching {url} for {watch:?} — any status published here is a flap");
tokio::time::sleep(watch).await;
match rx.try_recv() {
Err(_) => println!("no flap: the monitor stayed quiet"),
Ok(s) => panic!("the monitor flapped against a healthy server: {s:?}"),
}
}
#[tokio::test]
#[ignore = "requires a live embedding server (MINDFORK_EMBED_URL)"]
async fn embed_probe_reaches_ready_on_live_server() {
let Ok(url) = std::env::var("MINDFORK_EMBED_URL") else {
eprintln!("skip: MINDFORK_EMBED_URL not set");
return;
};
let (tx, mut rx) = unbounded_channel();
let setup = LlamaSupervisor::default().apply_embed(
&embed_external(&url),
None,
CancellationToken::new(),
tx,
ru(),
);
assert_eq!(setup.status, ServerStatus::Connecting);
let status = rx.recv().await;
println!("live embedding server {url} probed as: {status:?}");
assert_eq!(status, Some(ServerStatus::Ready));
}
#[tokio::test]
async fn embed_external_url_is_available() {
let setup = embed_setup(&embed_external("http://127.0.0.1:9/v1"));
assert!(setup.handle.is_none());
let err = setup
.embedder
.embed(vec!["x".into()], EmbedRole::Passage)
.await
.unwrap_err();
assert!(!err.to_string().contains("не настроен"), "{err}");
}
#[tokio::test]
async fn embed_unconfigured_is_unavailable() {
let setup = embed_setup(&EmbedSettings::default());
let err = setup
.embedder
.embed(vec!["x".into()], EmbedRole::Passage)
.await
.unwrap_err();
assert!(err.to_string().contains("not configured"));
}
}