1use std::sync::{Arc, RwLock};
7
8use khive_db::StorageBackend;
9#[cfg(test)]
10use khive_gate::AllowAllGate;
11use khive_gate::GateRequest;
12use khive_storage::{EntityStore, EventStore, GraphStore, NoteStore, SqlAccess};
13use khive_types::{EdgeEndpointRule, Namespace};
14use lattice_embed::{EmbeddingModel, EmbeddingService};
15
16use crate::config::{
17 build_embedder_registry, parse_embedding_model_alias, register_configured_embedding_models,
18 sanitize_key, vec_model_key,
19};
20use crate::error::{RuntimeError, RuntimeResult};
21
22pub type EntityTypeValidatorFn =
28 Arc<dyn Fn(&str, Option<&str>) -> Result<Option<String>, RuntimeError> + Send + Sync>;
29
30pub type NoteMutationHookFn = Arc<
41 dyn Fn(String, uuid::Uuid) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send>>
42 + Send
43 + Sync,
44>;
45
46pub use crate::config::{
47 assert_captured_db_anchor_consistent, assert_db_anchor_consistent, parse_pack_list,
48 resolve_db_anchor, resolve_project_actor_id, runtime_config_from_khive_config, BackendId,
49 NamespaceToken, RuntimeConfig,
50};
51
52#[derive(Clone)]
59pub struct KhiveRuntime {
60 backend: Arc<StorageBackend>,
61 core_backend: Option<Arc<StorageBackend>>,
65 config: RuntimeConfig,
66 embedder_registry: Arc<std::sync::RwLock<crate::embedder_registry::EmbedderRegistry>>,
73 default_embedder_name: Arc<str>,
74 edge_rules: Arc<RwLock<Vec<EdgeEndpointRule>>>,
78 valid_entity_kinds: Arc<RwLock<Vec<String>>>,
86 valid_note_kinds: Arc<RwLock<Vec<String>>>,
87 entity_type_validator: Arc<RwLock<Option<EntityTypeValidatorFn>>>,
94 note_mutation_hook: Arc<RwLock<Option<NoteMutationHookFn>>>,
106 blob_store: Arc<RwLock<Option<Arc<dyn khive_storage::BlobStore>>>>,
115}
116
117impl KhiveRuntime {
118 pub fn new(config: RuntimeConfig) -> RuntimeResult<Self> {
124 let backend = match &config.db_path {
125 Some(path) => {
126 if let Some(parent) = path.parent() {
127 std::fs::create_dir_all(parent).ok();
128 }
129 StorageBackend::sqlite(path)?
130 }
131 None => StorageBackend::memory()?,
132 };
133 {
136 let mut writer = backend.pool().try_writer()?;
137 khive_db::run_migrations(writer.conn_mut())?;
138 }
139 register_configured_embedding_models(&backend, &config)?;
140 let (registry, default_embedder_name) = build_embedder_registry(&config);
141 Ok(Self {
142 backend: Arc::new(backend),
143 core_backend: None,
144 config,
145 embedder_registry: Arc::new(std::sync::RwLock::new(registry)),
146 default_embedder_name,
147 edge_rules: Arc::new(RwLock::new(Vec::new())),
148 valid_entity_kinds: Arc::new(RwLock::new(Vec::new())),
149 valid_note_kinds: Arc::new(RwLock::new(Vec::new())),
150 entity_type_validator: Arc::new(RwLock::new(None)),
151 note_mutation_hook: Arc::new(RwLock::new(None)),
152 blob_store: Arc::new(RwLock::new(None)),
153 })
154 }
155
156 pub fn new_readonly(config: RuntimeConfig) -> RuntimeResult<Self> {
162 let backend = match &config.db_path {
163 Some(path) => StorageBackend::sqlite(path)?,
164 None => StorageBackend::memory()?,
165 };
166 {
167 let mut writer = backend.pool().try_writer()?;
168 khive_db::run_migrations(writer.conn_mut())?;
169 }
170 let (registry, default_embedder_name) = build_embedder_registry(&config);
171 Ok(Self {
172 backend: Arc::new(backend),
173 core_backend: None,
174 config,
175 embedder_registry: Arc::new(std::sync::RwLock::new(registry)),
176 default_embedder_name,
177 edge_rules: Arc::new(RwLock::new(Vec::new())),
178 valid_entity_kinds: Arc::new(RwLock::new(Vec::new())),
179 valid_note_kinds: Arc::new(RwLock::new(Vec::new())),
180 entity_type_validator: Arc::new(RwLock::new(None)),
181 note_mutation_hook: Arc::new(RwLock::new(None)),
182 blob_store: Arc::new(RwLock::new(None)),
183 })
184 }
185
186 pub fn from_backend(backend: Arc<StorageBackend>, config: RuntimeConfig) -> Self {
196 if let Err(err) = register_configured_embedding_models(&backend, &config) {
197 tracing::warn!(error = %err, "failed to register configured embedding models");
198 }
199 let (registry, default_embedder_name) = build_embedder_registry(&config);
200 Self {
201 backend,
202 core_backend: None,
203 config,
204 embedder_registry: Arc::new(std::sync::RwLock::new(registry)),
205 default_embedder_name,
206 edge_rules: Arc::new(RwLock::new(Vec::new())),
207 valid_entity_kinds: Arc::new(RwLock::new(Vec::new())),
208 valid_note_kinds: Arc::new(RwLock::new(Vec::new())),
209 entity_type_validator: Arc::new(RwLock::new(None)),
210 note_mutation_hook: Arc::new(RwLock::new(None)),
211 blob_store: Arc::new(RwLock::new(None)),
212 }
213 }
214
215 pub fn with_core_backend(mut self, core: Arc<StorageBackend>) -> Self {
224 debug_assert_ne!(
225 self.config.backend_id.as_str(),
226 BackendId::MAIN,
227 "with_core_backend must not be called on the main runtime"
228 );
229 self.core_backend = Some(core);
230 self
231 }
232
233 pub fn core(&self) -> KhiveRuntime {
253 match &self.core_backend {
254 None => self.clone(),
255 Some(main_arc) => {
256 let mut core_config = self.config.clone();
257 core_config.backend_id = BackendId::main();
258 KhiveRuntime {
259 backend: main_arc.clone(),
260 core_backend: None,
261 config: core_config,
262 embedder_registry: self.embedder_registry.clone(),
263 default_embedder_name: self.default_embedder_name.clone(),
264 edge_rules: self.edge_rules.clone(),
265 valid_entity_kinds: self.valid_entity_kinds.clone(),
266 valid_note_kinds: self.valid_note_kinds.clone(),
267 entity_type_validator: self.entity_type_validator.clone(),
268 note_mutation_hook: self.note_mutation_hook.clone(),
269 blob_store: self.blob_store.clone(),
270 }
271 }
272 }
273 }
274
275 pub fn memory() -> RuntimeResult<Self> {
277 Self::new(RuntimeConfig {
278 db_path: None,
279 packs: vec!["kg".to_string()],
280 brain_profile: None,
281 actor_id: None,
282 ..RuntimeConfig::no_embeddings()
283 })
284 }
285
286 pub fn backend_id(&self) -> &BackendId {
291 &self.config.backend_id
292 }
293
294 pub fn visible_namespaces(&self) -> &[Namespace] {
301 &self.config.visible_namespaces
302 }
303
304 pub fn config(&self) -> &RuntimeConfig {
306 &self.config
307 }
308
309 pub fn backend(&self) -> &StorageBackend {
311 &self.backend
312 }
313
314 pub fn backend_data_dir(&self) -> Option<std::path::PathBuf> {
317 self.backend.data_dir()
318 }
319
320 pub fn entities(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn EntityStore>> {
324 Ok(self
325 .backend
326 .entities_for_namespace(token.namespace().as_str())?)
327 }
328
329 pub fn graph(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn GraphStore>> {
331 Ok(self
332 .backend
333 .graph_for_namespace(token.namespace().as_str())?)
334 }
335
336 pub fn notes(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn NoteStore>> {
338 Ok(self
339 .backend
340 .notes_for_namespace(token.namespace().as_str())?)
341 }
342
343 pub fn events(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn EventStore>> {
345 Ok(self
346 .backend
347 .events_for_namespace(token.namespace().as_str())?)
348 }
349
350 pub fn sql(&self) -> Arc<dyn SqlAccess> {
352 self.backend.sql()
353 }
354
355 pub fn vectors(
359 &self,
360 token: &NamespaceToken,
361 ) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
362 let model = self.resolve_embedding_model(None)?;
363 self.vectors_for_embedding_model(token, model)
364 }
365
366 pub fn vectors_for_model(
374 &self,
375 token: &NamespaceToken,
376 model_name: &str,
377 ) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
378 if let Some(model) = parse_embedding_model_alias(model_name) {
379 let key = model.to_string();
382 let in_registry = self
383 .embedder_registry
384 .read()
385 .map(|reg| reg.contains(&key))
386 .unwrap_or(false);
387 if in_registry {
388 return self.vectors_for_embedding_model(token, model);
389 }
390 }
391 let dims = {
392 let registry = self.embedder_registry.read().map_err(|_| {
393 crate::RuntimeError::Internal("embedder registry lock poisoned".into())
394 })?;
395 registry
396 .get_provider(model_name)
397 .map(|p| p.dimensions())
398 .ok_or_else(|| crate::RuntimeError::UnknownModel(model_name.to_string()))?
399 };
400 let model_key = sanitize_key(model_name);
401 Ok(self.backend.vectors_for_namespace(
402 &model_key,
403 model_name,
404 dims,
405 token.namespace().as_str(),
406 )?)
407 }
408
409 fn vectors_for_embedding_model(
410 &self,
411 token: &NamespaceToken,
412 model: EmbeddingModel,
413 ) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
414 Ok(self.backend.vectors_for_namespace(
415 &vec_model_key(model),
416 &model.to_string(),
417 model.dimensions(),
418 token.namespace().as_str(),
419 )?)
420 }
421
422 pub fn text(
424 &self,
425 token: &NamespaceToken,
426 ) -> RuntimeResult<Arc<dyn khive_storage::TextSearch>> {
427 let _ = token;
428 Ok(self.backend.text("entities")?)
429 }
430
431 pub fn text_for_notes(
433 &self,
434 token: &NamespaceToken,
435 ) -> RuntimeResult<Arc<dyn khive_storage::TextSearch>> {
436 let _ = token;
437 Ok(self.backend.text("notes")?)
438 }
439
440 pub fn authorize(&self, ns: Namespace) -> RuntimeResult<NamespaceToken> {
456 let actor = crate::actor_identity::resolve_actor(self.config.actor_id.as_deref());
457 let req = GateRequest::new(
458 actor.clone(),
459 ns.clone(),
460 "authorize",
461 serde_json::Value::Null,
462 );
463 match self.config.gate.check(&req) {
464 Ok(ref decision) if decision.is_allow() => {
465 if let khive_gate::GateDecision::Allow { ref obligations } = decision {
466 if !obligations.is_empty() {
467 tracing::debug!(
468 namespace = %ns.as_str(),
469 "authorize: obligations={:?}",
470 obligations
471 );
472 }
473 }
474 Ok(NamespaceToken::mint_authorized(ns, actor))
475 }
476 Ok(khive_gate::GateDecision::Deny { reason }) => {
477 Err(crate::RuntimeError::PermissionDenied {
478 verb: "authorize".to_string(),
479 reason,
480 })
481 }
482 Ok(_) => Err(crate::RuntimeError::PermissionDenied {
483 verb: "authorize".to_string(),
484 reason: "gate denied".to_string(),
485 }),
486 Err(e) => Err(crate::RuntimeError::Internal(format!("gate error: {e}"))),
487 }
488 }
489
490 pub fn authorize_with_visibility(
505 &self,
506 primary: Namespace,
507 extra_visible: Vec<Namespace>,
508 ) -> RuntimeResult<NamespaceToken> {
509 let actor = crate::actor_identity::resolve_actor(self.config.actor_id.as_deref());
510 let req = GateRequest::new(
511 actor.clone(),
512 primary.clone(),
513 "authorize",
514 serde_json::Value::Null,
515 );
516 match self.config.gate.check(&req) {
517 Ok(ref decision) if decision.is_allow() => {
518 if let khive_gate::GateDecision::Allow { ref obligations } = decision {
519 if !obligations.is_empty() {
520 tracing::debug!(
521 namespace = %primary.as_str(),
522 "authorize_with_visibility: obligations={:?}",
523 obligations
524 );
525 }
526 }
527 Ok(NamespaceToken::mint_with_visibility(
528 primary,
529 extra_visible,
530 actor,
531 ))
532 }
533 Ok(khive_gate::GateDecision::Deny { reason }) => {
534 Err(crate::RuntimeError::PermissionDenied {
535 verb: "authorize".to_string(),
536 reason,
537 })
538 }
539 Ok(_) => Err(crate::RuntimeError::PermissionDenied {
540 verb: "authorize".to_string(),
541 reason: "gate denied".to_string(),
542 }),
543 Err(e) => Err(crate::RuntimeError::Internal(format!("gate error: {e}"))),
544 }
545 }
546
547 pub fn install_edge_rules(&self, rules: Vec<EdgeEndpointRule>) {
553 if let Ok(mut guard) = self.edge_rules.write() {
554 *guard = rules;
555 }
556 }
557
558 pub fn install_blob_store(&self, store: Arc<dyn khive_storage::BlobStore>) {
566 if let Ok(mut guard) = self.blob_store.write() {
567 *guard = Some(store);
568 }
569 }
570
571 pub fn blob_store(&self) -> Option<Arc<dyn khive_storage::BlobStore>> {
576 self.blob_store.read().ok().and_then(|guard| guard.clone())
577 }
578
579 pub fn install_kind_registry(&self, entity_kinds: Vec<String>, note_kinds: Vec<String>) {
589 if let Ok(mut guard) = self.valid_entity_kinds.write() {
590 *guard = entity_kinds;
591 }
592 if let Ok(mut guard) = self.valid_note_kinds.write() {
593 *guard = note_kinds;
594 }
595 }
596
597 pub(crate) fn validate_entity_kind(&self, kind: &str) -> crate::RuntimeResult<()> {
602 let guard = self.valid_entity_kinds.read().map_err(|_| {
603 crate::RuntimeError::Internal("entity kind registry lock poisoned".into())
604 })?;
605 if guard.is_empty() {
606 return Ok(());
607 }
608 if guard.iter().any(|k| k == kind) {
609 Ok(())
610 } else {
611 Err(crate::RuntimeError::InvalidInput(format!(
612 "unknown entity kind {kind:?}; valid: {}",
613 guard.join(", ")
614 )))
615 }
616 }
617
618 pub(crate) fn validate_note_kind(&self, kind: &str) -> crate::RuntimeResult<()> {
623 let guard = self.valid_note_kinds.read().map_err(|_| {
624 crate::RuntimeError::Internal("note kind registry lock poisoned".into())
625 })?;
626 if guard.is_empty() {
627 return Ok(());
628 }
629 if guard.iter().any(|k| k == kind) {
630 Ok(())
631 } else {
632 Err(crate::RuntimeError::InvalidInput(format!(
633 "unknown note kind {kind:?}; valid: {}",
634 guard.join(", ")
635 )))
636 }
637 }
638
639 pub fn install_entity_type_validator(&self, f: EntityTypeValidatorFn) {
649 if let Ok(mut guard) = self.entity_type_validator.write() {
650 *guard = Some(f);
651 }
652 }
653
654 pub(crate) fn validate_entity_type_for_kind(
659 &self,
660 kind: &str,
661 entity_type: Option<&str>,
662 ) -> crate::RuntimeResult<Option<String>> {
663 let guard = self.entity_type_validator.read().map_err(|_| {
664 crate::RuntimeError::Internal("entity type validator lock poisoned".into())
665 })?;
666 match guard.as_ref() {
667 None => Ok(entity_type.map(str::to_string)),
668 Some(validate) => validate(kind, entity_type),
669 }
670 }
671
672 pub fn install_note_mutation_hook(&self, f: NoteMutationHookFn) {
680 if let Ok(mut guard) = self.note_mutation_hook.write() {
681 *guard = Some(f);
682 }
683 }
684
685 pub(crate) async fn fire_note_mutation_hook(&self, kind: &str, id: uuid::Uuid) {
694 let hook = self
695 .note_mutation_hook
696 .read()
697 .ok()
698 .and_then(|guard| guard.clone());
699 if let Some(hook) = hook {
700 hook(kind.to_string(), id).await;
701 }
702 }
703
704 pub fn pack_edge_rules(&self) -> Vec<EdgeEndpointRule> {
713 self.edge_rules
714 .read()
715 .map(|g| g.clone())
716 .unwrap_or_default()
717 }
718
719 pub fn default_embedder_name(&self) -> &str {
721 self.default_embedder_name.as_ref()
722 }
723
724 pub fn resolve_embedding_model(&self, name: Option<&str>) -> RuntimeResult<EmbeddingModel> {
729 let model = match name {
730 Some(raw) => parse_embedding_model_alias(raw)
731 .ok_or_else(|| crate::RuntimeError::UnknownModel(raw.to_string()))?,
732 None => self
733 .config
734 .embedding_model
735 .ok_or_else(|| crate::RuntimeError::Unconfigured("embedding_model".into()))?,
736 };
737 let key = model.to_string();
738 let contains = self
739 .embedder_registry
740 .read()
741 .map(|reg| reg.contains(&key))
742 .unwrap_or(false);
743 if contains {
744 Ok(model)
745 } else {
746 Err(crate::RuntimeError::UnknownModel(
747 name.unwrap_or_else(|| self.default_embedder_name())
748 .to_string(),
749 ))
750 }
751 }
752
753 pub fn registered_embedding_model_names(&self) -> Vec<String> {
760 self.embedder_registry
761 .read()
762 .map(|reg| reg.names())
763 .unwrap_or_default()
764 }
765
766 pub async fn embedder(&self, name: &str) -> RuntimeResult<Arc<dyn EmbeddingService>> {
779 let canonical_key = match parse_embedding_model_alias(name) {
782 Some(model) => model.to_string(),
783 None => name.to_owned(),
784 };
785 let entry = {
788 let registry = self.embedder_registry.read().map_err(|_| {
789 crate::RuntimeError::Internal("embedder registry lock poisoned".into())
790 })?;
791 registry
792 .get_entry(&canonical_key)
793 .ok_or_else(|| crate::RuntimeError::UnknownModel(name.to_string()))?
794 };
795 entry.resolve().await
796 }
797
798 pub fn register_embedder(
811 &self,
812 provider: impl crate::embedder_registry::EmbedderProvider + 'static,
813 ) {
814 if let Ok(mut registry) = self.embedder_registry.write() {
815 registry.register(provider);
816 } else {
817 tracing::warn!(
818 "embedder registry lock poisoned — embedder {} not registered",
819 std::any::type_name::<dyn crate::embedder_registry::EmbedderProvider>()
820 );
821 }
822 }
823
824 pub async fn list_embedding_models(
831 &self,
832 engine_filter: Option<&str>,
833 ) -> RuntimeResult<Vec<khive_db::EmbeddingModelRegistryRecord>> {
834 use khive_storage::{SqlStatement, SqlValue};
835
836 let (sql_text, params) = if let Some(engine) = engine_filter {
837 (
838 "SELECT engine_name, model_id, key_version, dim, status, \
839 activated_at, superseded_at \
840 FROM _embedding_models WHERE engine_name = ?1 \
841 ORDER BY engine_name, activated_at IS NULL, activated_at"
842 .to_string(),
843 vec![SqlValue::Text(engine.to_string())],
844 )
845 } else {
846 (
847 "SELECT engine_name, model_id, key_version, dim, status, \
848 activated_at, superseded_at \
849 FROM _embedding_models \
850 ORDER BY engine_name, activated_at IS NULL, activated_at"
851 .to_string(),
852 vec![],
853 )
854 };
855
856 let stmt = SqlStatement {
857 sql: sql_text,
858 params,
859 label: Some("list_embedding_models".into()),
860 };
861
862 let mut reader = self
863 .sql()
864 .reader()
865 .await
866 .map_err(crate::RuntimeError::Storage)?;
867
868 let rows = match reader.query_all(stmt).await {
869 Ok(rows) => rows,
870 Err(e) if e.to_string().contains("no such table: _embedding_models") => {
871 return Ok(Vec::new())
872 }
873 Err(e) => return Err(crate::RuntimeError::Storage(e)),
874 };
875
876 let mut records = Vec::with_capacity(rows.len());
877 for row in rows {
878 macro_rules! required_text {
879 ($col:expr) => {
880 match row.get($col) {
881 Some(SqlValue::Text(s)) => s.clone(),
882 other => {
883 tracing::warn!(column = $col, value = ?other, "skipping registry row: unexpected type");
884 continue;
885 }
886 }
887 };
888 }
889 let engine_name = required_text!("engine_name");
890 let model_id = required_text!("model_id");
891 let key_version = required_text!("key_version");
892 let dimensions = match row.get("dim") {
893 Some(SqlValue::Integer(n)) => match u32::try_from(*n) {
894 Ok(d) => d,
895 Err(_) => {
896 tracing::warn!(dim = n, "skipping registry row: dim out of u32 range");
897 continue;
898 }
899 },
900 other => {
901 tracing::warn!(column = "dim", value = ?other, "skipping registry row: unexpected type");
902 continue;
903 }
904 };
905 let status = required_text!("status");
906 let activated_at = match row.get("activated_at") {
907 Some(SqlValue::Integer(n)) => Some(*n),
908 _ => None,
909 };
910 let superseded_at = match row.get("superseded_at") {
911 Some(SqlValue::Integer(n)) => Some(*n),
912 _ => None,
913 };
914 records.push(khive_db::EmbeddingModelRegistryRecord {
915 engine_name,
916 model_id,
917 key_version,
918 dimensions,
919 status,
920 activated_at,
921 superseded_at,
922 });
923 }
924
925 Ok(records)
926 }
927}
928
929#[cfg(test)]
933mod tests {
934 use super::*;
935 use khive_gate::GateRef;
936 use serial_test::serial;
937
938 #[test]
939 fn memory_runtime_creates_successfully() {
940 let rt = KhiveRuntime::memory().expect("memory runtime should create");
941 assert!(rt.config().db_path.is_none());
942 }
943
944 #[test]
945 fn backend_data_dir_returns_none_for_memory_backend() {
946 let rt = KhiveRuntime::memory().expect("memory runtime");
947 assert!(rt.backend_data_dir().is_none());
948 }
949
950 #[test]
951 fn backend_data_dir_returns_parent_dir_for_file_backend() {
952 let dir = tempfile::tempdir().unwrap();
953 let path = dir.path().join("test.db");
954 let config = RuntimeConfig {
955 git_write: Default::default(),
956 db_path: Some(path),
957 default_namespace: Namespace::local(),
958 embedding_model: None,
959 additional_embedding_models: vec![],
960 gate: Arc::new(AllowAllGate),
961 packs: vec!["kg".to_string()],
962 backend_id: BackendId::main(),
963 brain_profile: None,
964 visible_namespaces: vec![],
965 allowed_outbound_namespaces: vec![],
966 actor_id: None,
967 };
968 let rt = KhiveRuntime::new(config).expect("file runtime");
969 let data_dir = rt
970 .backend_data_dir()
971 .expect("file backend must return Some");
972 assert_eq!(data_dir, dir.path());
973 }
974
975 #[test]
976 fn backend_data_dir_returns_none_for_from_backend_with_memory() {
977 let backend = Arc::new(StorageBackend::memory().expect("memory backend"));
978 let config = RuntimeConfig {
979 git_write: Default::default(),
980 db_path: None,
981 default_namespace: Namespace::local(),
982 embedding_model: None,
983 additional_embedding_models: vec![],
984 gate: Arc::new(AllowAllGate),
985 packs: vec!["kg".to_string()],
986 backend_id: BackendId::main(),
987 brain_profile: None,
988 visible_namespaces: vec![],
989 allowed_outbound_namespaces: vec![],
990 actor_id: None,
991 };
992 let rt = KhiveRuntime::from_backend(backend, config);
993 assert!(rt.backend_data_dir().is_none());
994 }
995
996 #[test]
997 fn file_runtime_creates_successfully() {
998 let dir = tempfile::tempdir().unwrap();
999 let path = dir.path().join("test.db");
1000 let config = RuntimeConfig {
1001 git_write: Default::default(),
1002 db_path: Some(path.clone()),
1003 default_namespace: Namespace::parse("test").unwrap(),
1004 embedding_model: None,
1005 additional_embedding_models: vec![],
1006 gate: Arc::new(AllowAllGate),
1007 packs: vec!["kg".to_string()],
1008 backend_id: BackendId::main(),
1009 brain_profile: None,
1010 visible_namespaces: vec![],
1011 allowed_outbound_namespaces: vec![],
1012 actor_id: None,
1013 };
1014 let rt = KhiveRuntime::new(config).expect("file runtime should create");
1015 assert!(path.exists());
1016 assert_eq!(rt.config().default_namespace.as_str(), "test");
1017 }
1018
1019 #[test]
1020 fn from_backend_uses_provided_backend() {
1021 let backend = Arc::new(StorageBackend::memory().expect("memory backend"));
1022 let config = RuntimeConfig {
1023 git_write: Default::default(),
1024 db_path: None,
1025 default_namespace: Namespace::local(),
1026 embedding_model: None,
1027 additional_embedding_models: vec![],
1028 gate: Arc::new(AllowAllGate),
1029 packs: vec!["kg".to_string()],
1030 backend_id: BackendId::new("lore"),
1031 brain_profile: None,
1032 visible_namespaces: vec![],
1033 allowed_outbound_namespaces: vec![],
1034 actor_id: None,
1035 };
1036 let rt = KhiveRuntime::from_backend(backend, config);
1037 assert_eq!(rt.backend_id().as_str(), "lore");
1038 assert!(rt.config().db_path.is_none());
1039 }
1040
1041 #[test]
1042 fn backend_id_defaults_to_main() {
1043 let rt = KhiveRuntime::memory().unwrap();
1044 assert_eq!(rt.backend_id().as_str(), BackendId::MAIN);
1045 }
1046
1047 #[test]
1048 fn store_accessors_return_ok() {
1049 let rt = KhiveRuntime::memory().unwrap();
1050 let tok = NamespaceToken::local();
1051 assert!(rt.entities(&tok).is_ok());
1052 assert!(rt.graph(&tok).is_ok());
1053 assert!(rt.notes(&tok).is_ok());
1054 assert!(rt.events(&tok).is_ok());
1055 }
1056
1057 #[test]
1058 fn vectors_returns_unconfigured_without_model() {
1059 let rt = KhiveRuntime::memory().unwrap();
1060 let tok = NamespaceToken::local();
1061 match rt.vectors(&tok) {
1062 Err(crate::RuntimeError::Unconfigured(s)) => assert_eq!(s, "embedding_model"),
1063 Err(other) => panic!("expected Unconfigured, got {:?}", other),
1064 Ok(_) => panic!("expected Err, got Ok"),
1065 }
1066 }
1067
1068 #[test]
1069 fn vec_model_key_sanitizes_dots_and_dashes() {
1070 assert_eq!(
1071 vec_model_key(EmbeddingModel::BgeSmallEnV15),
1072 "bge_small_en_v1_5"
1073 );
1074 assert_eq!(
1075 vec_model_key(EmbeddingModel::BgeBaseEnV15),
1076 "bge_base_en_v1_5"
1077 );
1078 assert_eq!(
1079 vec_model_key(EmbeddingModel::AllMiniLmL6V2),
1080 "all_minilm_l6_v2"
1081 );
1082 }
1083
1084 #[test]
1085 fn default_config_uses_allow_all_gate() {
1086 let cfg = RuntimeConfig::default();
1087 assert_eq!(cfg.default_namespace.as_str(), "local");
1088 let _: GateRef = cfg.gate.clone();
1089 }
1090
1091 #[test]
1092 fn parse_pack_list_handles_comma_and_whitespace() {
1093 assert_eq!(parse_pack_list("kg"), vec!["kg".to_string()]);
1094 assert_eq!(
1095 parse_pack_list("kg,gtd"),
1096 vec!["kg".to_string(), "gtd".to_string()]
1097 );
1098 assert_eq!(
1099 parse_pack_list(" kg , gtd "),
1100 vec!["kg".to_string(), "gtd".to_string()]
1101 );
1102 assert_eq!(
1103 parse_pack_list("kg gtd"),
1104 vec!["kg".to_string(), "gtd".to_string()]
1105 );
1106 assert_eq!(parse_pack_list(",,"), Vec::<String>::new());
1107 assert_eq!(parse_pack_list(""), Vec::<String>::new());
1108 }
1109
1110 #[test]
1111 fn default_config_packs_loads_all_production_packs() {
1112 let prior = std::env::var("KHIVE_PACKS").ok();
1113 unsafe {
1115 std::env::remove_var("KHIVE_PACKS");
1116 }
1117 let cfg = RuntimeConfig::default();
1118 assert!(cfg.packs.contains(&"kg".to_string()));
1119 assert!(cfg.packs.contains(&"gtd".to_string()));
1120 assert!(cfg.packs.contains(&"memory".to_string()));
1121 assert!(cfg.packs.contains(&"brain".to_string()));
1122 assert!(cfg.packs.contains(&"comm".to_string()));
1123 assert!(cfg.packs.contains(&"schedule".to_string()));
1124 assert!(cfg.packs.contains(&"knowledge".to_string()));
1125 assert!(cfg.packs.contains(&"session".to_string()));
1128 assert!(cfg.packs.contains(&"git".to_string()));
1129 assert!(cfg.packs.contains(&"code".to_string()));
1130 assert!(cfg.packs.contains(&"workspace".to_string()));
1131 assert_eq!(cfg.packs.len(), 11);
1132 if let Some(v) = prior {
1133 unsafe {
1135 std::env::set_var("KHIVE_PACKS", v);
1136 }
1137 }
1138 }
1139
1140 #[test]
1141 fn default_config_uses_minilm_when_env_unset() {
1142 let prior = std::env::var("KHIVE_EMBEDDING_MODEL").ok();
1143 unsafe {
1146 std::env::remove_var("KHIVE_EMBEDDING_MODEL");
1147 }
1148 let cfg = RuntimeConfig::default();
1149 assert_eq!(cfg.embedding_model, Some(EmbeddingModel::AllMiniLmL6V2));
1150 if let Some(v) = prior {
1151 unsafe {
1153 std::env::set_var("KHIVE_EMBEDDING_MODEL", v);
1154 }
1155 }
1156 }
1157
1158 use crate::engine_config::{ActorConfig, KhiveConfig};
1161
1162 fn khive_cfg_with_actor(id: &str) -> KhiveConfig {
1163 KhiveConfig {
1164 engines: vec![],
1165 actor: ActorConfig {
1166 id: Some(id.to_string()),
1167 display_name: None,
1168 ..Default::default()
1169 },
1170 ..KhiveConfig::default()
1171 }
1172 }
1173
1174 #[test]
1175 fn runtime_config_from_khive_config_actor_id_does_not_override_default_namespace() {
1176 let base = RuntimeConfig {
1181 git_write: Default::default(),
1182 db_path: None,
1183 default_namespace: Namespace::local(),
1184 embedding_model: None,
1185 additional_embedding_models: vec![],
1186 gate: Arc::new(AllowAllGate),
1187 packs: vec!["kg".to_string()],
1188 backend_id: BackendId::main(),
1189 brain_profile: None,
1190 visible_namespaces: vec![],
1191 allowed_outbound_namespaces: vec![],
1192 actor_id: None,
1193 };
1194 let cfg = khive_cfg_with_actor("lambda:khive");
1195 let result = runtime_config_from_khive_config(&cfg, base);
1196 assert_eq!(
1197 result.default_namespace.as_str(),
1198 "local",
1199 "actor.id must not become default_namespace (ADR-007 Rev 4 Rule 0); writes pin to local"
1200 );
1201 }
1202
1203 #[test]
1204 fn runtime_config_from_khive_config_empty_actor_id_keeps_base_namespace() {
1205 let base = RuntimeConfig {
1206 git_write: Default::default(),
1207 db_path: None,
1208 default_namespace: Namespace::parse("lambda:base").unwrap(),
1209 embedding_model: None,
1210 additional_embedding_models: vec![],
1211 gate: Arc::new(AllowAllGate),
1212 packs: vec!["kg".to_string()],
1213 backend_id: BackendId::main(),
1214 brain_profile: None,
1215 visible_namespaces: vec![],
1216 allowed_outbound_namespaces: vec![],
1217 actor_id: None,
1218 };
1219 let cfg = KhiveConfig {
1220 engines: vec![],
1221 actor: ActorConfig {
1222 id: Some(String::new()),
1223 display_name: None,
1224 ..Default::default()
1225 },
1226 ..KhiveConfig::default()
1227 };
1228 let result = runtime_config_from_khive_config(&cfg, base);
1229 assert_eq!(
1230 result.default_namespace.as_str(),
1231 "lambda:base",
1232 "empty actor.id must not override base namespace"
1233 );
1234 }
1235
1236 #[test]
1237 fn runtime_config_from_khive_config_absent_actor_id_keeps_base_namespace() {
1238 let base = RuntimeConfig {
1239 git_write: Default::default(),
1240 db_path: None,
1241 default_namespace: Namespace::parse("lambda:base").unwrap(),
1242 embedding_model: None,
1243 additional_embedding_models: vec![],
1244 gate: Arc::new(AllowAllGate),
1245 packs: vec!["kg".to_string()],
1246 backend_id: BackendId::main(),
1247 brain_profile: None,
1248 visible_namespaces: vec![],
1249 allowed_outbound_namespaces: vec![],
1250 actor_id: None,
1251 };
1252 let cfg = KhiveConfig::default(); let result = runtime_config_from_khive_config(&cfg, base);
1254 assert_eq!(
1255 result.default_namespace.as_str(),
1256 "lambda:base",
1257 "absent actor.id must not override base namespace"
1258 );
1259 }
1260
1261 #[test]
1262 fn runtime_config_from_khive_config_actor_id_with_engines() {
1263 let base = RuntimeConfig {
1264 git_write: Default::default(),
1265 db_path: None,
1266 default_namespace: Namespace::local(),
1267 embedding_model: None,
1268 additional_embedding_models: vec![],
1269 gate: Arc::new(AllowAllGate),
1270 packs: vec!["kg".to_string()],
1271 backend_id: BackendId::main(),
1272 brain_profile: None,
1273 visible_namespaces: vec![],
1274 allowed_outbound_namespaces: vec![],
1275 actor_id: None,
1276 };
1277 let cfg = KhiveConfig {
1278 engines: vec![crate::engine_config::EngineConfig {
1279 name: "default".to_string(),
1280 model: "all-minilm-l6-v2".to_string(),
1281 default: true,
1282 fusion_weight: None,
1283 dims: None,
1284 }],
1285 actor: ActorConfig {
1286 id: Some("lambda:test".to_string()),
1287 display_name: None,
1288 ..Default::default()
1289 },
1290 ..KhiveConfig::default()
1291 };
1292 let result = runtime_config_from_khive_config(&cfg, base);
1293 assert_eq!(
1294 result.default_namespace.as_str(),
1295 "local",
1296 "actor.id must not override default_namespace (ADR-007 Rev 4 Rule 0); \
1297 writes pin to local; engine config is still applied"
1298 );
1299 assert!(result.embedding_model.is_some());
1300 }
1301
1302 #[test]
1311 #[serial]
1312 fn runtime_config_from_khive_config_engines_present_preserves_env_actor_when_toml_has_none() {
1313 let prior = std::env::var("KHIVE_ACTOR").ok();
1314 unsafe {
1316 std::env::set_var("KHIVE_ACTOR", "lambda:test-env-actor");
1317 }
1318 let base = RuntimeConfig::default();
1319 assert_eq!(base.actor_id.as_deref(), Some("lambda:test-env-actor"));
1320
1321 let cfg = KhiveConfig {
1322 engines: vec![crate::engine_config::EngineConfig {
1323 name: "default".to_string(),
1324 model: "all-minilm-l6-v2".to_string(),
1325 default: true,
1326 fusion_weight: None,
1327 dims: None,
1328 }],
1329 actor: ActorConfig::default(), ..KhiveConfig::default()
1331 };
1332 let result = runtime_config_from_khive_config(&cfg, base);
1333 assert_eq!(
1334 result.actor_id.as_deref(),
1335 Some("lambda:test-env-actor"),
1336 "engines-present arm must preserve base.actor_id (env actor) when TOML has no [actor] id"
1337 );
1338
1339 unsafe {
1341 match prior {
1342 Some(v) => std::env::set_var("KHIVE_ACTOR", v),
1343 None => std::env::remove_var("KHIVE_ACTOR"),
1344 }
1345 }
1346 }
1347
1348 #[test]
1349 #[serial]
1350 fn runtime_config_from_khive_config_engines_empty_preserves_env_actor_when_toml_has_none() {
1351 let prior = std::env::var("KHIVE_ACTOR").ok();
1352 unsafe {
1354 std::env::set_var("KHIVE_ACTOR", "lambda:test-env-actor");
1355 }
1356 let base = RuntimeConfig::default();
1357 assert_eq!(base.actor_id.as_deref(), Some("lambda:test-env-actor"));
1358
1359 let cfg = KhiveConfig {
1360 engines: vec![],
1361 actor: ActorConfig::default(), ..KhiveConfig::default()
1363 };
1364 let result = runtime_config_from_khive_config(&cfg, base);
1365 assert_eq!(
1366 result.actor_id.as_deref(),
1367 Some("lambda:test-env-actor"),
1368 "engines-empty early-return arm must preserve base.actor_id (env actor) when TOML has no [actor] id"
1369 );
1370
1371 unsafe {
1373 match prior {
1374 Some(v) => std::env::set_var("KHIVE_ACTOR", v),
1375 None => std::env::remove_var("KHIVE_ACTOR"),
1376 }
1377 }
1378 }
1379
1380 #[test]
1381 #[serial]
1382 fn runtime_config_from_khive_config_toml_actor_wins_over_env_actor() {
1383 let prior = std::env::var("KHIVE_ACTOR").ok();
1384 unsafe {
1386 std::env::set_var("KHIVE_ACTOR", "lambda:test-env-actor");
1387 }
1388 let base = RuntimeConfig::default();
1389 assert_eq!(base.actor_id.as_deref(), Some("lambda:test-env-actor"));
1390
1391 let cfg = khive_cfg_with_actor("lambda:toml-actor");
1392 let result = runtime_config_from_khive_config(&cfg, base);
1393 assert_eq!(
1394 result.actor_id.as_deref(),
1395 Some("lambda:toml-actor"),
1396 "TOML [actor] id must win over the env-resolved base.actor_id"
1397 );
1398
1399 unsafe {
1401 match prior {
1402 Some(v) => std::env::set_var("KHIVE_ACTOR", v),
1403 None => std::env::remove_var("KHIVE_ACTOR"),
1404 }
1405 }
1406 }
1407
1408 fn migrated_memory_backend() -> Arc<StorageBackend> {
1414 let backend = StorageBackend::memory().expect("memory backend");
1415 {
1416 let mut writer = backend.pool().try_writer().expect("writer");
1417 khive_db::run_migrations(writer.conn_mut()).expect("migrations");
1418 }
1419 Arc::new(backend)
1420 }
1421
1422 fn secondary_config() -> RuntimeConfig {
1423 RuntimeConfig {
1424 git_write: Default::default(),
1425 db_path: None,
1426 default_namespace: Namespace::local(),
1427 embedding_model: None,
1428 additional_embedding_models: vec![],
1429 gate: Arc::new(AllowAllGate),
1430 packs: vec!["kg".to_string()],
1431 backend_id: BackendId::new("lore"),
1432 brain_profile: None,
1433 visible_namespaces: vec![],
1434 allowed_outbound_namespaces: vec![],
1435 actor_id: None,
1436 }
1437 }
1438
1439 #[test]
1440 fn core_on_main_runtime_returns_same_backend_id() {
1441 let rt = KhiveRuntime::memory().unwrap();
1443 assert_eq!(rt.backend_id().as_str(), BackendId::MAIN);
1444 let core_rt = rt.core();
1445 assert_eq!(core_rt.backend_id().as_str(), BackendId::MAIN);
1446 }
1447
1448 #[tokio::test]
1449 async fn core_on_main_runtime_round_trips_note() {
1450 let rt = KhiveRuntime::memory().unwrap();
1453 let tok = NamespaceToken::local();
1454
1455 let note = rt
1456 .core()
1457 .create_note(
1458 &tok,
1459 "observation",
1460 None,
1461 "adr073-main-round-trip",
1462 None,
1463 None,
1464 vec![],
1465 )
1466 .await
1467 .expect("create_note via core()");
1468
1469 let found = rt
1470 .notes(&tok)
1471 .expect("notes store")
1472 .get_note(note.id)
1473 .await
1474 .expect("get_note");
1475
1476 assert!(
1477 found.is_some(),
1478 "note written via core() must be visible through original rt"
1479 );
1480 }
1481
1482 #[tokio::test]
1494 async fn cross_backend_split_note_to_main_aux_to_secondary() {
1495 use khive_storage::{SqlStatement, SqlValue};
1496
1497 let main_arc = migrated_memory_backend();
1498 let secondary_arc = migrated_memory_backend();
1499
1500 let main_config = RuntimeConfig {
1501 git_write: Default::default(),
1502 db_path: None,
1503 default_namespace: Namespace::local(),
1504 embedding_model: None,
1505 additional_embedding_models: vec![],
1506 gate: Arc::new(AllowAllGate),
1507 packs: vec!["kg".to_string()],
1508 backend_id: BackendId::main(),
1509 brain_profile: None,
1510 visible_namespaces: vec![],
1511 allowed_outbound_namespaces: vec![],
1512 actor_id: None,
1513 };
1514
1515 let rt_main = KhiveRuntime::from_backend(main_arc.clone(), main_config);
1516 let rt_secondary = KhiveRuntime::from_backend(secondary_arc, secondary_config())
1517 .with_core_backend(main_arc.clone());
1518
1519 let tok = NamespaceToken::local();
1520
1521 let note = rt_secondary
1524 .core()
1525 .create_note(
1526 &tok,
1527 "observation",
1528 None,
1529 "adr073-split-test",
1530 None,
1531 None,
1532 vec![],
1533 )
1534 .await
1535 .expect("create_note via core()");
1536 let note_id = note.id;
1537
1538 let in_main = rt_main
1540 .notes(&tok)
1541 .expect("main notes store")
1542 .get_note(note_id)
1543 .await
1544 .expect("get_note from main");
1545 assert!(
1546 in_main.is_some(),
1547 "note written via core() must appear in main backend A"
1548 );
1549
1550 let in_secondary = rt_secondary
1552 .notes(&tok)
1553 .expect("secondary notes store")
1554 .get_note(note_id)
1555 .await
1556 .expect("get_note from secondary");
1557 assert!(
1558 in_secondary.is_none(),
1559 "note written to main via core() must NOT appear in secondary backend B"
1560 );
1561
1562 {
1565 let mut writer = rt_secondary.sql().writer().await.expect("secondary writer");
1566 writer
1567 .execute(SqlStatement {
1568 sql: "CREATE TABLE IF NOT EXISTS _test_adr073_aux \
1569 (marker TEXT PRIMARY KEY)"
1570 .into(),
1571 params: vec![],
1572 label: None,
1573 })
1574 .await
1575 .expect("create aux table in B");
1576 writer
1577 .execute(SqlStatement {
1578 sql: "INSERT INTO _test_adr073_aux VALUES (?1)".into(),
1579 params: vec![SqlValue::Text("b-side-sentinel".into())],
1580 label: None,
1581 })
1582 .await
1583 .expect("insert into aux table in B");
1584 }
1585
1586 let mut reader_b = rt_secondary.sql().reader().await.expect("secondary reader");
1588 let rows_b = reader_b
1589 .query_all(SqlStatement {
1590 sql: "SELECT marker FROM _test_adr073_aux".into(),
1591 params: vec![],
1592 label: None,
1593 })
1594 .await
1595 .expect("select from B");
1596 assert_eq!(rows_b.len(), 1, "aux row must exist in B");
1597 match rows_b[0].get("marker") {
1598 Some(SqlValue::Text(s)) => {
1599 assert_eq!(s, "b-side-sentinel", "sentinel value must match")
1600 }
1601 other => panic!("expected Text('b-side-sentinel'), got {other:?}"),
1602 }
1603
1604 let mut reader_a = rt_main.sql().reader().await.expect("main reader");
1606 let result_a = reader_a
1607 .query_all(SqlStatement {
1608 sql: "SELECT marker FROM _test_adr073_aux".into(),
1609 params: vec![],
1610 label: None,
1611 })
1612 .await;
1613 match result_a {
1615 Err(e) => assert!(
1616 e.to_string().contains("no such table"),
1617 "expected 'no such table' error from A, got: {e}"
1618 ),
1619 Ok(rows) => assert!(
1620 rows.is_empty(),
1621 "aux table must not have rows in A, got {} rows",
1622 rows.len()
1623 ),
1624 }
1625 }
1626
1627 #[test]
1628 fn constructors_leave_core_backend_none_by_behavior() {
1629 let rt_mem = KhiveRuntime::memory().unwrap();
1632 assert_eq!(rt_mem.core().backend_id().as_str(), BackendId::MAIN);
1633
1634 let backend = migrated_memory_backend();
1635 let rt_from = KhiveRuntime::from_backend(
1636 backend,
1637 RuntimeConfig {
1638 git_write: Default::default(),
1639 db_path: None,
1640 default_namespace: Namespace::local(),
1641 embedding_model: None,
1642 additional_embedding_models: vec![],
1643 gate: Arc::new(AllowAllGate),
1644 packs: vec!["kg".to_string()],
1645 backend_id: BackendId::new("lore"),
1646 brain_profile: None,
1647 visible_namespaces: vec![],
1648 allowed_outbound_namespaces: vec![],
1649 actor_id: None,
1650 },
1651 );
1652 assert_eq!(rt_from.core().backend_id().as_str(), "lore");
1655 }
1656
1657 #[test]
1658 fn with_core_backend_sets_core_then_core_returns_main_id() {
1659 let main_arc = migrated_memory_backend();
1661 let secondary_arc = migrated_memory_backend();
1662
1663 let rt_secondary = KhiveRuntime::from_backend(secondary_arc, secondary_config())
1664 .with_core_backend(main_arc);
1665
1666 assert_eq!(rt_secondary.backend_id().as_str(), "lore");
1667 assert_eq!(
1668 rt_secondary.core().backend_id().as_str(),
1669 BackendId::MAIN,
1670 "core() on a secondary runtime must return a main-bound handle"
1671 );
1672 }
1673
1674 #[tokio::test]
1675 async fn list_embedding_models_returns_empty_when_table_absent() {
1676 let rt = KhiveRuntime::memory().expect("memory runtime");
1679 let records = rt
1680 .list_embedding_models(None)
1681 .await
1682 .expect("list ok on empty table");
1683 assert!(records.is_empty());
1684 }
1685
1686 #[tokio::test]
1687 async fn list_embedding_models_returns_row_after_insert() {
1688 use khive_storage::{SqlStatement, SqlValue};
1689
1690 let rt = KhiveRuntime::memory().expect("memory runtime");
1691 let sql = rt.sql();
1692
1693 let now = 1_000_000i64;
1694 let id = uuid::Uuid::new_v4();
1695 let canonical_key = b"test_engine:test-model-v1:v1:384".to_vec();
1696
1697 let mut writer = sql.writer().await.expect("writer");
1698 writer
1699 .execute(SqlStatement {
1700 sql: "INSERT INTO _embedding_models \
1701 (id, engine_name, model_id, key_version, dim, output_dim, status, \
1702 activated_at, superseded_at, superseded_by, canonical_key, created_at) \
1703 VALUES (?1, ?2, ?3, ?4, ?5, NULL, ?6, ?7, NULL, NULL, ?8, ?9)"
1704 .into(),
1705 params: vec![
1706 SqlValue::Blob(id.as_bytes().to_vec()),
1707 SqlValue::Text("test_engine".into()),
1708 SqlValue::Text("test-model-v1".into()),
1709 SqlValue::Text("v1".into()),
1710 SqlValue::Integer(384),
1711 SqlValue::Text("active".into()),
1712 SqlValue::Integer(now),
1713 SqlValue::Blob(canonical_key),
1714 SqlValue::Integer(now),
1715 ],
1716 label: None,
1717 })
1718 .await
1719 .expect("insert row");
1720 drop(writer);
1721
1722 let records = rt.list_embedding_models(None).await.expect("list ok");
1723 assert_eq!(records.len(), 1);
1724 assert_eq!(records[0].engine_name, "test_engine");
1725 assert_eq!(records[0].model_id, "test-model-v1");
1726 assert_eq!(records[0].key_version, "v1");
1727 assert_eq!(records[0].dimensions, 384);
1728 assert_eq!(records[0].status, "active");
1729
1730 let filtered = rt
1732 .list_embedding_models(Some("test_engine"))
1733 .await
1734 .expect("filter ok");
1735 assert_eq!(filtered.len(), 1);
1736
1737 let no_match = rt
1739 .list_embedding_models(Some("other_engine"))
1740 .await
1741 .expect("no-match ok");
1742 assert!(no_match.is_empty());
1743 }
1744}