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 backend_ann_root(&self) -> Option<std::path::PathBuf> {
325 self.backend.ann_root()
326 }
327
328 pub fn entities(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn EntityStore>> {
332 Ok(self
333 .backend
334 .entities_for_namespace(token.namespace().as_str())?)
335 }
336
337 pub fn graph(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn GraphStore>> {
339 Ok(self
340 .backend
341 .graph_for_namespace(token.namespace().as_str())?)
342 }
343
344 pub fn notes(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn NoteStore>> {
346 Ok(self
347 .backend
348 .notes_for_namespace(token.namespace().as_str())?)
349 }
350
351 pub fn events(&self, token: &NamespaceToken) -> RuntimeResult<Arc<dyn EventStore>> {
353 Ok(self
354 .backend
355 .events_for_namespace(token.namespace().as_str())?)
356 }
357
358 pub fn sql(&self) -> Arc<dyn SqlAccess> {
360 self.backend.sql()
361 }
362
363 pub fn vectors(
367 &self,
368 token: &NamespaceToken,
369 ) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
370 let model = self.resolve_embedding_model(None)?;
371 self.vectors_for_embedding_model(token, model)
372 }
373
374 pub fn vectors_for_model(
382 &self,
383 token: &NamespaceToken,
384 model_name: &str,
385 ) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
386 if let Some(model) = parse_embedding_model_alias(model_name) {
387 let key = model.to_string();
390 let in_registry = self
391 .embedder_registry
392 .read()
393 .map(|reg| reg.contains(&key))
394 .unwrap_or(false);
395 if in_registry {
396 return self.vectors_for_embedding_model(token, model);
397 }
398 }
399 let dims = {
400 let registry = self.embedder_registry.read().map_err(|_| {
401 crate::RuntimeError::Internal("embedder registry lock poisoned".into())
402 })?;
403 registry
404 .get_provider(model_name)
405 .map(|p| p.dimensions())
406 .ok_or_else(|| crate::RuntimeError::UnknownModel(model_name.to_string()))?
407 };
408 let model_key = sanitize_key(model_name);
409 Ok(self.backend.vectors_for_namespace(
410 &model_key,
411 model_name,
412 dims,
413 token.namespace().as_str(),
414 )?)
415 }
416
417 pub fn embedder_dimensions(&self, model_name: &str) -> Option<usize> {
424 if let Some(model) = parse_embedding_model_alias(model_name) {
425 let key = model.to_string();
426 let in_registry = self
427 .embedder_registry
428 .read()
429 .map(|reg| reg.contains(&key))
430 .unwrap_or(false);
431 if in_registry {
432 return Some(model.dimensions());
433 }
434 }
435 self.embedder_registry
436 .read()
437 .ok()?
438 .get_provider(model_name)
439 .map(|p| p.dimensions())
440 }
441
442 fn vectors_for_embedding_model(
443 &self,
444 token: &NamespaceToken,
445 model: EmbeddingModel,
446 ) -> RuntimeResult<Arc<dyn khive_storage::VectorStore>> {
447 Ok(self.backend.vectors_for_namespace(
448 &vec_model_key(model),
449 &model.to_string(),
450 model.dimensions(),
451 token.namespace().as_str(),
452 )?)
453 }
454
455 pub fn text(
457 &self,
458 token: &NamespaceToken,
459 ) -> RuntimeResult<Arc<dyn khive_storage::TextSearch>> {
460 let _ = token;
461 Ok(self.backend.text("entities")?)
462 }
463
464 pub fn text_for_notes(
466 &self,
467 token: &NamespaceToken,
468 ) -> RuntimeResult<Arc<dyn khive_storage::TextSearch>> {
469 let _ = token;
470 Ok(self.backend.text("notes")?)
471 }
472
473 pub fn authorize(&self, ns: Namespace) -> RuntimeResult<NamespaceToken> {
489 let actor = crate::actor_identity::resolve_actor(self.config.actor_id.as_deref());
490 let req = GateRequest::new(
491 actor.clone(),
492 ns.clone(),
493 "authorize",
494 serde_json::Value::Null,
495 );
496 match self.config.gate.check(&req) {
497 Ok(ref decision) if decision.is_allow() => {
498 if let khive_gate::GateDecision::Allow { ref obligations } = decision {
499 if !obligations.is_empty() {
500 tracing::debug!(
501 namespace = %ns.as_str(),
502 "authorize: obligations={:?}",
503 obligations
504 );
505 }
506 }
507 Ok(NamespaceToken::mint_authorized(ns, actor))
508 }
509 Ok(khive_gate::GateDecision::Deny { reason }) => {
510 Err(crate::RuntimeError::PermissionDenied {
511 verb: "authorize".to_string(),
512 reason,
513 })
514 }
515 Ok(_) => Err(crate::RuntimeError::PermissionDenied {
516 verb: "authorize".to_string(),
517 reason: "gate denied".to_string(),
518 }),
519 Err(e) => Err(crate::RuntimeError::Internal(format!("gate error: {e}"))),
520 }
521 }
522
523 pub fn authorize_with_visibility(
538 &self,
539 primary: Namespace,
540 extra_visible: Vec<Namespace>,
541 ) -> RuntimeResult<NamespaceToken> {
542 let actor = crate::actor_identity::resolve_actor(self.config.actor_id.as_deref());
543 let req = GateRequest::new(
544 actor.clone(),
545 primary.clone(),
546 "authorize",
547 serde_json::Value::Null,
548 );
549 match self.config.gate.check(&req) {
550 Ok(ref decision) if decision.is_allow() => {
551 if let khive_gate::GateDecision::Allow { ref obligations } = decision {
552 if !obligations.is_empty() {
553 tracing::debug!(
554 namespace = %primary.as_str(),
555 "authorize_with_visibility: obligations={:?}",
556 obligations
557 );
558 }
559 }
560 Ok(NamespaceToken::mint_with_visibility(
561 primary,
562 extra_visible,
563 actor,
564 ))
565 }
566 Ok(khive_gate::GateDecision::Deny { reason }) => {
567 Err(crate::RuntimeError::PermissionDenied {
568 verb: "authorize".to_string(),
569 reason,
570 })
571 }
572 Ok(_) => Err(crate::RuntimeError::PermissionDenied {
573 verb: "authorize".to_string(),
574 reason: "gate denied".to_string(),
575 }),
576 Err(e) => Err(crate::RuntimeError::Internal(format!("gate error: {e}"))),
577 }
578 }
579
580 pub fn install_edge_rules(&self, rules: Vec<EdgeEndpointRule>) {
586 if let Ok(mut guard) = self.edge_rules.write() {
587 *guard = rules;
588 }
589 }
590
591 pub fn install_blob_store(&self, store: Arc<dyn khive_storage::BlobStore>) {
599 if let Ok(mut guard) = self.blob_store.write() {
600 *guard = Some(store);
601 }
602 }
603
604 pub fn blob_store(&self) -> Option<Arc<dyn khive_storage::BlobStore>> {
609 self.blob_store.read().ok().and_then(|guard| guard.clone())
610 }
611
612 pub fn install_kind_registry(&self, entity_kinds: Vec<String>, note_kinds: Vec<String>) {
622 if let Ok(mut guard) = self.valid_entity_kinds.write() {
623 *guard = entity_kinds;
624 }
625 if let Ok(mut guard) = self.valid_note_kinds.write() {
626 *guard = note_kinds;
627 }
628 }
629
630 pub(crate) fn validate_entity_kind(&self, kind: &str) -> crate::RuntimeResult<()> {
635 let guard = self.valid_entity_kinds.read().map_err(|_| {
636 crate::RuntimeError::Internal("entity kind registry lock poisoned".into())
637 })?;
638 if guard.is_empty() {
639 return Ok(());
640 }
641 if guard.iter().any(|k| k == kind) {
642 Ok(())
643 } else {
644 Err(crate::RuntimeError::InvalidInput(format!(
645 "unknown entity kind {kind:?}; valid: {}",
646 guard.join(", ")
647 )))
648 }
649 }
650
651 pub(crate) fn validate_note_kind(&self, kind: &str) -> crate::RuntimeResult<()> {
656 let guard = self.valid_note_kinds.read().map_err(|_| {
657 crate::RuntimeError::Internal("note kind registry lock poisoned".into())
658 })?;
659 if guard.is_empty() {
660 return Ok(());
661 }
662 if guard.iter().any(|k| k == kind) {
663 Ok(())
664 } else {
665 Err(crate::RuntimeError::InvalidInput(format!(
666 "unknown note kind {kind:?}; valid: {}",
667 guard.join(", ")
668 )))
669 }
670 }
671
672 pub fn install_entity_type_validator(&self, f: EntityTypeValidatorFn) {
682 if let Ok(mut guard) = self.entity_type_validator.write() {
683 *guard = Some(f);
684 }
685 }
686
687 pub(crate) fn validate_entity_type_for_kind(
692 &self,
693 kind: &str,
694 entity_type: Option<&str>,
695 ) -> crate::RuntimeResult<Option<String>> {
696 let guard = self.entity_type_validator.read().map_err(|_| {
697 crate::RuntimeError::Internal("entity type validator lock poisoned".into())
698 })?;
699 match guard.as_ref() {
700 None => Ok(entity_type.map(str::to_string)),
701 Some(validate) => validate(kind, entity_type),
702 }
703 }
704
705 pub fn install_note_mutation_hook(&self, f: NoteMutationHookFn) {
713 if let Ok(mut guard) = self.note_mutation_hook.write() {
714 *guard = Some(f);
715 }
716 }
717
718 pub(crate) async fn fire_note_mutation_hook(&self, kind: &str, id: uuid::Uuid) {
727 let hook = self
728 .note_mutation_hook
729 .read()
730 .ok()
731 .and_then(|guard| guard.clone());
732 if let Some(hook) = hook {
733 hook(kind.to_string(), id).await;
734 }
735 }
736
737 pub fn pack_edge_rules(&self) -> Vec<EdgeEndpointRule> {
746 self.edge_rules
747 .read()
748 .map(|g| g.clone())
749 .unwrap_or_default()
750 }
751
752 pub fn default_embedder_name(&self) -> &str {
754 self.default_embedder_name.as_ref()
755 }
756
757 pub fn resolve_embedding_model(&self, name: Option<&str>) -> RuntimeResult<EmbeddingModel> {
762 let model = match name {
763 Some(raw) => parse_embedding_model_alias(raw)
764 .ok_or_else(|| crate::RuntimeError::UnknownModel(raw.to_string()))?,
765 None => self
766 .config
767 .embedding_model
768 .ok_or_else(|| crate::RuntimeError::Unconfigured("embedding_model".into()))?,
769 };
770 let key = model.to_string();
771 let contains = self
772 .embedder_registry
773 .read()
774 .map(|reg| reg.contains(&key))
775 .unwrap_or(false);
776 if contains {
777 Ok(model)
778 } else {
779 Err(crate::RuntimeError::UnknownModel(
780 name.unwrap_or_else(|| self.default_embedder_name())
781 .to_string(),
782 ))
783 }
784 }
785
786 pub fn registered_embedding_model_names(&self) -> Vec<String> {
793 self.embedder_registry
794 .read()
795 .map(|reg| reg.names())
796 .unwrap_or_default()
797 }
798
799 pub async fn embedder(&self, name: &str) -> RuntimeResult<Arc<dyn EmbeddingService>> {
812 let canonical_key = match parse_embedding_model_alias(name) {
815 Some(model) => model.to_string(),
816 None => name.to_owned(),
817 };
818 let entry = {
821 let registry = self.embedder_registry.read().map_err(|_| {
822 crate::RuntimeError::Internal("embedder registry lock poisoned".into())
823 })?;
824 registry
825 .get_entry(&canonical_key)
826 .ok_or_else(|| crate::RuntimeError::UnknownModel(name.to_string()))?
827 };
828 entry.resolve().await
829 }
830
831 pub fn register_embedder(
844 &self,
845 provider: impl crate::embedder_registry::EmbedderProvider + 'static,
846 ) {
847 if let Ok(mut registry) = self.embedder_registry.write() {
848 registry.register(provider);
849 } else {
850 tracing::warn!(
851 "embedder registry lock poisoned — embedder {} not registered",
852 std::any::type_name::<dyn crate::embedder_registry::EmbedderProvider>()
853 );
854 }
855 }
856
857 pub async fn list_embedding_models(
864 &self,
865 engine_filter: Option<&str>,
866 ) -> RuntimeResult<Vec<khive_db::EmbeddingModelRegistryRecord>> {
867 use khive_storage::{SqlStatement, SqlValue};
868
869 let (sql_text, params) = if let Some(engine) = engine_filter {
870 (
871 "SELECT engine_name, model_id, key_version, dim, status, \
872 activated_at, superseded_at \
873 FROM _embedding_models WHERE engine_name = ?1 \
874 ORDER BY engine_name, activated_at IS NULL, activated_at"
875 .to_string(),
876 vec![SqlValue::Text(engine.to_string())],
877 )
878 } else {
879 (
880 "SELECT engine_name, model_id, key_version, dim, status, \
881 activated_at, superseded_at \
882 FROM _embedding_models \
883 ORDER BY engine_name, activated_at IS NULL, activated_at"
884 .to_string(),
885 vec![],
886 )
887 };
888
889 let stmt = SqlStatement {
890 sql: sql_text,
891 params,
892 label: Some("list_embedding_models".into()),
893 };
894
895 let mut reader = self
896 .sql()
897 .reader()
898 .await
899 .map_err(crate::RuntimeError::Storage)?;
900
901 let rows = match reader.query_all(stmt).await {
902 Ok(rows) => rows,
903 Err(e) if e.to_string().contains("no such table: _embedding_models") => {
904 return Ok(Vec::new())
905 }
906 Err(e) => return Err(crate::RuntimeError::Storage(e)),
907 };
908
909 let mut records = Vec::with_capacity(rows.len());
910 for row in rows {
911 macro_rules! required_text {
912 ($col:expr) => {
913 match row.get($col) {
914 Some(SqlValue::Text(s)) => s.clone(),
915 other => {
916 tracing::warn!(column = $col, value = ?other, "skipping registry row: unexpected type");
917 continue;
918 }
919 }
920 };
921 }
922 let engine_name = required_text!("engine_name");
923 let model_id = required_text!("model_id");
924 let key_version = required_text!("key_version");
925 let dimensions = match row.get("dim") {
926 Some(SqlValue::Integer(n)) => match u32::try_from(*n) {
927 Ok(d) => d,
928 Err(_) => {
929 tracing::warn!(dim = n, "skipping registry row: dim out of u32 range");
930 continue;
931 }
932 },
933 other => {
934 tracing::warn!(column = "dim", value = ?other, "skipping registry row: unexpected type");
935 continue;
936 }
937 };
938 let status = required_text!("status");
939 let activated_at = match row.get("activated_at") {
940 Some(SqlValue::Integer(n)) => Some(*n),
941 _ => None,
942 };
943 let superseded_at = match row.get("superseded_at") {
944 Some(SqlValue::Integer(n)) => Some(*n),
945 _ => None,
946 };
947 records.push(khive_db::EmbeddingModelRegistryRecord {
948 engine_name,
949 model_id,
950 key_version,
951 dimensions,
952 status,
953 activated_at,
954 superseded_at,
955 });
956 }
957
958 Ok(records)
959 }
960}
961
962#[cfg(test)]
966mod tests {
967 use super::*;
968 use khive_gate::GateRef;
969 use serial_test::serial;
970
971 #[test]
972 fn memory_runtime_creates_successfully() {
973 let rt = KhiveRuntime::memory().expect("memory runtime should create");
974 assert!(rt.config().db_path.is_none());
975 }
976
977 #[test]
978 fn backend_data_dir_returns_none_for_memory_backend() {
979 let rt = KhiveRuntime::memory().expect("memory runtime");
980 assert!(rt.backend_data_dir().is_none());
981 }
982
983 #[test]
984 fn backend_data_dir_returns_parent_dir_for_file_backend() {
985 let dir = tempfile::tempdir().unwrap();
986 let path = dir.path().join("test.db");
987 let config = RuntimeConfig {
988 git_write: Default::default(),
989 db_path: Some(path),
990 default_namespace: Namespace::local(),
991 embedding_model: None,
992 additional_embedding_models: vec![],
993 gate: Arc::new(AllowAllGate),
994 packs: vec!["kg".to_string()],
995 backend_id: BackendId::main(),
996 brain_profile: None,
997 visible_namespaces: vec![],
998 allowed_outbound_namespaces: vec![],
999 actor_id: None,
1000 };
1001 let rt = KhiveRuntime::new(config).expect("file runtime");
1002 let data_dir = rt
1003 .backend_data_dir()
1004 .expect("file backend must return Some");
1005 assert_eq!(data_dir, dir.path());
1006 }
1007
1008 #[test]
1009 fn backend_data_dir_returns_none_for_from_backend_with_memory() {
1010 let backend = Arc::new(StorageBackend::memory().expect("memory backend"));
1011 let config = RuntimeConfig {
1012 git_write: Default::default(),
1013 db_path: None,
1014 default_namespace: Namespace::local(),
1015 embedding_model: None,
1016 additional_embedding_models: vec![],
1017 gate: Arc::new(AllowAllGate),
1018 packs: vec!["kg".to_string()],
1019 backend_id: BackendId::main(),
1020 brain_profile: None,
1021 visible_namespaces: vec![],
1022 allowed_outbound_namespaces: vec![],
1023 actor_id: None,
1024 };
1025 let rt = KhiveRuntime::from_backend(backend, config);
1026 assert!(rt.backend_data_dir().is_none());
1027 }
1028
1029 #[test]
1030 fn file_runtime_creates_successfully() {
1031 let dir = tempfile::tempdir().unwrap();
1032 let path = dir.path().join("test.db");
1033 let config = RuntimeConfig {
1034 git_write: Default::default(),
1035 db_path: Some(path.clone()),
1036 default_namespace: Namespace::parse("test").unwrap(),
1037 embedding_model: None,
1038 additional_embedding_models: vec![],
1039 gate: Arc::new(AllowAllGate),
1040 packs: vec!["kg".to_string()],
1041 backend_id: BackendId::main(),
1042 brain_profile: None,
1043 visible_namespaces: vec![],
1044 allowed_outbound_namespaces: vec![],
1045 actor_id: None,
1046 };
1047 let rt = KhiveRuntime::new(config).expect("file runtime should create");
1048 assert!(path.exists());
1049 assert_eq!(rt.config().default_namespace.as_str(), "test");
1050 }
1051
1052 #[test]
1053 fn from_backend_uses_provided_backend() {
1054 let backend = Arc::new(StorageBackend::memory().expect("memory backend"));
1055 let config = RuntimeConfig {
1056 git_write: Default::default(),
1057 db_path: None,
1058 default_namespace: Namespace::local(),
1059 embedding_model: None,
1060 additional_embedding_models: vec![],
1061 gate: Arc::new(AllowAllGate),
1062 packs: vec!["kg".to_string()],
1063 backend_id: BackendId::new("lore"),
1064 brain_profile: None,
1065 visible_namespaces: vec![],
1066 allowed_outbound_namespaces: vec![],
1067 actor_id: None,
1068 };
1069 let rt = KhiveRuntime::from_backend(backend, config);
1070 assert_eq!(rt.backend_id().as_str(), "lore");
1071 assert!(rt.config().db_path.is_none());
1072 }
1073
1074 #[test]
1075 fn backend_id_defaults_to_main() {
1076 let rt = KhiveRuntime::memory().unwrap();
1077 assert_eq!(rt.backend_id().as_str(), BackendId::MAIN);
1078 }
1079
1080 #[test]
1081 fn store_accessors_return_ok() {
1082 let rt = KhiveRuntime::memory().unwrap();
1083 let tok = NamespaceToken::local();
1084 assert!(rt.entities(&tok).is_ok());
1085 assert!(rt.graph(&tok).is_ok());
1086 assert!(rt.notes(&tok).is_ok());
1087 assert!(rt.events(&tok).is_ok());
1088 }
1089
1090 #[test]
1091 fn vectors_returns_unconfigured_without_model() {
1092 let rt = KhiveRuntime::memory().unwrap();
1093 let tok = NamespaceToken::local();
1094 match rt.vectors(&tok) {
1095 Err(crate::RuntimeError::Unconfigured(s)) => assert_eq!(s, "embedding_model"),
1096 Err(other) => panic!("expected Unconfigured, got {:?}", other),
1097 Ok(_) => panic!("expected Err, got Ok"),
1098 }
1099 }
1100
1101 #[test]
1102 fn vec_model_key_sanitizes_dots_and_dashes() {
1103 assert_eq!(
1104 vec_model_key(EmbeddingModel::BgeSmallEnV15),
1105 "bge_small_en_v1_5"
1106 );
1107 assert_eq!(
1108 vec_model_key(EmbeddingModel::BgeBaseEnV15),
1109 "bge_base_en_v1_5"
1110 );
1111 assert_eq!(
1112 vec_model_key(EmbeddingModel::AllMiniLmL6V2),
1113 "all_minilm_l6_v2"
1114 );
1115 }
1116
1117 #[test]
1118 fn default_config_uses_allow_all_gate() {
1119 let cfg = RuntimeConfig::default();
1120 assert_eq!(cfg.default_namespace.as_str(), "local");
1121 let _: GateRef = cfg.gate.clone();
1122 }
1123
1124 #[test]
1125 fn parse_pack_list_handles_comma_and_whitespace() {
1126 assert_eq!(parse_pack_list("kg"), vec!["kg".to_string()]);
1127 assert_eq!(
1128 parse_pack_list("kg,gtd"),
1129 vec!["kg".to_string(), "gtd".to_string()]
1130 );
1131 assert_eq!(
1132 parse_pack_list(" kg , gtd "),
1133 vec!["kg".to_string(), "gtd".to_string()]
1134 );
1135 assert_eq!(
1136 parse_pack_list("kg gtd"),
1137 vec!["kg".to_string(), "gtd".to_string()]
1138 );
1139 assert_eq!(parse_pack_list(",,"), Vec::<String>::new());
1140 assert_eq!(parse_pack_list(""), Vec::<String>::new());
1141 }
1142
1143 #[test]
1144 fn default_config_packs_loads_kg_only() {
1145 let prior = std::env::var("KHIVE_PACKS").ok();
1146 unsafe {
1148 std::env::remove_var("KHIVE_PACKS");
1149 }
1150 let cfg = RuntimeConfig::default();
1154 assert_eq!(cfg.packs, vec!["kg".to_string()]);
1155 if let Some(v) = prior {
1156 unsafe {
1158 std::env::set_var("KHIVE_PACKS", v);
1159 }
1160 }
1161 }
1162
1163 #[test]
1164 fn default_config_uses_minilm_when_env_unset() {
1165 let prior = std::env::var("KHIVE_EMBEDDING_MODEL").ok();
1166 unsafe {
1169 std::env::remove_var("KHIVE_EMBEDDING_MODEL");
1170 }
1171 let cfg = RuntimeConfig::default();
1172 assert_eq!(cfg.embedding_model, Some(EmbeddingModel::AllMiniLmL6V2));
1173 if let Some(v) = prior {
1174 unsafe {
1176 std::env::set_var("KHIVE_EMBEDDING_MODEL", v);
1177 }
1178 }
1179 }
1180
1181 use crate::engine_config::{ActorConfig, KhiveConfig};
1184
1185 fn khive_cfg_with_actor(id: &str) -> KhiveConfig {
1186 KhiveConfig {
1187 engines: vec![],
1188 actor: ActorConfig {
1189 id: Some(id.to_string()),
1190 display_name: None,
1191 ..Default::default()
1192 },
1193 ..KhiveConfig::default()
1194 }
1195 }
1196
1197 #[test]
1198 fn runtime_config_from_khive_config_actor_id_does_not_override_default_namespace() {
1199 let base = RuntimeConfig {
1204 git_write: Default::default(),
1205 db_path: None,
1206 default_namespace: Namespace::local(),
1207 embedding_model: None,
1208 additional_embedding_models: vec![],
1209 gate: Arc::new(AllowAllGate),
1210 packs: vec!["kg".to_string()],
1211 backend_id: BackendId::main(),
1212 brain_profile: None,
1213 visible_namespaces: vec![],
1214 allowed_outbound_namespaces: vec![],
1215 actor_id: None,
1216 };
1217 let cfg = khive_cfg_with_actor("lambda:khive");
1218 let result = runtime_config_from_khive_config(&cfg, base);
1219 assert_eq!(
1220 result.default_namespace.as_str(),
1221 "local",
1222 "actor.id must not become default_namespace (ADR-007 Rev 4 Rule 0); writes pin to local"
1223 );
1224 }
1225
1226 #[test]
1227 fn runtime_config_from_khive_config_empty_actor_id_keeps_base_namespace() {
1228 let base = RuntimeConfig {
1229 git_write: Default::default(),
1230 db_path: None,
1231 default_namespace: Namespace::parse("lambda:base").unwrap(),
1232 embedding_model: None,
1233 additional_embedding_models: vec![],
1234 gate: Arc::new(AllowAllGate),
1235 packs: vec!["kg".to_string()],
1236 backend_id: BackendId::main(),
1237 brain_profile: None,
1238 visible_namespaces: vec![],
1239 allowed_outbound_namespaces: vec![],
1240 actor_id: None,
1241 };
1242 let cfg = KhiveConfig {
1243 engines: vec![],
1244 actor: ActorConfig {
1245 id: Some(String::new()),
1246 display_name: None,
1247 ..Default::default()
1248 },
1249 ..KhiveConfig::default()
1250 };
1251 let result = runtime_config_from_khive_config(&cfg, base);
1252 assert_eq!(
1253 result.default_namespace.as_str(),
1254 "lambda:base",
1255 "empty actor.id must not override base namespace"
1256 );
1257 }
1258
1259 #[test]
1260 fn runtime_config_from_khive_config_absent_actor_id_keeps_base_namespace() {
1261 let base = RuntimeConfig {
1262 git_write: Default::default(),
1263 db_path: None,
1264 default_namespace: Namespace::parse("lambda:base").unwrap(),
1265 embedding_model: None,
1266 additional_embedding_models: vec![],
1267 gate: Arc::new(AllowAllGate),
1268 packs: vec!["kg".to_string()],
1269 backend_id: BackendId::main(),
1270 brain_profile: None,
1271 visible_namespaces: vec![],
1272 allowed_outbound_namespaces: vec![],
1273 actor_id: None,
1274 };
1275 let cfg = KhiveConfig::default(); let result = runtime_config_from_khive_config(&cfg, base);
1277 assert_eq!(
1278 result.default_namespace.as_str(),
1279 "lambda:base",
1280 "absent actor.id must not override base namespace"
1281 );
1282 }
1283
1284 #[test]
1285 fn runtime_config_from_khive_config_actor_id_with_engines() {
1286 let base = RuntimeConfig {
1287 git_write: Default::default(),
1288 db_path: None,
1289 default_namespace: Namespace::local(),
1290 embedding_model: None,
1291 additional_embedding_models: vec![],
1292 gate: Arc::new(AllowAllGate),
1293 packs: vec!["kg".to_string()],
1294 backend_id: BackendId::main(),
1295 brain_profile: None,
1296 visible_namespaces: vec![],
1297 allowed_outbound_namespaces: vec![],
1298 actor_id: None,
1299 };
1300 let cfg = KhiveConfig {
1301 engines: vec![crate::engine_config::EngineConfig {
1302 name: "default".to_string(),
1303 model: "all-minilm-l6-v2".to_string(),
1304 default: true,
1305 fusion_weight: None,
1306 dims: None,
1307 }],
1308 actor: ActorConfig {
1309 id: Some("lambda:test".to_string()),
1310 display_name: None,
1311 ..Default::default()
1312 },
1313 ..KhiveConfig::default()
1314 };
1315 let result = runtime_config_from_khive_config(&cfg, base);
1316 assert_eq!(
1317 result.default_namespace.as_str(),
1318 "local",
1319 "actor.id must not override default_namespace (ADR-007 Rev 4 Rule 0); \
1320 writes pin to local; engine config is still applied"
1321 );
1322 assert!(result.embedding_model.is_some());
1323 }
1324
1325 #[test]
1334 #[serial]
1335 fn runtime_config_from_khive_config_engines_present_preserves_env_actor_when_toml_has_none() {
1336 let prior = std::env::var("KHIVE_ACTOR").ok();
1337 unsafe {
1339 std::env::set_var("KHIVE_ACTOR", "lambda:test-env-actor");
1340 }
1341 let base = RuntimeConfig::default();
1342 assert_eq!(base.actor_id.as_deref(), Some("lambda:test-env-actor"));
1343
1344 let cfg = KhiveConfig {
1345 engines: vec![crate::engine_config::EngineConfig {
1346 name: "default".to_string(),
1347 model: "all-minilm-l6-v2".to_string(),
1348 default: true,
1349 fusion_weight: None,
1350 dims: None,
1351 }],
1352 actor: ActorConfig::default(), ..KhiveConfig::default()
1354 };
1355 let result = runtime_config_from_khive_config(&cfg, base);
1356 assert_eq!(
1357 result.actor_id.as_deref(),
1358 Some("lambda:test-env-actor"),
1359 "engines-present arm must preserve base.actor_id (env actor) when TOML has no [actor] id"
1360 );
1361
1362 unsafe {
1364 match prior {
1365 Some(v) => std::env::set_var("KHIVE_ACTOR", v),
1366 None => std::env::remove_var("KHIVE_ACTOR"),
1367 }
1368 }
1369 }
1370
1371 #[test]
1372 #[serial]
1373 fn runtime_config_from_khive_config_engines_empty_preserves_env_actor_when_toml_has_none() {
1374 let prior = std::env::var("KHIVE_ACTOR").ok();
1375 unsafe {
1377 std::env::set_var("KHIVE_ACTOR", "lambda:test-env-actor");
1378 }
1379 let base = RuntimeConfig::default();
1380 assert_eq!(base.actor_id.as_deref(), Some("lambda:test-env-actor"));
1381
1382 let cfg = KhiveConfig {
1383 engines: vec![],
1384 actor: ActorConfig::default(), ..KhiveConfig::default()
1386 };
1387 let result = runtime_config_from_khive_config(&cfg, base);
1388 assert_eq!(
1389 result.actor_id.as_deref(),
1390 Some("lambda:test-env-actor"),
1391 "engines-empty early-return arm must preserve base.actor_id (env actor) when TOML has no [actor] id"
1392 );
1393
1394 unsafe {
1396 match prior {
1397 Some(v) => std::env::set_var("KHIVE_ACTOR", v),
1398 None => std::env::remove_var("KHIVE_ACTOR"),
1399 }
1400 }
1401 }
1402
1403 #[test]
1404 #[serial]
1405 fn runtime_config_from_khive_config_toml_actor_wins_over_env_actor() {
1406 let prior = std::env::var("KHIVE_ACTOR").ok();
1407 unsafe {
1409 std::env::set_var("KHIVE_ACTOR", "lambda:test-env-actor");
1410 }
1411 let base = RuntimeConfig::default();
1412 assert_eq!(base.actor_id.as_deref(), Some("lambda:test-env-actor"));
1413
1414 let cfg = khive_cfg_with_actor("lambda:toml-actor");
1415 let result = runtime_config_from_khive_config(&cfg, base);
1416 assert_eq!(
1417 result.actor_id.as_deref(),
1418 Some("lambda:toml-actor"),
1419 "TOML [actor] id must win over the env-resolved base.actor_id"
1420 );
1421
1422 unsafe {
1424 match prior {
1425 Some(v) => std::env::set_var("KHIVE_ACTOR", v),
1426 None => std::env::remove_var("KHIVE_ACTOR"),
1427 }
1428 }
1429 }
1430
1431 fn migrated_memory_backend() -> Arc<StorageBackend> {
1437 let backend = StorageBackend::memory().expect("memory backend");
1438 {
1439 let mut writer = backend.pool().try_writer().expect("writer");
1440 khive_db::run_migrations(writer.conn_mut()).expect("migrations");
1441 }
1442 Arc::new(backend)
1443 }
1444
1445 fn secondary_config() -> RuntimeConfig {
1446 RuntimeConfig {
1447 git_write: Default::default(),
1448 db_path: None,
1449 default_namespace: Namespace::local(),
1450 embedding_model: None,
1451 additional_embedding_models: vec![],
1452 gate: Arc::new(AllowAllGate),
1453 packs: vec!["kg".to_string()],
1454 backend_id: BackendId::new("lore"),
1455 brain_profile: None,
1456 visible_namespaces: vec![],
1457 allowed_outbound_namespaces: vec![],
1458 actor_id: None,
1459 }
1460 }
1461
1462 #[test]
1463 fn core_on_main_runtime_returns_same_backend_id() {
1464 let rt = KhiveRuntime::memory().unwrap();
1466 assert_eq!(rt.backend_id().as_str(), BackendId::MAIN);
1467 let core_rt = rt.core();
1468 assert_eq!(core_rt.backend_id().as_str(), BackendId::MAIN);
1469 }
1470
1471 #[tokio::test]
1472 async fn core_on_main_runtime_round_trips_note() {
1473 let rt = KhiveRuntime::memory().unwrap();
1476 let tok = NamespaceToken::local();
1477
1478 let note = rt
1479 .core()
1480 .create_note(
1481 &tok,
1482 "observation",
1483 None,
1484 "adr073-main-round-trip",
1485 None,
1486 None,
1487 vec![],
1488 )
1489 .await
1490 .expect("create_note via core()");
1491
1492 let found = rt
1493 .notes(&tok)
1494 .expect("notes store")
1495 .get_note(note.id)
1496 .await
1497 .expect("get_note");
1498
1499 assert!(
1500 found.is_some(),
1501 "note written via core() must be visible through original rt"
1502 );
1503 }
1504
1505 #[tokio::test]
1517 async fn cross_backend_split_note_to_main_aux_to_secondary() {
1518 use khive_storage::{SqlStatement, SqlValue};
1519
1520 let main_arc = migrated_memory_backend();
1521 let secondary_arc = migrated_memory_backend();
1522
1523 let main_config = RuntimeConfig {
1524 git_write: Default::default(),
1525 db_path: None,
1526 default_namespace: Namespace::local(),
1527 embedding_model: None,
1528 additional_embedding_models: vec![],
1529 gate: Arc::new(AllowAllGate),
1530 packs: vec!["kg".to_string()],
1531 backend_id: BackendId::main(),
1532 brain_profile: None,
1533 visible_namespaces: vec![],
1534 allowed_outbound_namespaces: vec![],
1535 actor_id: None,
1536 };
1537
1538 let rt_main = KhiveRuntime::from_backend(main_arc.clone(), main_config);
1539 let rt_secondary = KhiveRuntime::from_backend(secondary_arc, secondary_config())
1540 .with_core_backend(main_arc.clone());
1541
1542 let tok = NamespaceToken::local();
1543
1544 let note = rt_secondary
1547 .core()
1548 .create_note(
1549 &tok,
1550 "observation",
1551 None,
1552 "adr073-split-test",
1553 None,
1554 None,
1555 vec![],
1556 )
1557 .await
1558 .expect("create_note via core()");
1559 let note_id = note.id;
1560
1561 let in_main = rt_main
1563 .notes(&tok)
1564 .expect("main notes store")
1565 .get_note(note_id)
1566 .await
1567 .expect("get_note from main");
1568 assert!(
1569 in_main.is_some(),
1570 "note written via core() must appear in main backend A"
1571 );
1572
1573 let in_secondary = rt_secondary
1575 .notes(&tok)
1576 .expect("secondary notes store")
1577 .get_note(note_id)
1578 .await
1579 .expect("get_note from secondary");
1580 assert!(
1581 in_secondary.is_none(),
1582 "note written to main via core() must NOT appear in secondary backend B"
1583 );
1584
1585 {
1588 let mut writer = rt_secondary.sql().writer().await.expect("secondary writer");
1589 writer
1590 .execute(SqlStatement {
1591 sql: "CREATE TABLE IF NOT EXISTS _test_adr073_aux \
1592 (marker TEXT PRIMARY KEY)"
1593 .into(),
1594 params: vec![],
1595 label: None,
1596 })
1597 .await
1598 .expect("create aux table in B");
1599 writer
1600 .execute(SqlStatement {
1601 sql: "INSERT INTO _test_adr073_aux VALUES (?1)".into(),
1602 params: vec![SqlValue::Text("b-side-sentinel".into())],
1603 label: None,
1604 })
1605 .await
1606 .expect("insert into aux table in B");
1607 }
1608
1609 let mut reader_b = rt_secondary.sql().reader().await.expect("secondary reader");
1611 let rows_b = reader_b
1612 .query_all(SqlStatement {
1613 sql: "SELECT marker FROM _test_adr073_aux".into(),
1614 params: vec![],
1615 label: None,
1616 })
1617 .await
1618 .expect("select from B");
1619 assert_eq!(rows_b.len(), 1, "aux row must exist in B");
1620 match rows_b[0].get("marker") {
1621 Some(SqlValue::Text(s)) => {
1622 assert_eq!(s, "b-side-sentinel", "sentinel value must match")
1623 }
1624 other => panic!("expected Text('b-side-sentinel'), got {other:?}"),
1625 }
1626
1627 let mut reader_a = rt_main.sql().reader().await.expect("main reader");
1629 let result_a = reader_a
1630 .query_all(SqlStatement {
1631 sql: "SELECT marker FROM _test_adr073_aux".into(),
1632 params: vec![],
1633 label: None,
1634 })
1635 .await;
1636 match result_a {
1638 Err(e) => assert!(
1639 e.to_string().contains("no such table"),
1640 "expected 'no such table' error from A, got: {e}"
1641 ),
1642 Ok(rows) => assert!(
1643 rows.is_empty(),
1644 "aux table must not have rows in A, got {} rows",
1645 rows.len()
1646 ),
1647 }
1648 }
1649
1650 #[test]
1651 fn constructors_leave_core_backend_none_by_behavior() {
1652 let rt_mem = KhiveRuntime::memory().unwrap();
1655 assert_eq!(rt_mem.core().backend_id().as_str(), BackendId::MAIN);
1656
1657 let backend = migrated_memory_backend();
1658 let rt_from = KhiveRuntime::from_backend(
1659 backend,
1660 RuntimeConfig {
1661 git_write: Default::default(),
1662 db_path: None,
1663 default_namespace: Namespace::local(),
1664 embedding_model: None,
1665 additional_embedding_models: vec![],
1666 gate: Arc::new(AllowAllGate),
1667 packs: vec!["kg".to_string()],
1668 backend_id: BackendId::new("lore"),
1669 brain_profile: None,
1670 visible_namespaces: vec![],
1671 allowed_outbound_namespaces: vec![],
1672 actor_id: None,
1673 },
1674 );
1675 assert_eq!(rt_from.core().backend_id().as_str(), "lore");
1678 }
1679
1680 #[test]
1681 fn with_core_backend_sets_core_then_core_returns_main_id() {
1682 let main_arc = migrated_memory_backend();
1684 let secondary_arc = migrated_memory_backend();
1685
1686 let rt_secondary = KhiveRuntime::from_backend(secondary_arc, secondary_config())
1687 .with_core_backend(main_arc);
1688
1689 assert_eq!(rt_secondary.backend_id().as_str(), "lore");
1690 assert_eq!(
1691 rt_secondary.core().backend_id().as_str(),
1692 BackendId::MAIN,
1693 "core() on a secondary runtime must return a main-bound handle"
1694 );
1695 }
1696
1697 #[tokio::test]
1698 async fn list_embedding_models_returns_empty_when_table_absent() {
1699 let rt = KhiveRuntime::memory().expect("memory runtime");
1702 let records = rt
1703 .list_embedding_models(None)
1704 .await
1705 .expect("list ok on empty table");
1706 assert!(records.is_empty());
1707 }
1708
1709 #[tokio::test]
1710 async fn list_embedding_models_returns_row_after_insert() {
1711 use khive_storage::{SqlStatement, SqlValue};
1712
1713 let rt = KhiveRuntime::memory().expect("memory runtime");
1714 let sql = rt.sql();
1715
1716 let now = 1_000_000i64;
1717 let id = uuid::Uuid::new_v4();
1718 let canonical_key = b"test_engine:test-model-v1:v1:384".to_vec();
1719
1720 let mut writer = sql.writer().await.expect("writer");
1721 writer
1722 .execute(SqlStatement {
1723 sql: "INSERT INTO _embedding_models \
1724 (id, engine_name, model_id, key_version, dim, output_dim, status, \
1725 activated_at, superseded_at, superseded_by, canonical_key, created_at) \
1726 VALUES (?1, ?2, ?3, ?4, ?5, NULL, ?6, ?7, NULL, NULL, ?8, ?9)"
1727 .into(),
1728 params: vec![
1729 SqlValue::Blob(id.as_bytes().to_vec()),
1730 SqlValue::Text("test_engine".into()),
1731 SqlValue::Text("test-model-v1".into()),
1732 SqlValue::Text("v1".into()),
1733 SqlValue::Integer(384),
1734 SqlValue::Text("active".into()),
1735 SqlValue::Integer(now),
1736 SqlValue::Blob(canonical_key),
1737 SqlValue::Integer(now),
1738 ],
1739 label: None,
1740 })
1741 .await
1742 .expect("insert row");
1743 drop(writer);
1744
1745 let records = rt.list_embedding_models(None).await.expect("list ok");
1746 assert_eq!(records.len(), 1);
1747 assert_eq!(records[0].engine_name, "test_engine");
1748 assert_eq!(records[0].model_id, "test-model-v1");
1749 assert_eq!(records[0].key_version, "v1");
1750 assert_eq!(records[0].dimensions, 384);
1751 assert_eq!(records[0].status, "active");
1752
1753 let filtered = rt
1755 .list_embedding_models(Some("test_engine"))
1756 .await
1757 .expect("filter ok");
1758 assert_eq!(filtered.len(), 1);
1759
1760 let no_match = rt
1762 .list_embedding_models(Some("other_engine"))
1763 .await
1764 .expect("no-match ok");
1765 assert!(no_match.is_empty());
1766 }
1767}