1use std::sync::{Arc, RwLock};
7
8use khive_db::StorageBackend;
9use khive_gate::{ActorRef, AllowAllGate, GateRequest};
10use khive_storage::{EntityStore, EventStore, GraphStore, NoteStore, SqlAccess};
11use khive_types::{EdgeEndpointRule, Namespace};
12use lattice_embed::{EmbeddingModel, EmbeddingService};
13
14use crate::config::{
15 build_embedder_registry, parse_embedding_model_alias, register_configured_embedding_models,
16 sanitize_key, vec_model_key,
17};
18use crate::error::RuntimeResult;
19
20pub use crate::config::{
21 parse_pack_list, runtime_config_from_khive_config, BackendId, NamespaceToken, RuntimeConfig,
22};
23
24#[derive(Clone)]
31pub struct KhiveRuntime {
32 backend: Arc<StorageBackend>,
33 config: RuntimeConfig,
34 embedder_registry: Arc<std::sync::RwLock<crate::embedder_registry::EmbedderRegistry>>,
41 default_embedder_name: Arc<str>,
42 edge_rules: Arc<RwLock<Vec<EdgeEndpointRule>>>,
46 valid_entity_kinds: Arc<RwLock<Vec<String>>>,
54 valid_note_kinds: Arc<RwLock<Vec<String>>>,
55}
56
57impl KhiveRuntime {
58 pub fn new(config: RuntimeConfig) -> RuntimeResult<Self> {
64 let backend = match &config.db_path {
65 Some(path) => {
66 if let Some(parent) = path.parent() {
67 std::fs::create_dir_all(parent).ok();
68 }
69 StorageBackend::sqlite(path)?
70 }
71 None => StorageBackend::memory()?,
72 };
73 {
80 let mut writer = backend.pool().try_writer()?;
81 khive_db::run_migrations(writer.conn_mut())?;
82 }
83 register_configured_embedding_models(&backend, &config)?;
84 let (registry, default_embedder_name) = build_embedder_registry(&config);
85 Ok(Self {
86 backend: Arc::new(backend),
87 config,
88 embedder_registry: Arc::new(std::sync::RwLock::new(registry)),
89 default_embedder_name,
90 edge_rules: Arc::new(RwLock::new(Vec::new())),
91 valid_entity_kinds: Arc::new(RwLock::new(Vec::new())),
92 valid_note_kinds: Arc::new(RwLock::new(Vec::new())),
93 })
94 }
95
96 pub fn new_readonly(config: RuntimeConfig) -> RuntimeResult<Self> {
102 let backend = match &config.db_path {
103 Some(path) => StorageBackend::sqlite(path)?,
104 None => StorageBackend::memory()?,
105 };
106 {
107 let mut writer = backend.pool().try_writer()?;
108 khive_db::run_migrations(writer.conn_mut())?;
109 }
110 let (registry, default_embedder_name) = build_embedder_registry(&config);
111 Ok(Self {
112 backend: Arc::new(backend),
113 config,
114 embedder_registry: Arc::new(std::sync::RwLock::new(registry)),
115 default_embedder_name,
116 edge_rules: Arc::new(RwLock::new(Vec::new())),
117 valid_entity_kinds: Arc::new(RwLock::new(Vec::new())),
118 valid_note_kinds: Arc::new(RwLock::new(Vec::new())),
119 })
120 }
121
122 pub fn from_backend(backend: Arc<StorageBackend>, config: RuntimeConfig) -> Self {
132 if let Err(err) = register_configured_embedding_models(&backend, &config) {
133 tracing::warn!(error = %err, "failed to register configured embedding models");
134 }
135 let (registry, default_embedder_name) = build_embedder_registry(&config);
136 Self {
137 backend,
138 config,
139 embedder_registry: Arc::new(std::sync::RwLock::new(registry)),
140 default_embedder_name,
141 edge_rules: Arc::new(RwLock::new(Vec::new())),
142 valid_entity_kinds: Arc::new(RwLock::new(Vec::new())),
143 valid_note_kinds: Arc::new(RwLock::new(Vec::new())),
144 }
145 }
146
147 pub fn memory() -> RuntimeResult<Self> {
149 Self::new(RuntimeConfig {
150 db_path: None,
151 default_namespace: Namespace::local(),
152 embedding_model: None,
153 additional_embedding_models: vec![],
154 gate: Arc::new(AllowAllGate),
155 packs: vec!["kg".to_string()],
156 backend_id: BackendId::main(),
157 brain_profile: None,
158 })
159 }
160
161 pub fn backend_id(&self) -> &BackendId {
166 &self.config.backend_id
167 }
168
169 pub fn config(&self) -> &RuntimeConfig {
171 &self.config
172 }
173
174 pub fn backend(&self) -> &StorageBackend {
176 &self.backend
177 }
178
179 pub fn entities(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn EntityStore>> {
183 Ok(self
184 .backend
185 .entities_for_namespace(token.namespace().as_str())?)
186 }
187
188 pub fn graph(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn GraphStore>> {
190 Ok(self
191 .backend
192 .graph_for_namespace(token.namespace().as_str())?)
193 }
194
195 pub fn notes(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn NoteStore>> {
197 Ok(self
198 .backend
199 .notes_for_namespace(token.namespace().as_str())?)
200 }
201
202 pub fn events(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn EventStore>> {
204 Ok(self
205 .backend
206 .events_for_namespace(token.namespace().as_str())?)
207 }
208
209 pub fn sql(&self) -> Arc<dyn SqlAccess> {
211 self.backend.sql()
212 }
213
214 pub fn vectors(
218 &self,
219 token: &NamespaceToken,
220 ) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
221 let model = self.resolve_embedding_model(None)?;
222 self.vectors_for_embedding_model(token, model)
223 }
224
225 pub fn vectors_for_model(
233 &self,
234 token: &NamespaceToken,
235 model_name: &str,
236 ) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
237 if let Some(model) = parse_embedding_model_alias(model_name) {
239 let key = model.to_string();
242 let in_registry = self
243 .embedder_registry
244 .read()
245 .map(|reg| reg.contains(&key))
246 .unwrap_or(false);
247 if in_registry {
248 return self.vectors_for_embedding_model(token, model);
249 }
250 }
251 let dims = {
254 let registry = self.embedder_registry.read().map_err(|_| {
255 crate::RuntimeError::Internal("embedder registry lock poisoned".into())
256 })?;
257 registry
258 .get_provider(model_name)
259 .map(|p| p.dimensions())
260 .ok_or_else(|| crate::RuntimeError::UnknownModel(model_name.to_string()))?
261 };
262 let model_key = sanitize_key(model_name);
263 Ok(self.backend.vectors_for_namespace(
264 &model_key,
265 model_name,
266 dims,
267 token.namespace().as_str(),
268 )?)
269 }
270
271 fn vectors_for_embedding_model(
272 &self,
273 token: &NamespaceToken,
274 model: EmbeddingModel,
275 ) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
276 Ok(self.backend.vectors_for_namespace(
277 &vec_model_key(model),
278 &model.to_string(),
279 model.dimensions(),
280 token.namespace().as_str(),
281 )?)
282 }
283
284 pub fn text(
286 &self,
287 token: &NamespaceToken,
288 ) -> RuntimeResult<Arc<dyn khive_storage::TextSearch>> {
289 let key = format!("entities_{}", sanitize_key(token.namespace().as_str()));
290 Ok(self.backend.text(&key)?)
291 }
292
293 pub fn text_for_notes(
295 &self,
296 token: &NamespaceToken,
297 ) -> RuntimeResult<Arc<dyn khive_storage::TextSearch>> {
298 let key = format!("notes_{}", sanitize_key(token.namespace().as_str()));
299 Ok(self.backend.text(&key)?)
300 }
301
302 pub fn authorize(&self, ns: Namespace) -> RuntimeResult<NamespaceToken> {
309 let actor = ActorRef::anonymous();
310 let req = GateRequest::new(
311 actor.clone(),
312 ns.clone(),
313 "authorize",
314 serde_json::Value::Null,
315 );
316 match self.config.gate.check(&req) {
317 Ok(ref decision) if decision.is_allow() => {
318 if let khive_gate::GateDecision::Allow { ref obligations } = decision {
319 if !obligations.is_empty() {
320 tracing::debug!(
321 namespace = %ns.as_str(),
322 "authorize: obligations={:?}",
323 obligations
324 );
325 }
326 }
327 Ok(NamespaceToken::mint_authorized(ns, actor))
328 }
329 Ok(khive_gate::GateDecision::Deny { reason }) => {
330 Err(crate::RuntimeError::PermissionDenied {
331 verb: "authorize".to_string(),
332 reason,
333 })
334 }
335 Ok(_) => Err(crate::RuntimeError::PermissionDenied {
336 verb: "authorize".to_string(),
337 reason: "gate denied".to_string(),
338 }),
339 Err(e) => Err(crate::RuntimeError::Internal(format!("gate error: {e}"))),
340 }
341 }
342
343 pub fn install_edge_rules(&self, rules: Vec<EdgeEndpointRule>) {
349 if let Ok(mut guard) = self.edge_rules.write() {
350 *guard = rules;
351 }
352 }
353
354 pub fn install_kind_registry(&self, entity_kinds: Vec<String>, note_kinds: Vec<String>) {
364 if let Ok(mut guard) = self.valid_entity_kinds.write() {
365 *guard = entity_kinds;
366 }
367 if let Ok(mut guard) = self.valid_note_kinds.write() {
368 *guard = note_kinds;
369 }
370 }
371
372 pub(crate) fn validate_entity_kind(&self, kind: &str) -> crate::RuntimeResult<()> {
377 let guard = self.valid_entity_kinds.read().map_err(|_| {
378 crate::RuntimeError::Internal("entity kind registry lock poisoned".into())
379 })?;
380 if guard.is_empty() {
381 return Ok(());
382 }
383 if guard.iter().any(|k| k == kind) {
384 Ok(())
385 } else {
386 Err(crate::RuntimeError::InvalidInput(format!(
387 "unknown entity kind {kind:?}; valid: {}",
388 guard.join(", ")
389 )))
390 }
391 }
392
393 pub(crate) fn validate_note_kind(&self, kind: &str) -> crate::RuntimeResult<()> {
398 let guard = self.valid_note_kinds.read().map_err(|_| {
399 crate::RuntimeError::Internal("note kind registry lock poisoned".into())
400 })?;
401 if guard.is_empty() {
402 return Ok(());
403 }
404 if guard.iter().any(|k| k == kind) {
405 Ok(())
406 } else {
407 Err(crate::RuntimeError::InvalidInput(format!(
408 "unknown note kind {kind:?}; valid: {}",
409 guard.join(", ")
410 )))
411 }
412 }
413
414 pub(crate) fn pack_edge_rules(&self) -> Vec<EdgeEndpointRule> {
416 self.edge_rules
417 .read()
418 .map(|g| g.clone())
419 .unwrap_or_default()
420 }
421
422 pub fn default_embedder_name(&self) -> &str {
424 self.default_embedder_name.as_ref()
425 }
426
427 pub fn resolve_embedding_model(&self, name: Option<&str>) -> RuntimeResult<EmbeddingModel> {
432 let model = match name {
433 Some(raw) => parse_embedding_model_alias(raw)
434 .ok_or_else(|| crate::RuntimeError::UnknownModel(raw.to_string()))?,
435 None => self
436 .config
437 .embedding_model
438 .ok_or_else(|| crate::RuntimeError::Unconfigured("embedding_model".into()))?,
439 };
440 let key = model.to_string();
441 let contains = self
442 .embedder_registry
443 .read()
444 .map(|reg| reg.contains(&key))
445 .unwrap_or(false);
446 if contains {
447 Ok(model)
448 } else {
449 Err(crate::RuntimeError::UnknownModel(
450 name.unwrap_or_else(|| self.default_embedder_name())
451 .to_string(),
452 ))
453 }
454 }
455
456 pub fn registered_embedding_model_names(&self) -> Vec<String> {
464 self.embedder_registry
465 .read()
466 .map(|reg| reg.names())
467 .unwrap_or_default()
468 }
469
470 pub async fn embedder(&self, name: &str) -> RuntimeResult<Arc<dyn EmbeddingService>> {
483 let canonical_key = match parse_embedding_model_alias(name) {
488 Some(model) => model.to_string(),
489 None => name.to_owned(),
490 };
491 let entry = {
494 let registry = self.embedder_registry.read().map_err(|_| {
495 crate::RuntimeError::Internal("embedder registry lock poisoned".into())
496 })?;
497 registry
498 .get_entry(&canonical_key)
499 .ok_or_else(|| crate::RuntimeError::UnknownModel(name.to_string()))?
500 };
501 entry.resolve().await
502 }
503
504 pub fn register_embedder(
517 &self,
518 provider: impl crate::embedder_registry::EmbedderProvider + 'static,
519 ) {
520 if let Ok(mut registry) = self.embedder_registry.write() {
521 registry.register(provider);
522 } else {
523 tracing::warn!(
524 "embedder registry lock poisoned — embedder {} not registered",
525 std::any::type_name::<dyn crate::embedder_registry::EmbedderProvider>()
526 );
527 }
528 }
529
530 pub async fn list_embedding_models(
537 &self,
538 engine_filter: Option<&str>,
539 ) -> RuntimeResult<Vec<khive_db::EmbeddingModelRegistryRecord>> {
540 use khive_storage::{SqlStatement, SqlValue};
541
542 let (sql_text, params) = if let Some(engine) = engine_filter {
543 (
544 "SELECT engine_name, model_id, key_version, dim, status, \
545 activated_at, superseded_at \
546 FROM _embedding_models WHERE engine_name = ?1 \
547 ORDER BY engine_name, activated_at IS NULL, activated_at"
548 .to_string(),
549 vec![SqlValue::Text(engine.to_string())],
550 )
551 } else {
552 (
553 "SELECT engine_name, model_id, key_version, dim, status, \
554 activated_at, superseded_at \
555 FROM _embedding_models \
556 ORDER BY engine_name, activated_at IS NULL, activated_at"
557 .to_string(),
558 vec![],
559 )
560 };
561
562 let stmt = SqlStatement {
563 sql: sql_text,
564 params,
565 label: Some("list_embedding_models".into()),
566 };
567
568 let mut reader = self
569 .sql()
570 .reader()
571 .await
572 .map_err(crate::RuntimeError::Storage)?;
573
574 let rows = match reader.query_all(stmt).await {
575 Ok(rows) => rows,
576 Err(e) if e.to_string().contains("no such table: _embedding_models") => {
577 return Ok(Vec::new())
578 }
579 Err(e) => return Err(crate::RuntimeError::Storage(e)),
580 };
581
582 let mut records = Vec::with_capacity(rows.len());
583 for row in rows {
584 macro_rules! required_text {
585 ($col:expr) => {
586 match row.get($col) {
587 Some(SqlValue::Text(s)) => s.clone(),
588 other => {
589 tracing::warn!(column = $col, value = ?other, "skipping registry row: unexpected type");
590 continue;
591 }
592 }
593 };
594 }
595 let engine_name = required_text!("engine_name");
596 let model_id = required_text!("model_id");
597 let key_version = required_text!("key_version");
598 let dimensions = match row.get("dim") {
599 Some(SqlValue::Integer(n)) => match u32::try_from(*n) {
600 Ok(d) => d,
601 Err(_) => {
602 tracing::warn!(dim = n, "skipping registry row: dim out of u32 range");
603 continue;
604 }
605 },
606 other => {
607 tracing::warn!(column = "dim", value = ?other, "skipping registry row: unexpected type");
608 continue;
609 }
610 };
611 let status = required_text!("status");
612 let activated_at = match row.get("activated_at") {
613 Some(SqlValue::Integer(n)) => Some(*n),
614 _ => None,
615 };
616 let superseded_at = match row.get("superseded_at") {
617 Some(SqlValue::Integer(n)) => Some(*n),
618 _ => None,
619 };
620 records.push(khive_db::EmbeddingModelRegistryRecord {
621 engine_name,
622 model_id,
623 key_version,
624 dimensions,
625 status,
626 activated_at,
627 superseded_at,
628 });
629 }
630
631 Ok(records)
632 }
633}
634
635#[cfg(test)]
639mod tests {
640 use super::*;
641 use khive_gate::GateRef;
642
643 #[test]
644 fn memory_runtime_creates_successfully() {
645 let rt = KhiveRuntime::memory().expect("memory runtime should create");
646 assert!(rt.config().db_path.is_none());
647 }
648
649 #[test]
650 fn file_runtime_creates_successfully() {
651 let dir = tempfile::tempdir().unwrap();
652 let path = dir.path().join("test.db");
653 let config = RuntimeConfig {
654 db_path: Some(path.clone()),
655 default_namespace: Namespace::parse("test").unwrap(),
656 embedding_model: None,
657 additional_embedding_models: vec![],
658 gate: Arc::new(AllowAllGate),
659 packs: vec!["kg".to_string()],
660 backend_id: BackendId::main(),
661 brain_profile: None,
662 };
663 let rt = KhiveRuntime::new(config).expect("file runtime should create");
664 assert!(path.exists());
665 assert_eq!(rt.config().default_namespace.as_str(), "test");
666 }
667
668 #[test]
669 fn from_backend_uses_provided_backend() {
670 let backend = Arc::new(StorageBackend::memory().expect("memory backend"));
671 let config = RuntimeConfig {
672 db_path: None,
673 default_namespace: Namespace::local(),
674 embedding_model: None,
675 additional_embedding_models: vec![],
676 gate: Arc::new(AllowAllGate),
677 packs: vec!["kg".to_string()],
678 backend_id: BackendId::new("lore"),
679 brain_profile: None,
680 };
681 let rt = KhiveRuntime::from_backend(backend, config);
682 assert_eq!(rt.backend_id().as_str(), "lore");
683 assert!(rt.config().db_path.is_none());
684 }
685
686 #[test]
687 fn backend_id_defaults_to_main() {
688 let rt = KhiveRuntime::memory().unwrap();
689 assert_eq!(rt.backend_id().as_str(), BackendId::MAIN);
690 }
691
692 #[test]
693 fn store_accessors_return_ok() {
694 let rt = KhiveRuntime::memory().unwrap();
695 let tok = NamespaceToken::local();
696 assert!(rt.entities(&tok).is_ok());
697 assert!(rt.graph(&tok).is_ok());
698 assert!(rt.notes(&tok).is_ok());
699 assert!(rt.events(&tok).is_ok());
700 }
701
702 #[test]
703 fn vectors_returns_unconfigured_without_model() {
704 let rt = KhiveRuntime::memory().unwrap();
705 let tok = NamespaceToken::local();
706 match rt.vectors(&tok) {
707 Err(crate::RuntimeError::Unconfigured(s)) => assert_eq!(s, "embedding_model"),
708 Err(other) => panic!("expected Unconfigured, got {:?}", other),
709 Ok(_) => panic!("expected Err, got Ok"),
710 }
711 }
712
713 #[test]
714 fn vec_model_key_sanitizes_dots_and_dashes() {
715 assert_eq!(
716 vec_model_key(EmbeddingModel::BgeSmallEnV15),
717 "bge_small_en_v1_5"
718 );
719 assert_eq!(
720 vec_model_key(EmbeddingModel::BgeBaseEnV15),
721 "bge_base_en_v1_5"
722 );
723 assert_eq!(
724 vec_model_key(EmbeddingModel::AllMiniLmL6V2),
725 "all_minilm_l6_v2"
726 );
727 }
728
729 #[test]
730 fn default_config_uses_allow_all_gate() {
731 let cfg = RuntimeConfig::default();
732 assert_eq!(cfg.default_namespace.as_str(), "local");
733 let _: GateRef = cfg.gate.clone();
734 }
735
736 #[test]
737 fn parse_pack_list_handles_comma_and_whitespace() {
738 assert_eq!(parse_pack_list("kg"), vec!["kg".to_string()]);
739 assert_eq!(
740 parse_pack_list("kg,gtd"),
741 vec!["kg".to_string(), "gtd".to_string()]
742 );
743 assert_eq!(
744 parse_pack_list(" kg , gtd "),
745 vec!["kg".to_string(), "gtd".to_string()]
746 );
747 assert_eq!(
748 parse_pack_list("kg gtd"),
749 vec!["kg".to_string(), "gtd".to_string()]
750 );
751 assert_eq!(parse_pack_list(",,"), Vec::<String>::new());
752 assert_eq!(parse_pack_list(""), Vec::<String>::new());
753 }
754
755 #[test]
756 fn default_config_packs_loads_all_production_packs() {
757 let prior = std::env::var("KHIVE_PACKS").ok();
758 unsafe {
760 std::env::remove_var("KHIVE_PACKS");
761 }
762 let cfg = RuntimeConfig::default();
763 assert!(cfg.packs.contains(&"kg".to_string()));
764 assert!(cfg.packs.contains(&"gtd".to_string()));
765 assert!(cfg.packs.contains(&"memory".to_string()));
766 assert!(cfg.packs.contains(&"brain".to_string()));
767 assert!(cfg.packs.contains(&"comm".to_string()));
768 assert!(cfg.packs.contains(&"schedule".to_string()));
769 assert!(cfg.packs.contains(&"knowledge".to_string()));
770 assert_eq!(cfg.packs.len(), 7);
771 if let Some(v) = prior {
772 unsafe {
774 std::env::set_var("KHIVE_PACKS", v);
775 }
776 }
777 }
778
779 #[test]
780 fn default_config_uses_minilm_when_env_unset() {
781 let prior = std::env::var("KHIVE_EMBEDDING_MODEL").ok();
782 unsafe {
785 std::env::remove_var("KHIVE_EMBEDDING_MODEL");
786 }
787 let cfg = RuntimeConfig::default();
788 assert_eq!(cfg.embedding_model, Some(EmbeddingModel::AllMiniLmL6V2));
789 if let Some(v) = prior {
790 unsafe {
792 std::env::set_var("KHIVE_EMBEDDING_MODEL", v);
793 }
794 }
795 }
796
797 use crate::engine_config::{ActorConfig, KhiveConfig, RuntimeSectionConfig};
800
801 fn khive_cfg_with_actor(id: &str) -> KhiveConfig {
802 KhiveConfig {
803 engines: vec![],
804 actor: ActorConfig {
805 id: Some(id.to_string()),
806 display_name: None,
807 },
808 runtime: RuntimeSectionConfig::default(),
809 }
810 }
811
812 #[test]
813 fn runtime_config_from_khive_config_applies_actor_id_as_default_namespace() {
814 let base = RuntimeConfig {
815 db_path: None,
816 default_namespace: Namespace::local(),
817 embedding_model: None,
818 additional_embedding_models: vec![],
819 gate: Arc::new(AllowAllGate),
820 packs: vec!["kg".to_string()],
821 backend_id: BackendId::main(),
822 brain_profile: None,
823 };
824 let cfg = khive_cfg_with_actor("lambda:khive");
825 let result = runtime_config_from_khive_config(&cfg, base);
826 assert_eq!(result.default_namespace.as_str(), "lambda:khive");
827 }
828
829 #[test]
830 fn runtime_config_from_khive_config_empty_actor_id_keeps_base_namespace() {
831 let base = RuntimeConfig {
832 db_path: None,
833 default_namespace: Namespace::parse("lambda:base").unwrap(),
834 embedding_model: None,
835 additional_embedding_models: vec![],
836 gate: Arc::new(AllowAllGate),
837 packs: vec!["kg".to_string()],
838 backend_id: BackendId::main(),
839 brain_profile: None,
840 };
841 let cfg = KhiveConfig {
842 engines: vec![],
843 actor: ActorConfig {
844 id: Some(String::new()),
845 display_name: None,
846 },
847 runtime: RuntimeSectionConfig::default(),
848 };
849 let result = runtime_config_from_khive_config(&cfg, base);
850 assert_eq!(
851 result.default_namespace.as_str(),
852 "lambda:base",
853 "empty actor.id must not override base namespace"
854 );
855 }
856
857 #[test]
858 fn runtime_config_from_khive_config_absent_actor_id_keeps_base_namespace() {
859 let base = RuntimeConfig {
860 db_path: None,
861 default_namespace: Namespace::parse("lambda:base").unwrap(),
862 embedding_model: None,
863 additional_embedding_models: vec![],
864 gate: Arc::new(AllowAllGate),
865 packs: vec!["kg".to_string()],
866 backend_id: BackendId::main(),
867 brain_profile: None,
868 };
869 let cfg = KhiveConfig::default(); let result = runtime_config_from_khive_config(&cfg, base);
871 assert_eq!(
872 result.default_namespace.as_str(),
873 "lambda:base",
874 "absent actor.id must not override base namespace"
875 );
876 }
877
878 #[test]
879 fn runtime_config_from_khive_config_actor_id_with_engines() {
880 let base = RuntimeConfig {
881 db_path: None,
882 default_namespace: Namespace::local(),
883 embedding_model: None,
884 additional_embedding_models: vec![],
885 gate: Arc::new(AllowAllGate),
886 packs: vec!["kg".to_string()],
887 backend_id: BackendId::main(),
888 brain_profile: None,
889 };
890 let cfg = KhiveConfig {
891 engines: vec![crate::engine_config::EngineConfig {
892 name: "default".to_string(),
893 model: "all-minilm-l6-v2".to_string(),
894 default: true,
895 fusion_weight: None,
896 dims: None,
897 }],
898 actor: ActorConfig {
899 id: Some("lambda:test".to_string()),
900 display_name: None,
901 },
902 runtime: RuntimeSectionConfig::default(),
903 };
904 let result = runtime_config_from_khive_config(&cfg, base);
905 assert_eq!(result.default_namespace.as_str(), "lambda:test");
906 assert!(result.embedding_model.is_some());
907 }
908
909 #[tokio::test]
912 async fn list_embedding_models_returns_empty_when_table_absent() {
913 let rt = KhiveRuntime::memory().expect("memory runtime");
916 let records = rt
917 .list_embedding_models(None)
918 .await
919 .expect("list ok on empty table");
920 assert!(records.is_empty());
921 }
922
923 #[tokio::test]
924 async fn list_embedding_models_returns_row_after_insert() {
925 use khive_storage::{SqlStatement, SqlValue};
926
927 let rt = KhiveRuntime::memory().expect("memory runtime");
928 let sql = rt.sql();
929
930 let now = 1_000_000i64;
931 let id = uuid::Uuid::new_v4();
932 let canonical_key = b"test_engine:test-model-v1:v1:384".to_vec();
933
934 let mut writer = sql.writer().await.expect("writer");
935 writer
936 .execute(SqlStatement {
937 sql: "INSERT INTO _embedding_models \
938 (id, engine_name, model_id, key_version, dim, output_dim, status, \
939 activated_at, superseded_at, superseded_by, canonical_key, created_at) \
940 VALUES (?1, ?2, ?3, ?4, ?5, NULL, ?6, ?7, NULL, NULL, ?8, ?9)"
941 .into(),
942 params: vec![
943 SqlValue::Blob(id.as_bytes().to_vec()),
944 SqlValue::Text("test_engine".into()),
945 SqlValue::Text("test-model-v1".into()),
946 SqlValue::Text("v1".into()),
947 SqlValue::Integer(384),
948 SqlValue::Text("active".into()),
949 SqlValue::Integer(now),
950 SqlValue::Blob(canonical_key),
951 SqlValue::Integer(now),
952 ],
953 label: None,
954 })
955 .await
956 .expect("insert row");
957 drop(writer);
958
959 let records = rt.list_embedding_models(None).await.expect("list ok");
960 assert_eq!(records.len(), 1);
961 assert_eq!(records[0].engine_name, "test_engine");
962 assert_eq!(records[0].model_id, "test-model-v1");
963 assert_eq!(records[0].key_version, "v1");
964 assert_eq!(records[0].dimensions, 384);
965 assert_eq!(records[0].status, "active");
966
967 let filtered = rt
969 .list_embedding_models(Some("test_engine"))
970 .await
971 .expect("filter ok");
972 assert_eq!(filtered.len(), 1);
973
974 let no_match = rt
976 .list_embedding_models(Some("other_engine"))
977 .await
978 .expect("no-match ok");
979 assert!(no_match.is_empty());
980 }
981}