use std::sync::Arc;
use khive_db::StorageBackend;
pub use khive_db::WalCeilingSource;
use khive_gate::{ActorRef, AllowAllGate, GateRef};
use khive_types::Namespace;
use lattice_embed::EmbeddingModel;
use crate::error::RuntimeResult;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct BackendId(String);
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum BackendIdError {
#[error("backend id must not be empty or whitespace-only")]
Empty,
}
impl BackendId {
pub const MAIN: &'static str = "main";
pub fn parse(name: impl Into<String>) -> Result<Self, BackendIdError> {
let name = name.into();
if name.trim().is_empty() {
return Err(BackendIdError::Empty);
}
Ok(Self(name))
}
pub fn main() -> Self {
Self(Self::MAIN.to_string())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<String> for BackendId {
type Error = BackendIdError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::parse(value)
}
}
impl TryFrom<&str> for BackendId {
type Error = BackendIdError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::parse(value)
}
}
impl std::str::FromStr for BackendId {
type Err = BackendIdError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::parse(value)
}
}
impl std::fmt::Display for BackendId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[cfg(test)]
mod backend_id_tests {
use super::BackendId;
#[test]
fn empty_and_whitespace_only_backend_ids_are_rejected() {
for invalid in ["", " ", "\t\n"] {
assert!(
BackendId::parse(invalid).is_err(),
"backend id {invalid:?} must be rejected"
);
}
}
#[test]
fn nonempty_backend_id_round_trips() {
let id = BackendId::parse("archive").expect("valid backend id");
assert_eq!(id.as_str(), "archive");
assert_eq!(id.to_string(), "archive");
}
}
mod private {
#[derive(Clone, Debug)]
pub(crate) struct Sealed;
}
#[derive(Clone, Debug)]
pub struct NamespaceToken {
namespace: Namespace,
gate_namespace: Namespace,
gate_explicit_namespace: Option<String>,
request_id: Option<u64>,
visible: Vec<Namespace>,
actor: ActorRef,
process_ref: Option<String>,
_sealed: private::Sealed,
}
impl NamespaceToken {
pub(crate) fn mint_with_visibility(
namespace: Namespace,
extra_visible: Vec<Namespace>,
actor: ActorRef,
) -> Self {
let mut visible = vec![namespace.clone()];
for ns in extra_visible {
if !visible.contains(&ns) {
visible.push(ns);
}
}
debug_assert!(!visible.is_empty(), "visible set must be non-empty");
Self {
gate_namespace: namespace.clone(),
gate_explicit_namespace: None,
request_id: None,
namespace,
visible,
actor,
process_ref: None,
_sealed: private::Sealed,
}
}
pub(crate) fn mint_authorized(namespace: Namespace, actor: ActorRef) -> Self {
Self::mint_with_visibility(namespace, vec![], actor)
}
#[allow(dead_code)]
pub(crate) fn local() -> Self {
Self::mint_authorized(Namespace::local(), ActorRef::anonymous())
}
#[allow(dead_code)]
pub(crate) fn for_namespace(ns: Namespace) -> Self {
Self::mint_authorized(ns, ActorRef::anonymous())
}
pub fn namespace(&self) -> &Namespace {
&self.namespace
}
pub fn gate_namespace(&self) -> &Namespace {
&self.gate_namespace
}
pub(crate) fn with_gate_namespace(mut self, namespace: Namespace) -> Self {
self.gate_namespace = namespace;
self
}
pub(crate) fn gate_explicit_namespace(&self) -> Option<&str> {
self.gate_explicit_namespace.as_deref()
}
pub(crate) fn with_gate_explicit_namespace(mut self, namespace: Option<String>) -> Self {
self.gate_explicit_namespace = namespace;
self
}
pub(crate) fn request_id(&self) -> Option<u64> {
self.request_id
}
pub(crate) fn with_request_id(mut self, request_id: Option<u64>) -> Self {
self.request_id = request_id;
self
}
pub fn visible_namespaces(&self) -> &[Namespace] {
&self.visible
}
pub fn visible_namespace_strs(&self) -> Vec<&str> {
self.visible.iter().map(|ns| ns.as_str()).collect()
}
pub fn actor(&self) -> &ActorRef {
&self.actor
}
pub fn process_ref(&self) -> Option<&str> {
self.process_ref.as_deref()
}
pub(crate) fn with_process_ref(mut self, process_ref: Option<String>) -> Self {
self.process_ref = process_ref;
self
}
pub fn with_namespace(&self, ns: Namespace) -> Self {
Self::mint_authorized(ns, self.actor.clone()).with_process_ref(self.process_ref.clone())
}
}
pub fn process_ref_from_env() -> Option<String> {
match std::env::var("KHIVE_PROCESS_REF") {
Ok(value) => Some(value),
Err(std::env::VarError::NotPresent) => None,
Err(std::env::VarError::NotUnicode(_)) => {
tracing::warn!(
"KHIVE_PROCESS_REF is not valid Unicode and cannot be represented in request metadata"
);
None
}
}
}
#[derive(Clone, Debug)]
pub struct RuntimeConfig {
pub credentials: Vec<crate::credentials::CredentialConfig>,
pub visibility_receipts: Option<crate::credentials::VisibilityReceiptConfig>,
pub mounts: Vec<crate::mount_config::MountConfig>,
pub db_path: Option<std::path::PathBuf>,
pub wal_ceiling_bytes: u64,
pub wal_ceiling_configured_bytes: u64,
pub wal_ceiling_source: WalCeilingSource,
pub wal_ceiling_env_raw: Option<String>,
pub disk_guard_environment: khive_db::DiskGuardEnvironment,
pub disk_guard_config: Option<khive_db::EffectiveDiskGuardConfig>,
pub volume_lock_dir: Option<std::path::PathBuf>,
pub default_namespace: Namespace,
pub embedding_model: Option<EmbeddingModel>,
pub additional_embedding_models: Vec<EmbeddingModel>,
pub gate: GateRef,
pub packs: Vec<String>,
pub blob_hydration_bytes: u64,
pub backend_id: BackendId,
pub brain_profile: Option<String>,
pub visible_namespaces: Vec<Namespace>,
pub allowed_outbound_namespaces: Vec<Namespace>,
pub actor_id: Option<String>,
pub brain: crate::engine_config::BrainSectionConfig,
pub git_write: crate::engine_config::GitWriteSectionConfig,
pub blob: crate::engine_config::BlobSectionConfig,
pub exec: crate::engine_config::ExecSectionConfig,
pub telemetry: crate::telemetry_config::TelemetryConfig,
pub display_timezone: chrono_tz::Tz,
pub events_split: Option<crate::events_split::EventsSplitConfig>,
pub web: crate::engine_config::WebSectionConfig,
}
pub fn parse_pack_list(s: &str) -> Vec<String> {
s.split(|c: char| c == ',' || c.is_whitespace())
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect()
}
const ANN_REBUILD_THRESHOLD_DEFAULT: f64 = 0.20;
pub fn ann_rebuild_threshold_from_env() -> f64 {
std::env::var("KHIVE_ANN_REBUILD_THRESHOLD")
.ok()
.and_then(|v| v.parse::<f64>().ok())
.filter(|v| *v > 0.0 && *v <= 1.0)
.unwrap_or(ANN_REBUILD_THRESHOLD_DEFAULT)
}
#[cfg(test)]
#[path = "config/ann_rebuild_threshold_tests.rs"]
mod ann_rebuild_threshold_tests;
fn ann_fresh_tail_enabled_from_value(value: Option<&str>) -> bool {
value != Some("0")
}
pub fn ann_fresh_tail_enabled_from_env() -> bool {
let value = std::env::var("KHIVE_ANN_FRESH_TAIL").ok();
ann_fresh_tail_enabled_from_value(value.as_deref())
}
pub fn resolve_default_display_timezone() -> chrono_tz::Tz {
iana_time_zone::get_timezone()
.ok()
.and_then(|name| name.parse::<chrono_tz::Tz>().ok())
.unwrap_or(chrono_tz::Tz::UTC)
}
impl Default for RuntimeConfig {
fn default() -> Self {
let db_path = std::env::var("HOME")
.ok()
.map(|h| std::path::PathBuf::from(h).join(".khive/khive.db"));
let embedding_model = std::env::var("KHIVE_EMBEDDING_MODEL")
.ok()
.and_then(|s| s.parse().ok())
.or(Some(EmbeddingModel::AllMiniLmL6V2));
let additional_embedding_models = std::env::var("KHIVE_ADDITIONAL_EMBEDDING_MODELS")
.ok()
.map(|s| parse_embedding_model_list(&s))
.unwrap_or_default();
let packs = std::env::var("KHIVE_PACKS")
.ok()
.map(|s| parse_pack_list(&s))
.filter(|v| !v.is_empty())
.unwrap_or_else(Self::built_in_packs);
let brain_profile = std::env::var("KHIVE_BRAIN_PROFILE")
.ok()
.filter(|s| !s.trim().is_empty());
let actor_id = std::env::var("KHIVE_ACTOR")
.ok()
.filter(|s| !s.trim().is_empty());
Self {
credentials: Vec::new(),
visibility_receipts: None,
db_path,
wal_ceiling_bytes: 0,
wal_ceiling_configured_bytes: 0,
wal_ceiling_source: WalCeilingSource::Default,
wal_ceiling_env_raw: std::env::var_os("KHIVE_SQLITE_WAL_CEILING_BYTES")
.map(|value| value.to_string_lossy().into_owned()),
disk_guard_environment: khive_db::DiskGuardEnvironment::capture(),
disk_guard_config: None,
volume_lock_dir: crate::daemon::volume_lock_dir().ok(),
default_namespace: Namespace::local(),
embedding_model,
additional_embedding_models,
gate: Arc::new(AllowAllGate),
packs,
blob_hydration_bytes: crate::blob::DEFAULT_BLOB_HYDRATION_BYTES,
backend_id: BackendId::main(),
brain_profile,
visible_namespaces: vec![],
allowed_outbound_namespaces: vec![],
actor_id,
brain: crate::engine_config::BrainSectionConfig::default(),
git_write: crate::engine_config::GitWriteSectionConfig::default(),
blob: crate::engine_config::BlobSectionConfig {
file_transfers: std::env::var("KHIVE_FILE_TRANSFERS")
.map(|value| value == "1")
.unwrap_or(false),
},
exec: crate::engine_config::ExecSectionConfig::default(),
telemetry: crate::telemetry_config::TelemetryConfig::default(),
mounts: Vec::new(),
display_timezone: resolve_default_display_timezone(),
events_split: None,
web: crate::engine_config::WebSectionConfig::default(),
}
}
}
impl RuntimeConfig {
pub fn wal_ceiling_policy(&self) -> khive_db::WalCeilingPolicy {
khive_db::WalCeilingPolicy {
bytes: if self.wal_ceiling_configured_bytes == 0 {
self.wal_ceiling_bytes
} else {
self.wal_ceiling_configured_bytes
},
source: self.wal_ceiling_source,
}
}
pub fn resolve_wal_ceiling_policy(
&mut self,
read_only: bool,
) -> RuntimeResult<khive_db::WalCeilingPolicy> {
let policy = self.wal_ceiling_policy();
let backend_field = match self.wal_ceiling_source {
WalCeilingSource::BackendField => Some(policy.bytes),
WalCeilingSource::Environment if self.wal_ceiling_env_raw.is_some() => None,
_ if policy.bytes != 0 => Some(policy.bytes),
_ => None,
};
let kind = if self.db_path.is_some() {
crate::BackendKind::Sqlite
} else {
crate::BackendKind::Memory
};
let resolved = crate::resolve_wal_ceiling(
backend_field,
self.wal_ceiling_env_raw.as_deref(),
self.backend_id.as_str(),
kind,
true,
read_only,
)
.map_err(|error| khive_db::SqliteError::InvalidConfig(error.to_string()))?;
self.wal_ceiling_configured_bytes = resolved.configured_bytes;
self.wal_ceiling_bytes = resolved.effective_bytes;
self.wal_ceiling_source = resolved.source;
Ok(self.wal_ceiling_policy())
}
pub fn resolve_disk_guard_policy(
&mut self,
read_only: bool,
) -> RuntimeResult<Option<khive_db::EffectiveDiskGuardConfig>> {
if self.db_path.is_none() {
if self
.disk_guard_config
.is_some_and(|policy| policy.reserve_bytes != 0)
{
return Err(khive_db::SqliteError::InvalidConfig(
"nonzero disk_reserve_bytes requires a file-backed SQLite backend".into(),
)
.into());
}
return Ok(None);
}
if read_only {
return Ok(None);
}
let policy = match self.disk_guard_config {
Some(policy) => {
policy.validate()?;
policy
}
None => self.disk_guard_environment.resolve(None, None)?,
};
self.disk_guard_config = Some(policy);
Ok(Some(policy))
}
pub fn for_metadata_registry(mut self) -> Self {
self.db_path = None;
self.wal_ceiling_bytes = 0;
self.wal_ceiling_configured_bytes = 0;
self.wal_ceiling_source = WalCeilingSource::BackendField;
self.disk_guard_config = None;
self.embedding_model = None;
self.additional_embedding_models.clear();
self
}
pub fn built_in_packs() -> Vec<String> {
[
"kg",
"gtd",
"memory",
"brain",
"comm",
"schedule",
"knowledge",
"session",
"tool",
"exec",
"git",
"code",
"workspace",
"blob",
]
.into_iter()
.map(String::from)
.collect()
}
pub fn no_embeddings() -> Self {
Self {
embedding_model: None,
additional_embedding_models: Vec::new(),
..Self::default()
}
}
}
pub fn expand_tilde(path: &std::path::Path) -> std::path::PathBuf {
let s = path.to_string_lossy();
if let Some(rest) = s.strip_prefix("~/") {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
std::path::PathBuf::from(format!("{home}/{rest}"))
} else if s == "~" {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
std::path::PathBuf::from(home)
} else {
path.to_path_buf()
}
}
pub fn resolve_db_anchor(db: Option<&str>) -> Option<std::path::PathBuf> {
match db {
Some(":memory:") => None,
Some(path) => Some(expand_tilde(std::path::Path::new(path))),
None => {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
Some(std::path::PathBuf::from(format!("{home}/.khive/khive.db")))
}
}
}
pub fn assert_db_anchor_consistent(
resolved_db_path: Option<&std::path::Path>,
args_db: Option<&str>,
) -> anyhow::Result<()> {
let db_anchor = resolve_db_anchor(args_db);
assert_captured_db_anchor_consistent(resolved_db_path, db_anchor.as_deref())
}
pub fn assert_captured_db_anchor_consistent(
resolved_db_path: Option<&std::path::Path>,
db_anchor: Option<&std::path::Path>,
) -> anyhow::Result<()> {
let Some(anchor) = db_anchor else {
return Ok(());
};
if resolved_db_path != Some(anchor) {
anyhow::bail!(
"db-path resolution drift at server construction: resolved db_path {:?} \
does not match the canonical anchor {:?} computed by resolve_db_anchor \
from the same --db input; this construction path likely recomputed the \
db path independently instead of routing through the shared resolver, \
which would desynchronize config_id from other processes sharing the \
same database",
resolved_db_path,
anchor
);
}
Ok(())
}
pub fn resolve_project_actor_id(
config_path: Option<&std::path::Path>,
) -> Result<Option<String>, crate::engine_config::ConfigError> {
let khive_cfg = crate::engine_config::KhiveConfig::load_with_home_fallback(config_path, None)?;
Ok(khive_cfg
.and_then(|cfg| cfg.actor.id)
.filter(|s| !s.trim().is_empty()))
}
pub(crate) fn vec_model_key(model: EmbeddingModel) -> String {
sanitize_key(&model.to_string())
}
pub(crate) fn sanitize_key(s: &str) -> String {
s.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect()
}
pub(crate) fn build_embedder_registry(
config: &RuntimeConfig,
) -> (crate::embedder_registry::EmbedderRegistry, Arc<str>) {
use crate::embedder_registry::{EmbedderRegistry, LatticeEmbedderProvider};
let mut registry = EmbedderRegistry::new();
for model in configured_embedding_models(config) {
registry.register_builtin(LatticeEmbedderProvider::new(model));
}
let default_embedder_name = config
.embedding_model
.map(|model| Arc::<str>::from(model.to_string()))
.unwrap_or_else(|| Arc::<str>::from(""));
(registry, default_embedder_name)
}
fn configured_embedding_models(config: &RuntimeConfig) -> Vec<EmbeddingModel> {
let mut models: Vec<EmbeddingModel> = Vec::new();
if let Some(model) = config.embedding_model {
models.push(model);
}
for model in config.additional_embedding_models.iter().copied() {
if !models.contains(&model) {
models.push(model);
}
}
models
}
pub(crate) fn register_configured_embedding_models(
backend: &StorageBackend,
config: &RuntimeConfig,
) -> RuntimeResult<()> {
for model in configured_embedding_models(config) {
backend.register_embedding_model(
&model.to_string(),
model.model_id(),
model.key_version(),
model.dimensions() as u32,
)?;
}
Ok(())
}
pub fn runtime_config_from_khive_config(
khive_cfg: &crate::engine_config::KhiveConfig,
base: RuntimeConfig,
) -> RuntimeConfig {
let default_namespace = base.default_namespace.clone();
let mounts = khive_cfg.mounts.clone();
let credentials = khive_cfg.credentials.clone();
let visibility_receipts = khive_cfg.visibility_receipts.clone();
let brain_profile = base.brain_profile.clone().or_else(|| {
khive_cfg
.runtime
.brain_profile
.clone()
.filter(|s| !s.trim().is_empty())
});
let visible_namespaces: Vec<Namespace> = khive_cfg
.actor
.visible_namespaces
.as_deref()
.unwrap_or_default()
.iter()
.filter_map(|s| match Namespace::parse(s) {
Ok(ns) => Some(ns),
Err(e) => {
tracing::warn!(ns = %s, error = %e, "actor.visible_namespaces: invalid namespace; skipped");
None
}
})
.collect();
let visible_namespaces = if let Some(id) = khive_cfg.actor.id.as_deref() {
match Namespace::parse(id) {
Ok(actor_ns) if actor_ns != Namespace::local() => {
let mut v = visible_namespaces;
if !v.contains(&actor_ns) {
v.push(actor_ns);
}
v
}
_ => visible_namespaces,
}
} else {
visible_namespaces
};
let allowed_outbound_namespaces: Vec<Namespace> = khive_cfg
.actor
.allowed_outbound_namespaces
.iter()
.filter_map(|s| match Namespace::parse(s) {
Ok(ns) => Some(ns),
Err(e) => {
tracing::warn!(ns = %s, error = %e, "actor.allowed_outbound_namespaces: invalid namespace; skipped");
None
}
})
.collect();
let actor_id = khive_cfg
.actor
.id
.clone()
.filter(|s| !s.trim().is_empty())
.or_else(|| base.actor_id.clone());
let gate = khive_cfg
.gate
.as_ref()
.map(|gate| {
Arc::new(khive_gate::CallerEnrollmentGate::with_write_denials(
gate.granted_actors.clone(),
gate.grant_unattributed,
gate.deny_writes_for.clone(),
)) as GateRef
})
.unwrap_or_else(|| base.gate.clone());
let gate = crate::mailbox_view::configured_mailbox_gate(&khive_cfg.actor, gate);
let brain = khive_cfg.brain.clone();
let git_write = khive_cfg.git_write.clone();
let blob = crate::engine_config::BlobSectionConfig {
file_transfers: khive_cfg.blob.file_transfers || base.blob.file_transfers,
};
let exec = khive_cfg.exec.clone();
let telemetry = khive_cfg.telemetry.clone();
let web = khive_cfg.web.clone();
let blob_hydration_bytes = khive_cfg
.runtime
.blob_hydration_bytes
.unwrap_or(base.blob_hydration_bytes);
let display_timezone = khive_cfg
.display
.timezone
.as_deref()
.and_then(|s| s.parse::<chrono_tz::Tz>().ok())
.unwrap_or(base.display_timezone);
if khive_cfg.engines.is_empty() {
return RuntimeConfig {
credentials,
visibility_receipts,
default_namespace,
brain_profile,
visible_namespaces,
allowed_outbound_namespaces,
actor_id,
gate,
brain,
git_write,
blob,
exec,
telemetry,
mounts,
blob_hydration_bytes,
display_timezone,
web,
..base
};
}
let mut embedding_model: Option<EmbeddingModel> = None;
let mut additional: Vec<EmbeddingModel> = Vec::new();
for engine in &khive_cfg.engines {
match parse_embedding_model_alias(&engine.model) {
Some(model) => {
if engine.default {
embedding_model = Some(model);
} else {
additional.push(model);
}
}
None => {
tracing::warn!(
engine = %engine.name,
model = %engine.model,
"engine config: unknown model name; engine will be skipped"
);
}
}
}
RuntimeConfig {
credentials,
visibility_receipts,
embedding_model,
additional_embedding_models: additional,
default_namespace,
brain_profile,
visible_namespaces,
allowed_outbound_namespaces,
actor_id,
gate,
brain,
git_write,
blob,
exec,
telemetry,
mounts,
blob_hydration_bytes,
display_timezone,
web,
..base
}
}
#[cfg(test)]
mod display_timezone_tests {
use super::resolve_default_display_timezone;
#[test]
fn resolve_default_display_timezone_never_panics() {
let _tz = resolve_default_display_timezone();
}
}
fn parse_embedding_model_list(s: &str) -> Vec<EmbeddingModel> {
parse_pack_list(s)
.into_iter()
.filter_map(|raw| {
let parsed = parse_embedding_model_alias(&raw);
if parsed.is_none() && !raw.trim().is_empty() {
tracing::warn!(
model = %raw,
"KHIVE_ADDITIONAL_EMBEDDING_MODELS contains unknown model name; ignored. \
Valid forms: short alias like 'paraphrase' or a fully-qualified key \
from lattice_embed::EmbeddingModel::from_str."
);
}
parsed
})
.collect()
}
pub(crate) fn parse_embedding_model_alias(name: &str) -> Option<EmbeddingModel> {
let normalized = name.trim().to_ascii_lowercase().replace('_', "-");
match normalized.as_str() {
"paraphrase" => Some(EmbeddingModel::ParaphraseMultilingualMiniLmL12V2),
_ => normalized.parse().ok(),
}
}
#[cfg(test)]
mod resolve_db_anchor_tests {
use super::resolve_db_anchor;
#[test]
fn memory_sentinel_maps_to_none() {
assert_eq!(resolve_db_anchor(Some(":memory:")), None);
}
#[test]
fn explicit_path_maps_to_some() {
assert_eq!(
resolve_db_anchor(Some("/tmp/khive-anchor-test.db")),
Some(std::path::PathBuf::from("/tmp/khive-anchor-test.db"))
);
}
#[test]
fn absent_maps_to_home_default() {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
let expected = std::path::PathBuf::from(format!("{home}/.khive/khive.db"));
assert_eq!(resolve_db_anchor(None), Some(expected));
}
}
#[cfg(test)]
mod assert_db_anchor_consistent_tests {
use super::{assert_captured_db_anchor_consistent, resolve_db_anchor};
use crate::assert_db_anchor_consistent;
#[test]
fn diverging_db_path_is_rejected_naming_both_paths() {
let args_db = "/tmp/khive-anchor-guard-real.db";
let anchor = resolve_db_anchor(Some(args_db)).expect("explicit path always anchors");
let wrong = std::path::PathBuf::from("/tmp/khive-anchor-guard-wrong.db");
let err =
assert_captured_db_anchor_consistent(Some(wrong.as_path()), Some(anchor.as_path()))
.expect_err("a resolved db_path diverging from the anchor must be rejected");
let msg = err.to_string();
assert!(
msg.contains(&wrong.display().to_string()),
"error must name the resolved (wrong) path: {msg}"
);
assert!(
msg.contains(&anchor.display().to_string()),
"error must name the canonical anchor path: {msg}"
);
}
#[test]
fn matching_explicit_db_path_passes() {
let args_db = "/tmp/khive-anchor-guard-consistent.db";
let anchor = resolve_db_anchor(Some(args_db)).expect("explicit path always anchors");
assert!(assert_captured_db_anchor_consistent(
Some(anchor.as_path()),
Some(anchor.as_path())
)
.is_ok());
}
#[test]
fn memory_sentinel_anchor_is_inert() {
let bogus = std::path::PathBuf::from("/tmp/should-not-matter.db");
assert!(assert_captured_db_anchor_consistent(Some(bogus.as_path()), None).is_ok());
assert!(assert_captured_db_anchor_consistent(None, None).is_ok());
}
#[test]
fn normal_boot_with_db_unset_passes_silently() {
let anchor = resolve_db_anchor(None);
assert!(assert_captured_db_anchor_consistent(anchor.as_deref(), anchor.as_deref()).is_ok());
}
#[test]
fn public_compatibility_wrapper_accepts_path_and_memory_sentinel() {
let args_db = "/tmp/khive-anchor-guard-public-api.db";
let anchor = resolve_db_anchor(Some(args_db)).expect("explicit path always anchors");
assert!(assert_db_anchor_consistent(Some(anchor.as_path()), Some(args_db)).is_ok());
let unrelated = std::path::Path::new("/tmp/khive-anchor-guard-unrelated.db");
assert!(assert_db_anchor_consistent(Some(unrelated), Some(":memory:")).is_ok());
}
}
#[cfg(test)]
mod resolve_project_actor_id_tests {
use super::resolve_project_actor_id;
fn write_toml(dir: &tempfile::TempDir, body: &str) -> std::path::PathBuf {
let path = dir.path().join("config.toml");
std::fs::write(&path, body).expect("write config.toml");
path
}
#[test]
fn extracts_non_empty_actor_id_from_explicit_path() {
let dir = tempfile::tempdir().expect("tempdir");
let path = write_toml(&dir, "[actor]\nid = \"lambda:explicit-actor\"\n");
assert_eq!(
resolve_project_actor_id(Some(&path)).expect("no error"),
Some("lambda:explicit-actor".to_string())
);
}
#[test]
fn missing_explicit_path_fails_loud() {
let missing = std::path::PathBuf::from("/nonexistent/khive-project-actor-test/config.toml");
let err = resolve_project_actor_id(Some(&missing))
.expect_err("a missing explicit path must fail loud");
assert!(
matches!(
err,
crate::engine_config::ConfigError::ExplicitConfigMissing { .. }
),
"expected ExplicitConfigMissing, got {err:?}"
);
}
#[test]
fn propagates_load_error_for_invalid_actor_id() {
let dir = tempfile::tempdir().expect("tempdir");
let path = write_toml(&dir, "[actor]\nid = \"\"\n");
let err = resolve_project_actor_id(Some(&path)).expect_err("invalid actor.id must error");
let root = match &err {
crate::engine_config::ConfigError::InFile { source, .. } => source.as_ref(),
other => other,
};
assert!(
matches!(
root,
crate::engine_config::ConfigError::InvalidActorId { .. }
),
"expected InvalidActorId, got {err:?}"
);
}
#[test]
fn returns_none_when_config_has_no_actor_section() {
let dir = tempfile::tempdir().expect("tempdir");
let path = write_toml(
&dir,
"[[engines]]\nname = \"primary\"\nmodel = \"bge-small-en-v1.5\"\ndefault = true\n",
);
assert_eq!(
resolve_project_actor_id(Some(&path)).expect("no error"),
None,
"a config file with no [actor] section must resolve to None"
);
}
}
#[cfg(test)]
mod no_embeddings_tests {
use super::*;
use serial_test::serial;
#[test]
fn no_embeddings_clears_both_fields() {
let config = RuntimeConfig::no_embeddings();
assert_eq!(config.embedding_model, None);
assert!(config.additional_embedding_models.is_empty());
assert!(
configured_embedding_models(&config).is_empty(),
"no_embeddings() must yield zero configured embedders"
);
}
#[test]
fn blob_hydration_default_is_four_portable_whole_objects() {
assert_eq!(
RuntimeConfig::default().blob_hydration_bytes,
4 * khive_storage::MAX_BLOB_WHOLE_BYTES
);
}
#[test]
#[serial]
fn no_embeddings_ignores_additional_env_override() {
std::env::set_var("KHIVE_ADDITIONAL_EMBEDDING_MODELS", "paraphrase");
let config = RuntimeConfig::no_embeddings();
std::env::remove_var("KHIVE_ADDITIONAL_EMBEDDING_MODELS");
assert!(config.additional_embedding_models.is_empty());
assert!(configured_embedding_models(&config).is_empty());
}
#[test]
#[serial]
fn default_computes_additional_models_independently_of_no_embeddings() {
std::env::set_var("KHIVE_ADDITIONAL_EMBEDDING_MODELS", "paraphrase");
let config = RuntimeConfig::default();
let buggy_form = RuntimeConfig {
embedding_model: None,
..RuntimeConfig::default()
};
std::env::remove_var("KHIVE_ADDITIONAL_EMBEDDING_MODELS");
assert_eq!(
config.additional_embedding_models,
vec![EmbeddingModel::ParaphraseMultilingualMiniLmL12V2]
);
assert!(
!buggy_form.additional_embedding_models.is_empty(),
"Default's independent-field seeding must remain unchanged; \
no_embeddings() is the fix, not a change to Default"
);
}
#[test]
#[serial]
fn default_ships_a_single_engine_when_env_unset() {
std::env::remove_var("KHIVE_ADDITIONAL_EMBEDDING_MODELS");
let config = RuntimeConfig::default();
assert!(
config.additional_embedding_models.is_empty(),
"shipped default must register one engine; a second is opt-in via \
KHIVE_ADDITIONAL_EMBEDDING_MODELS"
);
assert_eq!(
configured_embedding_models(&config),
vec![EmbeddingModel::AllMiniLmL6V2]
);
}
}
#[cfg(test)]
mod ann_fresh_tail_config_tests {
use super::ann_fresh_tail_enabled_from_value;
#[test]
fn only_exact_zero_disables_fresh_tail() {
assert!(ann_fresh_tail_enabled_from_value(None));
assert!(!ann_fresh_tail_enabled_from_value(Some("0")));
assert!(ann_fresh_tail_enabled_from_value(Some("1")));
assert!(ann_fresh_tail_enabled_from_value(Some("false")));
assert!(ann_fresh_tail_enabled_from_value(Some(" 0")));
}
}
#[cfg(test)]
mod configured_embedding_models_order_tests {
use super::*;
#[test]
fn preserves_primary_first_then_additional_in_declared_order() {
let config = RuntimeConfig {
embedding_model: Some(EmbeddingModel::AllMiniLmL6V2),
additional_embedding_models: vec![
EmbeddingModel::Qwen3Embedding4B,
EmbeddingModel::BgeSmallEnV15,
],
..RuntimeConfig::default()
};
assert_eq!(
configured_embedding_models(&config),
vec![
EmbeddingModel::AllMiniLmL6V2,
EmbeddingModel::Qwen3Embedding4B,
EmbeddingModel::BgeSmallEnV15,
],
"order must be primary-first, then additional models as declared, \
not alphabetized"
);
}
#[test]
fn dedupes_model_shared_between_primary_and_additional() {
let config = RuntimeConfig {
embedding_model: Some(EmbeddingModel::AllMiniLmL6V2),
additional_embedding_models: vec![
EmbeddingModel::AllMiniLmL6V2,
EmbeddingModel::BgeSmallEnV15,
],
..RuntimeConfig::default()
};
assert_eq!(
configured_embedding_models(&config),
vec![EmbeddingModel::AllMiniLmL6V2, EmbeddingModel::BgeSmallEnV15],
"the shared model must appear once, in its primary position"
);
}
}