#![cfg(all(feature = "extractor-http", feature = "embedder-http"))]
use std::process::{Command, Output};
const STARTUP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
fn start_with_extractor(backend: &str) -> Output {
let store = tempfile::tempdir().expect("tempdir");
let mut child = Command::new(env!("CARGO_BIN_EXE_velesdb-memory"))
.env_clear()
.env("HOME", store.path())
.env("VELESDB_MEMORY_PATH", store.path())
.env("VELESDB_MEMORY_EXTRACTOR", backend)
.env("VELESDB_MEMORY_QUIET", "1")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.expect("failed to spawn velesdb-memory");
let start = std::time::Instant::now();
loop {
if let Some(_status) = child.try_wait().expect("wait on velesdb-memory") {
return child.wait_with_output().expect("collect output");
}
assert!(
start.elapsed() < STARTUP_TIMEOUT,
"velesdb-memory did not exit within {STARTUP_TIMEOUT:?} — it should \
have refused its extractor configuration at startup"
);
std::thread::sleep(std::time::Duration::from_millis(50));
}
}
fn stderr_of(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).into_owned()
}
#[test]
fn openai_refuses_without_a_url_because_it_has_no_default_server() {
let output = start_with_extractor("openai");
let stderr = stderr_of(&output);
assert!(
!output.status.success(),
"a backend missing its required configuration must not start; got {output:?}"
);
assert!(
stderr.contains("VELESDB_MEMORY_EXTRACTOR_URL"),
"the refusal must name the variable to set. An `openai` request served \
by the Ollama builder would have defaulted the URL and never mentioned \
it — which is precisely the bug. Got: {stderr}"
);
}
#[test]
fn ollama_still_defaults_its_url_and_asks_only_for_the_model() {
let output = start_with_extractor("ollama");
let stderr = stderr_of(&output);
assert!(!output.status.success(), "no model configured, so no start");
assert!(
stderr.contains("VELESDB_MEMORY_EXTRACTOR_MODEL"),
"Ollama's own refusal is about the model, got: {stderr}"
);
assert!(
!stderr.contains("VELESDB_MEMORY_EXTRACTOR_URL"),
"Ollama defaults its URL to the canonical local address and must keep \
doing so — naming the URL here would mean the `openai` builder answered \
for it. Got: {stderr}"
);
}
#[test]
fn an_unknown_backend_is_refused_before_any_client_is_built() {
let output = start_with_extractor("lmstudio");
let stderr = stderr_of(&output);
assert!(!output.status.success(), "an unknown name must not start");
assert!(
stderr.contains("lmstudio") && stderr.contains("openai"),
"the refusal must quote what was asked for and name the accepted forms, \
got: {stderr}"
);
}
fn start_with_embedder(backend: &str, extra: &[(&str, &str)]) -> Output {
let store = tempfile::tempdir().expect("tempdir");
let mut command = Command::new(env!("CARGO_BIN_EXE_velesdb-memory"));
command
.env_clear()
.env("HOME", store.path())
.env("VELESDB_MEMORY_PATH", store.path())
.env("VELESDB_MEMORY_EMBEDDER", backend);
for (name, value) in extra {
command.env(name, value);
}
command
.stdin(std::process::Stdio::null())
.output()
.expect("failed to spawn velesdb-memory")
}
#[test]
fn the_embedding_role_refuses_openai_without_a_url_too() {
let output = start_with_embedder("openai", &[]);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!output.status.success(), "no URL configured, so no start");
assert!(
stderr.contains("VELESDB_MEMORY_EMBEDDER_URL"),
"the refusal must name the embedding role's own variable, got: {stderr}"
);
}
#[test]
fn the_role_named_url_wins_over_the_legacy_alias_and_says_so_once() {
let output = start_with_embedder(
"openai",
&[
("VELESDB_MEMORY_EMBEDDER_URL", "http://127.0.0.1:1"),
("VELESDB_MEMORY_OLLAMA_URL", "http://127.0.0.1:2"),
("VELESDB_MEMORY_EMBEDDER_MODEL", "bge-m3"),
],
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("127.0.0.1:1"),
"the role-named URL must be the one actually called, got: {stderr}"
);
assert!(
!stderr.contains("127.0.0.1:2"),
"the legacy alias must not be what was called, got: {stderr}"
);
let notices = stderr
.lines()
.filter(|line| line.contains("VELESDB_MEMORY_OLLAMA_URL"))
.count();
assert_eq!(
notices, 1,
"exactly ONE notice about the disagreement — not one per read, not none. \
Got: {stderr}"
);
}
#[test]
fn the_legacy_alias_alone_still_configures_the_embedder() {
let output = start_with_embedder(
"openai",
&[
("VELESDB_MEMORY_OLLAMA_URL", "http://127.0.0.1:2"),
("VELESDB_MEMORY_OLLAMA_MODEL", "bge-m3"),
],
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("127.0.0.1:2"),
"the legacy alias must still be honoured when it is the only one set, \
got: {stderr}"
);
assert!(
!stderr.contains("VELESDB_MEMORY_OLLAMA_URL is set under two names"),
"using the alias as intended is not a conflict, got: {stderr}"
);
}
#[test]
fn an_empty_api_token_is_refused_rather_than_sent_as_a_blank_bearer() {
let store = tempfile::tempdir().expect("tempdir");
let output = Command::new(env!("CARGO_BIN_EXE_velesdb-memory"))
.env_clear()
.env("HOME", store.path())
.env("VELESDB_MEMORY_PATH", store.path())
.env("VELESDB_MEMORY_EXTRACTOR", "openai")
.env("VELESDB_MEMORY_EXTRACTOR_URL", "http://localhost:8028")
.env("VELESDB_MEMORY_EXTRACTOR_MODEL", "ornith")
.env("VELESDB_MEMORY_EXTRACTOR_API_TOKEN", "")
.env("VELESDB_MEMORY_QUIET", "1")
.stdin(std::process::Stdio::null())
.output()
.expect("failed to spawn velesdb-memory");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(!output.status.success(), "an empty token must not start");
assert!(
stderr.contains("VELESDB_MEMORY_EXTRACTOR_API_TOKEN"),
"the refusal must name the variable, got: {stderr}"
);
}