use khive_runtime::{KhiveRuntime, Namespace, RuntimeConfig};
use khive_storage::types::{Direction, TraversalOptions, TraversalRequest};
use khive_storage::{EdgeRelation, Event};
use khive_types::{EventKind, SubstrateKind};
use uuid::Uuid;
fn rt() -> KhiveRuntime {
KhiveRuntime::memory().expect("in-memory runtime")
}
#[tokio::test]
async fn entity_create_and_get_roundtrip() {
let rt = rt();
let tok = rt.authorize(Namespace::local()).unwrap();
let entity = rt
.create_entity(
&tok,
"concept",
None,
"LoRA",
Some("Low-Rank Adaptation"),
None,
vec![],
)
.await
.unwrap();
let fetched = rt.get_entity(&tok, entity.id).await.unwrap();
assert_eq!(fetched.id, entity.id);
assert_eq!(fetched.name, "LoRA");
assert_eq!(fetched.kind, "concept");
assert_eq!(fetched.description.as_deref(), Some("Low-Rank Adaptation"));
}
#[tokio::test]
async fn entity_create_with_properties_and_tags() {
let rt = rt();
let research_tok = rt.authorize(Namespace::parse("research").unwrap()).unwrap();
let props = serde_json::json!({"domain": "fine-tuning", "type": "technique"});
let entity = rt
.create_entity(
&research_tok,
"concept",
None,
"QLoRA",
Some("Quantized LoRA"),
Some(props.clone()),
vec!["fine-tuning".to_string(), "quantization".to_string()],
)
.await
.unwrap();
let fetched = rt.get_entity(&research_tok, entity.id).await.unwrap();
assert_eq!(fetched.properties, Some(props));
assert_eq!(fetched.tags, vec!["fine-tuning", "quantization"]);
}
#[tokio::test]
async fn entity_list_by_kind() {
let rt = rt();
let tok = rt.authorize(Namespace::local()).unwrap();
rt.create_entity(&tok, "concept", None, "FlashAttention", None, None, vec![])
.await
.unwrap();
rt.create_entity(&tok, "concept", None, "GQA", None, None, vec![])
.await
.unwrap();
rt.create_entity(
&tok,
"document",
None,
"Attention Is All You Need",
None,
None,
vec![],
)
.await
.unwrap();
let concepts = rt
.list_entities(&tok, Some("concept"), None, 50, 0)
.await
.unwrap();
assert_eq!(concepts.len(), 2);
assert!(concepts.iter().any(|e| e.name == "FlashAttention"));
assert!(concepts.iter().any(|e| e.name == "GQA"));
let docs = rt
.list_entities(&tok, Some("document"), None, 50, 0)
.await
.unwrap();
assert_eq!(docs.len(), 1);
assert_eq!(docs[0].name, "Attention Is All You Need");
let all = rt.list_entities(&tok, None, None, 50, 0).await.unwrap();
assert_eq!(all.len(), 3);
}
#[tokio::test]
async fn entity_delete_soft() {
let rt = rt();
let tok = rt.authorize(Namespace::local()).unwrap();
let entity = rt
.create_entity(&tok, "concept", None, "to-delete", None, None, vec![])
.await
.unwrap();
let deleted = rt.delete_entity(&tok, entity.id, false).await.unwrap();
assert!(deleted);
let fetched = rt.get_entity(&tok, entity.id).await;
assert!(fetched.is_err());
}
#[tokio::test]
async fn entity_count_by_kind() {
let rt = rt();
let tok = rt.authorize(Namespace::local()).unwrap();
for _ in 0..3 {
rt.create_entity(&tok, "concept", None, "concept-X", None, None, vec![])
.await
.unwrap();
}
for _ in 0..2 {
rt.create_entity(&tok, "document", None, "doc-Y", None, None, vec![])
.await
.unwrap();
}
let concept_count = rt.count_entities(&tok, Some("concept")).await.unwrap();
let doc_count = rt.count_entities(&tok, Some("document")).await.unwrap();
let total = rt.count_entities(&tok, None).await.unwrap();
assert_eq!(concept_count, 3);
assert_eq!(doc_count, 2);
assert_eq!(total, 5);
}
#[tokio::test]
async fn link_and_neighbors() {
let rt = rt();
let tok = rt.authorize(Namespace::local()).unwrap();
let lora = rt
.create_entity(&tok, "concept", None, "LoRA", None, None, vec![])
.await
.unwrap();
let qlora = rt
.create_entity(&tok, "concept", None, "QLoRA", None, None, vec![])
.await
.unwrap();
rt.link(&tok, qlora.id, lora.id, EdgeRelation::VariantOf, 1.0, None)
.await
.unwrap();
let hits = rt
.neighbors(&tok, qlora.id, Direction::Out, None, None)
.await
.unwrap();
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].node_id, lora.id);
assert_eq!(hits[0].relation, EdgeRelation::VariantOf);
}
#[tokio::test]
async fn traverse_multi_hop() {
let rt = rt();
let tok = rt.authorize(Namespace::local()).unwrap();
let a = rt
.create_entity(&tok, "concept", None, "A", None, None, vec![])
.await
.unwrap();
let b = rt
.create_entity(&tok, "concept", None, "B", None, None, vec![])
.await
.unwrap();
let c = rt
.create_entity(&tok, "concept", None, "C", None, None, vec![])
.await
.unwrap();
rt.link(&tok, a.id, b.id, EdgeRelation::Extends, 1.0, None)
.await
.unwrap();
rt.link(&tok, b.id, c.id, EdgeRelation::Extends, 1.0, None)
.await
.unwrap();
let request = TraversalRequest {
roots: vec![a.id],
options: TraversalOptions {
max_depth: 2,
direction: Direction::Out,
relations: Some(vec![EdgeRelation::Extends]),
..Default::default()
},
include_roots: false,
};
let paths = rt.traverse(&tok, request).await.unwrap();
assert!(!paths.is_empty());
let reachable_ids: Vec<Uuid> = paths
.iter()
.flat_map(|p| p.nodes.iter().map(|n| n.node_id))
.collect();
assert!(reachable_ids.contains(&b.id));
assert!(reachable_ids.contains(&c.id));
}
#[tokio::test]
async fn create_note_and_list_notes() {
let rt = rt();
let tok = rt.authorize(Namespace::local()).unwrap();
rt.create_note(
&tok,
"observation",
None,
"LoRA is a fine-tuning technique",
Some(0.9),
None,
vec![],
)
.await
.unwrap();
rt.create_note(
&tok,
"observation",
None,
"QLoRA uses quantization",
Some(0.8),
None,
vec![],
)
.await
.unwrap();
rt.create_note(
&tok,
"question",
None,
"Review LoRA paper",
Some(0.7),
None,
vec![],
)
.await
.unwrap();
let observations = rt
.list_notes(&tok, Some("observation"), 50, 0)
.await
.unwrap();
assert_eq!(observations.len(), 2);
let questions = rt.list_notes(&tok, Some("question"), 50, 0).await.unwrap();
assert_eq!(questions.len(), 1);
assert_eq!(questions[0].content, "Review LoRA paper");
let all = rt.list_notes(&tok, None, 50, 0).await.unwrap();
assert_eq!(all.len(), 3);
}
#[tokio::test]
async fn create_all_note_kinds() {
let rt = rt();
let tok = rt.authorize(Namespace::local()).unwrap();
for kind in [
"observation",
"insight",
"question",
"decision",
"reference",
] {
rt.create_note(&tok, kind, None, "content", Some(0.5), None, vec![])
.await
.unwrap();
}
let all = rt.list_notes(&tok, None, 50, 0).await.unwrap();
assert_eq!(all.len(), 5);
}
#[tokio::test]
async fn query_via_gql() {
let rt = rt();
let tok = rt.authorize(Namespace::local()).unwrap();
let lora = rt
.create_entity(&tok, "concept", None, "LoRA", None, None, vec![])
.await
.unwrap();
let qlora = rt
.create_entity(&tok, "concept", None, "QLoRA", None, None, vec![])
.await
.unwrap();
rt.link(&tok, qlora.id, lora.id, EdgeRelation::VariantOf, 1.0, None)
.await
.unwrap();
let rows = rt
.query(
&tok,
"MATCH (a:concept)-[e:variant_of]->(b:concept) RETURN a, e, b LIMIT 10",
)
.await
.unwrap();
assert_eq!(rows.len(), 1);
let first_row = &rows[0];
assert!(first_row.get("a_name").is_some() || first_row.get("a_kind").is_some());
}
#[tokio::test]
async fn namespace_isolation() {
let rt = rt();
let ns_a_tok = rt.authorize(Namespace::parse("ns-a").unwrap()).unwrap();
let ns_b_tok = rt.authorize(Namespace::parse("ns-b").unwrap()).unwrap();
rt.create_entity(&ns_a_tok, "concept", None, "EntityA", None, None, vec![])
.await
.unwrap();
rt.create_entity(&ns_b_tok, "concept", None, "EntityB", None, None, vec![])
.await
.unwrap();
let a_entities = rt
.list_entities(&ns_a_tok, None, None, 50, 0)
.await
.unwrap();
assert_eq!(a_entities.len(), 1);
assert_eq!(a_entities[0].name, "EntityA");
let b_entities = rt
.list_entities(&ns_b_tok, None, None, 50, 0)
.await
.unwrap();
assert_eq!(b_entities.len(), 1);
assert_eq!(b_entities[0].name, "EntityB");
}
#[tokio::test]
async fn create_entity_indexes_into_text_search() {
let rt = KhiveRuntime::memory().expect("in-memory runtime");
let tok = rt.authorize(Namespace::local()).unwrap();
let entity = rt
.create_entity(
&tok,
"concept",
None,
"FlashAttention",
Some("efficient attention mechanism"),
None,
vec![],
)
.await
.unwrap();
let hits = rt
.hybrid_search(&tok, "FlashAttention", None, 10, None, None)
.await
.unwrap();
assert!(
hits.iter().any(|h| h.entity_id == entity.id),
"newly created entity should be findable via hybrid_search (text path)"
);
}
#[tokio::test]
async fn create_entity_no_embedding_model_does_not_propagate_vector_error() {
let rt = KhiveRuntime::memory().expect("in-memory runtime");
let tok = rt.authorize(Namespace::local()).unwrap();
let result = rt
.create_entity(
&tok,
"concept",
None,
"SilentVectorSkip",
None,
None,
vec![],
)
.await;
assert!(
result.is_ok(),
"create_entity must not propagate Unconfigured from vector store"
);
}
#[tokio::test]
async fn hybrid_search_excludes_soft_deleted_entities() {
let rt = KhiveRuntime::memory().expect("in-memory runtime");
let tok = rt.authorize(Namespace::local()).unwrap();
let entity = rt
.create_entity(
&tok,
"concept",
None,
"SoftDeleteMe",
Some("entity that will be soft-deleted"),
None,
vec![],
)
.await
.unwrap();
let hits_before = rt
.hybrid_search(&tok, "SoftDeleteMe", None, 10, None, None)
.await
.unwrap();
assert!(
hits_before.iter().any(|h| h.entity_id == entity.id),
"entity should appear in hybrid_search before soft-delete"
);
rt.delete_entity(&tok, entity.id, false).await.unwrap();
let hits_after = rt
.hybrid_search(&tok, "SoftDeleteMe", None, 10, None, None)
.await
.unwrap();
assert!(
!hits_after.iter().any(|h| h.entity_id == entity.id),
"soft-deleted entity must not appear in hybrid_search"
);
}
#[tokio::test]
async fn hybrid_search_excludes_hard_deleted_entities() {
let rt = KhiveRuntime::memory().expect("in-memory runtime");
let tok = rt.authorize(Namespace::local()).unwrap();
let entity = rt
.create_entity(
&tok,
"concept",
None,
"HardDeleteMe",
Some("entity that will be hard-deleted"),
None,
vec![],
)
.await
.unwrap();
let hits_before = rt
.hybrid_search(&tok, "HardDeleteMe", None, 10, None, None)
.await
.unwrap();
assert!(
hits_before.iter().any(|h| h.entity_id == entity.id),
"entity should appear in hybrid_search before hard-delete"
);
rt.delete_entity(&tok, entity.id, true).await.unwrap();
let hits_after = rt
.hybrid_search(&tok, "HardDeleteMe", None, 10, None, None)
.await
.unwrap();
assert!(
!hits_after.iter().any(|h| h.entity_id == entity.id),
"hard-deleted entity must not appear in hybrid_search"
);
}
#[tokio::test]
async fn list_notes_excludes_soft_deleted() {
use khive_storage::types::DeleteMode;
let rt = KhiveRuntime::memory().expect("in-memory runtime");
let tok = rt.authorize(Namespace::local()).unwrap();
let note = rt
.create_note(
&tok,
"observation",
None,
"soft-delete-test",
Some(0.9),
None,
vec![],
)
.await
.unwrap();
let notes_before = rt.list_notes(&tok, None, 50, 0).await.unwrap();
assert!(
notes_before.iter().any(|n| n.id == note.id),
"note should appear before soft-delete"
);
rt.notes(&tok)
.unwrap()
.delete_note(note.id, DeleteMode::Soft)
.await
.unwrap();
let notes_after = rt.list_notes(&tok, None, 50, 0).await.unwrap();
assert!(
!notes_after.iter().any(|n| n.id == note.id),
"soft-deleted note must not appear in list"
);
}
#[tokio::test]
async fn file_backed_runtime_persists() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("persist.db");
{
let config = RuntimeConfig {
db_path: Some(path.clone()),
default_namespace: Namespace::local(),
embedding_model: None,
gate: std::sync::Arc::new(khive_runtime::AllowAllGate),
packs: vec!["kg".to_string()],
backend_id: khive_runtime::BackendId::main(),
additional_embedding_models: vec![],
};
let rt = KhiveRuntime::new(config).unwrap();
let tok = rt.authorize(Namespace::local()).unwrap();
rt.create_entity(&tok, "concept", None, "Persistent", None, None, vec![])
.await
.unwrap();
}
{
let config = RuntimeConfig {
db_path: Some(path.clone()),
default_namespace: Namespace::local(),
embedding_model: None,
gate: std::sync::Arc::new(khive_runtime::AllowAllGate),
packs: vec!["kg".to_string()],
backend_id: khive_runtime::BackendId::main(),
additional_embedding_models: vec![],
};
let rt = KhiveRuntime::new(config).unwrap();
let tok = rt.authorize(Namespace::local()).unwrap();
let entities = rt.list_entities(&tok, None, None, 50, 0).await.unwrap();
assert_eq!(entities.len(), 1);
assert_eq!(entities[0].name, "Persistent");
}
}
#[tokio::test]
async fn synthetic_edge_observed_as_selected_returns_memory_note() {
let rt = rt();
let tok = rt.authorize(Namespace::local()).unwrap();
let ns = "local";
let memory_note = rt
.create_note(
&tok,
"memory",
None,
"recalled memory content",
Some(0.9),
None,
vec![],
)
.await
.unwrap();
let memory_id = memory_note.id;
let event_store = rt.events(&tok).unwrap();
let mut event = Event::new(
ns,
"rerank",
EventKind::RerankExecuted,
SubstrateKind::Note,
"agent:test",
);
event.payload = serde_json::json!({
"candidates": [],
"selected": [memory_id.to_string()]
});
event_store.append_event(event).await.unwrap();
let rows = rt
.query(
&tok,
"MATCH (ev)-[:observed_as_selected]->(m:memory) RETURN m",
)
.await
.unwrap();
assert!(
!rows.is_empty(),
"CRIT-1: synthetic edge query must return at least one row (memory note was seeded); \
got 0 rows — event_observations join is broken"
);
let memory_id_str = memory_id.to_string();
let found = rows.iter().any(|row| {
row.columns.iter().any(|col| {
if let khive_storage::types::SqlValue::Text(s) = &col.value {
s.contains(&memory_id_str)
} else {
false
}
})
});
assert!(
found,
"CRIT-1: returned rows must include the seeded memory note id {}; columns: {:?}",
memory_id,
rows.iter()
.map(|r| r
.columns
.iter()
.map(|c| (&c.name, &c.value))
.collect::<Vec<_>>())
.collect::<Vec<_>>()
);
}
#[tokio::test]
async fn update_edge_returns_surviving_canonical_id_on_conflict() {
use khive_runtime::EdgePatch;
let rt = rt();
let tok = rt.authorize(Namespace::local()).unwrap();
let a = rt
.create_entity(&tok, "concept", None, "SurvA", None, None, vec![])
.await
.unwrap();
let b = rt
.create_entity(&tok, "concept", None, "SurvB", None, None, vec![])
.await
.unwrap();
let e1 = rt
.link(&tok, a.id, b.id, EdgeRelation::CompetesWith, 1.0, None)
.await
.unwrap();
let (src, tgt) = if a.id > b.id {
(a.id, b.id)
} else {
(b.id, a.id)
};
let e2 = rt
.link(&tok, src, tgt, EdgeRelation::Extends, 0.5, None)
.await
.unwrap();
assert_ne!(
e1.id, e2.id,
"pre-condition: E1 and E2 must be distinct edges"
);
let returned = rt
.update_edge(
&tok,
e2.id.into(),
EdgePatch {
relation: Some(EdgeRelation::CompetesWith),
weight: Some(0.9),
..Default::default()
},
)
.await
.expect("update_edge must succeed even when conflict is absorbed");
assert_eq!(
returned.id, e1.id,
"Bug 1: update_edge must return the SURVIVING canonical row id (E1={:?}), \
got E2={:?}",
e1.id, returned.id
);
let fetched = rt
.get_edge(&tok, returned.id.into())
.await
.expect("get_edge on returned id must not error")
.expect("get_edge on returned id must find a row (not 404)");
assert_eq!(
fetched.id, e1.id,
"fetched row id must match E1 (surviving canonical)"
);
let e2_lookup = rt
.get_edge(&tok, e2.id.into())
.await
.expect("get_edge on deleted id must not error");
assert!(
e2_lookup.is_none(),
"Bug 1: deleted edge E2 must not be findable after conflict absorption"
);
}
#[tokio::test]
async fn update_edge_canonical_orientation_conflict() {
use khive_runtime::EdgePatch;
let rt = rt();
let tok = rt.authorize(Namespace::local()).unwrap();
let a = rt
.create_entity(&tok, "concept", None, "CanOrA", None, None, vec![])
.await
.unwrap();
let b = rt
.create_entity(&tok, "concept", None, "CanOrB", None, None, vec![])
.await
.unwrap();
let (canon_lo, canon_hi) = if a.id < b.id {
(a.id, b.id)
} else {
(b.id, a.id)
};
let e1 = rt
.link(
&tok,
canon_lo,
canon_hi,
EdgeRelation::CompetesWith,
1.0,
None,
)
.await
.unwrap();
let e2 = rt
.link(&tok, canon_lo, canon_hi, EdgeRelation::Extends, 0.5, None)
.await
.unwrap();
assert_ne!(
e1.id, e2.id,
"pre-condition: E1 and E2 must be distinct edges"
);
rt.update_edge(
&tok,
e2.id.into(),
EdgePatch {
relation: Some(EdgeRelation::CompetesWith),
..Default::default()
},
)
.await
.expect("Bug 2: update_edge on canonical-orientation conflict must not error");
let edges = rt
.list_edges(
&tok,
khive_runtime::EdgeListFilter {
source_id: Some(canon_lo),
target_id: Some(canon_hi),
relations: vec![EdgeRelation::CompetesWith],
..Default::default()
},
100,
)
.await
.expect("list_edges must succeed");
assert_eq!(
edges.len(),
1,
"Bug 2: exactly one competes_with edge must exist after non-flipped conflict absorption; \
found {} edges: {edges:?}",
edges.len()
);
}
mod embedder_registry_tests {
use async_trait::async_trait;
use khive_gate::AllowAllGate;
use khive_runtime::{EmbedderProvider, KhiveRuntime, RuntimeConfig, RuntimeError};
use khive_types::Namespace;
use lattice_embed::{EmbeddingModel, EmbeddingService};
use std::sync::Arc;
struct MockEmbedderProvider {
name: String,
dims: usize,
}
impl MockEmbedderProvider {
fn new(name: &str, dims: usize) -> Self {
Self {
name: name.to_owned(),
dims,
}
}
}
struct MockEmbeddingService {
dims: usize,
}
#[async_trait]
impl EmbeddingService for MockEmbeddingService {
async fn embed(
&self,
texts: &[String],
_model: EmbeddingModel,
) -> Result<Vec<Vec<f32>>, lattice_embed::EmbedError> {
Ok(texts.iter().map(|_| vec![42.0_f32; self.dims]).collect())
}
fn supports_model(&self, _model: EmbeddingModel) -> bool {
true
}
fn name(&self) -> &'static str {
"mock-embedding-service"
}
}
#[async_trait]
impl EmbedderProvider for MockEmbedderProvider {
fn name(&self) -> &str {
&self.name
}
fn dimensions(&self) -> usize {
self.dims
}
async fn build(&self) -> Result<Arc<dyn EmbeddingService>, RuntimeError> {
Ok(Arc::new(MockEmbeddingService { dims: self.dims }))
}
}
fn memory_rt_no_model() -> KhiveRuntime {
KhiveRuntime::new(RuntimeConfig {
db_path: None,
default_namespace: Namespace::local(),
embedding_model: None,
additional_embedding_models: vec![],
gate: Arc::new(AllowAllGate),
packs: vec!["kg".to_string()],
backend_id: khive_runtime::BackendId::main(),
})
.expect("in-memory runtime")
}
#[tokio::test]
async fn register_embedder_and_retrieve_via_embedder_method() {
let rt = memory_rt_no_model();
rt.register_embedder(MockEmbedderProvider::new("mock", 384));
let service = rt
.embedder("mock")
.await
.expect("embedder lookup must succeed after registration");
let texts = vec!["hello world".to_string()];
let vecs = service
.embed(&texts, EmbeddingModel::AllMiniLmL6V2)
.await
.expect("mock service must embed successfully");
assert_eq!(vecs.len(), 1);
assert_eq!(vecs[0].len(), 384);
assert!(
vecs[0].iter().all(|&v| (v - 42.0_f32).abs() < 1e-6),
"mock service must return constant 42.0 vector"
);
}
#[tokio::test]
async fn registered_names_includes_custom_provider() {
let rt = memory_rt_no_model();
rt.register_embedder(MockEmbedderProvider::new("my-encoder", 128));
let names = rt.registered_embedding_model_names();
assert!(
names.contains(&"my-encoder".to_string()),
"registered_embedding_model_names must include custom provider 'my-encoder'; got {names:?}"
);
}
#[tokio::test]
async fn dual_embedding_regression_both_models_registered() {
use khive_runtime::RuntimeConfig;
let rt = KhiveRuntime::new(RuntimeConfig {
db_path: None,
default_namespace: Namespace::local(),
embedding_model: Some(EmbeddingModel::AllMiniLmL6V2),
additional_embedding_models: vec![EmbeddingModel::ParaphraseMultilingualMiniLmL12V2],
gate: Arc::new(AllowAllGate),
packs: vec!["kg".to_string()],
backend_id: khive_runtime::BackendId::main(),
})
.expect("runtime with two models");
let names = rt.registered_embedding_model_names();
assert!(
names.contains(&"all-minilm-l6-v2".to_string()),
"MiniLM must be registered; names: {names:?}"
);
assert!(
names.contains(&"paraphrase-multilingual-minilm-l12-v2".to_string()),
"paraphrase must be registered; names: {names:?}"
);
rt.resolve_embedding_model(Some("all-minilm-l6-v2"))
.expect("MiniLM must resolve");
rt.resolve_embedding_model(Some("paraphrase"))
.expect("paraphrase alias must resolve");
}
#[tokio::test]
async fn embedder_unknown_name_returns_error() {
let rt = memory_rt_no_model();
let err = rt
.embedder("no-such-model")
.await
.err()
.expect("expected Err for unknown embedder name, got Ok");
assert!(
matches!(err, RuntimeError::UnknownModel(ref n) if n == "no-such-model"),
"expected UnknownModel for unregistered name; got {err:?}"
);
}
#[tokio::test]
async fn pack_register_embedders_hook_makes_provider_reachable() {
use async_trait::async_trait;
use khive_runtime::pack::HandlerDef;
use khive_runtime::NamespaceToken;
use khive_runtime::{PackRuntime, VerbRegistry, VerbRegistryBuilder};
use khive_types::Pack;
use serde_json::Value;
struct EmbedderPack;
impl Pack for EmbedderPack {
const NAME: &'static str = "embedder-test-pack";
const NOTE_KINDS: &'static [&'static str] = &[];
const ENTITY_KINDS: &'static [&'static str] = &[];
const HANDLERS: &'static [HandlerDef] = &[];
}
#[async_trait]
impl PackRuntime for EmbedderPack {
fn name(&self) -> &str {
Self::NAME
}
fn note_kinds(&self) -> &'static [&'static str] {
Self::NOTE_KINDS
}
fn entity_kinds(&self) -> &'static [&'static str] {
Self::ENTITY_KINDS
}
fn handlers(&self) -> &'static [HandlerDef] {
Self::HANDLERS
}
fn register_embedders(&self, runtime: &KhiveRuntime) {
runtime.register_embedder(MockEmbedderProvider::new("pack-custom-encoder", 256));
}
async fn dispatch(
&self,
_verb: &str,
_params: Value,
_registry: &VerbRegistry,
_token: &NamespaceToken,
) -> Result<Value, khive_runtime::RuntimeError> {
Ok(Value::Null)
}
}
let rt = memory_rt_no_model();
let mut builder = VerbRegistryBuilder::new();
builder.register(EmbedderPack);
let registry = builder.build().expect("registry builds");
registry.call_register_embedders(&rt);
let service = rt
.embedder("pack-custom-encoder")
.await
.expect("pack-contributed provider must be reachable after call_register_embedders");
let texts = vec!["test sentence".to_string()];
let vecs = service
.embed(&texts, EmbeddingModel::AllMiniLmL6V2)
.await
.expect("custom service must embed without error");
assert_eq!(vecs.len(), 1);
assert_eq!(
vecs[0].len(),
256,
"dims must match provider declaration (256)"
);
}
#[tokio::test]
async fn failing_provider_build_returns_err_not_panic() {
struct FailingProvider;
#[async_trait]
impl EmbedderProvider for FailingProvider {
fn name(&self) -> &str {
"failing-provider"
}
fn dimensions(&self) -> usize {
128
}
async fn build(&self) -> Result<Arc<dyn EmbeddingService>, RuntimeError> {
Err(RuntimeError::Internal(
"simulated provider construction failure".into(),
))
}
}
let rt = memory_rt_no_model();
rt.register_embedder(FailingProvider);
let result = rt.embedder("failing-provider").await;
assert!(
result.is_err(),
"embedder() must return Err when build() fails, not panic; got Ok"
);
let err = result.err().expect("checked above");
let msg = err.to_string();
assert!(
msg.contains("simulated provider construction failure")
|| msg.contains("build() failed")
|| msg.contains("Internal"),
"error must carry build failure context; got: {msg}"
);
}
}