9e8db7518d
The `document.md` file has been replaced by `document.json`. This new format acts as a JSON envelope, containing the original markdown content along with metadata such as `covered_mtime`, `written_at`, and `backend_id`. This change addresses a bug where new recordings arriving during an LLM analysis call could be ignored. The `covered_mtime` field in the new JSON envelope ensures that the document's metadata accurately reflects the state of recordings at the time of analysis, preventing incorrect "analysis current" states. Additionally, the `analysis_input.json` file is now removed upon successful analysis, and a failure marker is used to indicate analysis failures. This ensures that a case is re-analyzed if necessary. The `analysis_idle_threshold_secs` configuration option has been introduced to control the delay before a case in the `Required` state is automatically promoted to `Idle` and enqueued for LLM analysis.
222 lines
7.8 KiB
Rust
222 lines
7.8 KiB
Rust
//! `Config` construction for tests.
|
|
//!
|
|
//! Collapses the ~13 lookalike `test_config_*` / `build_config` helpers
|
|
//! scattered across the integration tests into one fluent builder.
|
|
//! Every builder call is a minimal override on top of
|
|
//! `Config::test_default()`; the builder automatically derives the
|
|
//! `api_key → slug` index map from the collected users.
|
|
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
use std::sync::{Arc, OnceLock};
|
|
|
|
use doctate_server::config::{Config, User};
|
|
use doctate_server::settings::Settings;
|
|
|
|
/// Make a (test) `IONOS_API_KEY` visible before the static backend catalog
|
|
/// in `analyze::backend` reads it. Idempotent — only the first call writes
|
|
/// the env var; later calls are a no-op. Tests that exercise paths gated
|
|
/// by `analyze::backend::any_backend_available()` should call this (the
|
|
/// builder does it automatically when `with_llm` is invoked).
|
|
///
|
|
/// SAFETY: in Edition 2024, `std::env::set_var` is `unsafe` because it
|
|
/// races with concurrent `getenv` on POSIX. The `OnceLock` guarantees
|
|
/// that the write happens at most once, before any test thread has had
|
|
/// a chance to read the env via `analyze::backend::backends()`. As long
|
|
/// as `ensure_test_llm_env()` is called in `TestConfig::new()` (i.e. at
|
|
/// the top of every test setup), no other test thread observes the
|
|
/// env-var being mutated mid-flight.
|
|
pub fn ensure_test_llm_env() {
|
|
static ONCE: OnceLock<()> = OnceLock::new();
|
|
ONCE.get_or_init(|| {
|
|
// SAFETY: see function docstring — single-threaded init under OnceLock.
|
|
unsafe {
|
|
std::env::set_var("IONOS_API_KEY", "test-key");
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Unique tempdir path for a given label. `process::id()` + UUID v4
|
|
/// keeps parallel `cargo test` runs from colliding; the label lets
|
|
/// humans identify which test owns which leftover directory when
|
|
/// cleanup fails.
|
|
pub fn unique_tmpdir(label: &str) -> PathBuf {
|
|
std::env::temp_dir().join(format!(
|
|
"doctate-test-{label}-{}-{}",
|
|
std::process::id(),
|
|
uuid::Uuid::new_v4()
|
|
))
|
|
}
|
|
|
|
/// Fluent builder for `Arc<Config>`. All methods take `self` by value
|
|
/// and return `Self` so calls chain naturally:
|
|
///
|
|
/// ```ignore
|
|
/// let cfg = TestConfig::new()
|
|
/// .with_user(test_user("dr_a"))
|
|
/// .with_llm(mock.uri())
|
|
/// .build();
|
|
/// ```
|
|
pub struct TestConfig {
|
|
data_path: Option<PathBuf>,
|
|
users: Vec<User>,
|
|
llm_url: Option<String>,
|
|
llm_api_key: Option<String>,
|
|
llm_model: Option<String>,
|
|
whisper_url: Option<String>,
|
|
whisper_timeout_seconds: Option<u64>,
|
|
ollama_url: Option<String>,
|
|
idle_threshold_secs: Option<u64>,
|
|
label: &'static str,
|
|
}
|
|
|
|
impl TestConfig {
|
|
/// Start a fresh builder. `label` is used only to name the tempdir
|
|
/// when no explicit `data_path` is set via `with_data_path`.
|
|
pub fn new() -> Self {
|
|
// Pre-seed the test API key so any code path that touches the
|
|
// backend catalog observes a non-empty `IONOS_API_KEY`.
|
|
ensure_test_llm_env();
|
|
Self {
|
|
data_path: None,
|
|
users: Vec::new(),
|
|
llm_url: None,
|
|
llm_api_key: None,
|
|
llm_model: None,
|
|
whisper_url: None,
|
|
whisper_timeout_seconds: None,
|
|
ollama_url: None,
|
|
idle_threshold_secs: None,
|
|
label: "common",
|
|
}
|
|
}
|
|
|
|
/// Label used when auto-generating the tempdir name.
|
|
pub fn with_label(mut self, label: &'static str) -> Self {
|
|
self.label = label;
|
|
self
|
|
}
|
|
|
|
/// Override the data path. By default `build()` generates a unique
|
|
/// tempdir under `std::env::temp_dir()`.
|
|
pub fn with_data_path(mut self, path: PathBuf) -> Self {
|
|
self.data_path = Some(path);
|
|
self
|
|
}
|
|
|
|
/// Add a single user. Can be called repeatedly.
|
|
pub fn with_user(mut self, user: User) -> Self {
|
|
self.users.push(user);
|
|
self
|
|
}
|
|
|
|
/// Replace the user list outright.
|
|
pub fn with_users(mut self, users: Vec<User>) -> Self {
|
|
self.users = users;
|
|
self
|
|
}
|
|
|
|
/// Configure a hosted-provider LLM with the common test defaults
|
|
/// (`api_key = "test-key"`, `model = "test-model"`). For Ollama-style
|
|
/// endpoints that expect no `Authorization` header, use
|
|
/// [`with_llm_explicit`] and pass an empty api_key.
|
|
pub fn with_llm(mut self, url: impl Into<String>) -> Self {
|
|
self.llm_url = Some(url.into());
|
|
self.llm_api_key.get_or_insert_with(|| "test-key".into());
|
|
self.llm_model.get_or_insert_with(|| "test-model".into());
|
|
self
|
|
}
|
|
|
|
/// Configure the LLM with explicit values. `api_key` may be empty
|
|
/// to exercise the Ollama-style no-auth path.
|
|
pub fn with_llm_explicit(
|
|
mut self,
|
|
url: impl Into<String>,
|
|
api_key: impl Into<String>,
|
|
model: impl Into<String>,
|
|
) -> Self {
|
|
self.llm_url = Some(url.into());
|
|
self.llm_api_key = Some(api_key.into());
|
|
self.llm_model = Some(model.into());
|
|
self
|
|
}
|
|
|
|
/// Whisper ASR mock URL + timeout.
|
|
pub fn with_whisper(mut self, url: impl Into<String>, timeout_seconds: u64) -> Self {
|
|
self.whisper_url = Some(url.into());
|
|
self.whisper_timeout_seconds = Some(timeout_seconds);
|
|
self
|
|
}
|
|
|
|
/// Ollama (oneliner generator) mock URL.
|
|
pub fn with_ollama(mut self, url: impl Into<String>) -> Self {
|
|
self.ollama_url = Some(url.into());
|
|
self
|
|
}
|
|
|
|
/// Idle threshold in seconds: how long after the latest recording mtime
|
|
/// a `Required` case is auto-promoted to `Idle` (and enqueued). Default
|
|
/// is `Config::test_default()` (= `0`, eager). Tests that exercise the
|
|
/// "wait for quiet period" branch set a positive value.
|
|
pub fn with_idle_threshold(mut self, secs: u64) -> Self {
|
|
self.idle_threshold_secs = Some(secs);
|
|
self
|
|
}
|
|
|
|
/// Materialize the config alone. Bucket-B overrides (`with_llm`,
|
|
/// `with_whisper`, `with_ollama`) are silently dropped — call
|
|
/// [`build_pair`] when those need to take effect.
|
|
pub fn build(self) -> Arc<Config> {
|
|
self.build_pair().0
|
|
}
|
|
|
|
/// Materialize both `Config` and `Settings`. Use when a test exercises
|
|
/// LLM, Whisper or Ollama paths and needs the builder-set values to
|
|
/// reach the worker.
|
|
pub fn build_pair(self) -> (Arc<Config>, Arc<Settings>) {
|
|
let data_path = self.data_path.unwrap_or_else(|| unique_tmpdir(self.label));
|
|
let api_keys: HashMap<String, String> = self
|
|
.users
|
|
.iter()
|
|
.map(|u| (u.api_key.clone(), u.slug.clone()))
|
|
.collect();
|
|
|
|
let mut config = Config {
|
|
data_path,
|
|
users: self.users,
|
|
api_keys,
|
|
..Config::test_default()
|
|
};
|
|
if let Some(v) = self.idle_threshold_secs {
|
|
config.analysis_idle_threshold_secs = v;
|
|
}
|
|
let config = Arc::new(config);
|
|
|
|
let mut settings = Settings::default();
|
|
// LLM-related fields used to live on `Settings`; with the multi-backend
|
|
// refactor they are now owned by `analyze::backend`. The `with_llm*`
|
|
// builder methods are kept for source-compat but their stored URL/
|
|
// api_key/model values no longer flow into `settings`. Tests that
|
|
// rely on a working LLM mock must inject backends through other
|
|
// means (currently: those tests are #[ignore]'d, see the test
|
|
// attribute comment).
|
|
let _ = (self.llm_url, self.llm_api_key, self.llm_model);
|
|
if let Some(v) = self.whisper_url {
|
|
settings.whisper.url = v;
|
|
}
|
|
if let Some(v) = self.whisper_timeout_seconds {
|
|
settings.whisper.timeout_seconds = v;
|
|
}
|
|
if let Some(v) = self.ollama_url {
|
|
settings.ollama.url = v;
|
|
}
|
|
(config, Arc::new(settings))
|
|
}
|
|
}
|
|
|
|
impl Default for TestConfig {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|