1use std::collections::{HashMap, HashSet, VecDeque};
14use std::sync::Arc;
15use std::time::Instant;
16
17use crate::runtime::NamespaceToken;
18use async_trait::async_trait;
19use khive_gate::{ActorRef, AllowAllGate, AuditEvent, GateDecision, GateRef, GateRequest};
20use khive_storage::{Event, EventStore, EventView, SubstrateKind};
21use khive_types::{EventKind, EventOutcome, Namespace};
22use serde_json::Value;
23
24pub use khive_types::{
25 EdgeEndpointRule, EndpointKind, HandlerDef, NoteKindSpec, NoteLifecycleSpec, PackSchemaPlan,
26 ParamDef, VerbCategory, VerbPresentationPolicy, Visibility,
27};
28#[allow(deprecated)]
30pub use khive_types::VerbDef;
31
32use crate::validation::ValidationRule;
33
34#[derive(Debug, Default, Clone)]
44pub struct SchemaPlan {
45 pub pack: &'static str,
47 pub statements: &'static [&'static str],
51}
52
53impl SchemaPlan {
54 pub const fn empty() -> Self {
59 Self {
60 pack: "",
61 statements: &[],
62 }
63 }
64
65 pub fn is_empty(&self) -> bool {
67 self.statements.is_empty()
68 }
69}
70
71#[async_trait]
76pub trait DispatchHook: Send + Sync {
77 async fn on_dispatch(&self, view: &EventView);
82}
83
84use crate::error::{
85 CircularPackDependency, MissingPackDependencies, MissingPackDependency, RuntimeError,
86};
87use crate::KhiveRuntime;
88
89#[async_trait]
98pub trait PackRuntime: Send + Sync {
99 fn name(&self) -> &str;
101
102 fn note_kinds(&self) -> &'static [&'static str];
104
105 fn entity_kinds(&self) -> &'static [&'static str];
107
108 fn handlers(&self) -> &'static [HandlerDef];
110
111 fn edge_rules(&self) -> &'static [EdgeEndpointRule] {
115 &[]
116 }
117
118 fn requires(&self) -> &'static [&'static str] {
121 &[]
122 }
123
124 fn note_kind_specs(&self) -> &'static [NoteKindSpec] {
131 &[]
132 }
133
134 fn kind_hook(&self, _kind: &str) -> Option<Arc<dyn KindHook>> {
142 None
143 }
144
145 fn schema_plan(&self) -> SchemaPlan {
161 SchemaPlan::empty()
162 }
163
164 fn validation_rules(&self) -> &'static [ValidationRule] {
172 &[]
173 }
174
175 fn register_embedders(&self, _runtime: &KhiveRuntime) {}
192
193 fn register_entity_type_validator(&self, _runtime: &KhiveRuntime) {}
205
206 async fn warm(&self) {}
217
218 async fn dispatch(
225 &self,
226 verb: &str,
227 params: Value,
228 registry: &VerbRegistry,
229 token: &NamespaceToken,
230 ) -> Result<Value, RuntimeError>;
231}
232
233#[async_trait]
247pub trait KindHook: Send + Sync + std::fmt::Debug {
248 async fn prepare_create(
254 &self,
255 runtime: &KhiveRuntime,
256 args: &mut Value,
257 ) -> Result<(), RuntimeError>;
258
259 async fn after_create(
268 &self,
269 runtime: &KhiveRuntime,
270 id: uuid::Uuid,
271 args: &Value,
272 ) -> Result<(), RuntimeError>;
273}
274
275#[async_trait]
283pub trait PackByIdResolver: Send + Sync {
284 async fn resolve_by_id(
294 &self,
295 id: uuid::Uuid,
296 ) -> Result<Option<crate::Resolved>, crate::RuntimeError>;
297
298 async fn resolve_by_id_including_deleted(
303 &self,
304 id: uuid::Uuid,
305 ) -> Result<Option<crate::Resolved>, crate::RuntimeError> {
306 self.resolve_by_id(id).await
307 }
308
309 async fn delete_by_id(
318 &self,
319 id: uuid::Uuid,
320 hard: bool,
321 ) -> Result<serde_json::Value, crate::RuntimeError>;
322}
323
324pub struct VerbRegistryBuilder {
329 packs: Vec<Box<dyn PackRuntime>>,
330 resolvers: Vec<(String, Box<dyn PackByIdResolver>)>,
331 gate: GateRef,
332 default_namespace: String,
333 visible_namespaces: Vec<Namespace>,
341 actor_id: Option<String>,
344 event_store: Option<Arc<dyn EventStore>>,
351 dispatch_hook: Option<Arc<dyn DispatchHook>>,
357}
358
359impl VerbRegistryBuilder {
360 pub fn new() -> Self {
362 Self {
363 packs: Vec::new(),
364 resolvers: Vec::new(),
365 gate: std::sync::Arc::new(AllowAllGate),
366 default_namespace: Namespace::local().as_str().to_string(),
367 visible_namespaces: vec![],
368 actor_id: None,
369 event_store: None,
370 dispatch_hook: None,
371 }
372 }
373
374 pub fn with_visible_namespaces(&mut self, ns: Vec<Namespace>) -> &mut Self {
382 self.visible_namespaces = ns;
383 self
384 }
385
386 pub fn with_actor_id(&mut self, actor_id: Option<String>) -> &mut Self {
393 self.actor_id = actor_id;
394 self
395 }
396
397 pub fn register<P: khive_types::Pack + PackRuntime + 'static>(&mut self, pack: P) -> &mut Self {
400 self.packs.push(Box::new(pack));
401 self
402 }
403
404 pub(crate) fn register_boxed(&mut self, pack: Box<dyn PackRuntime>) -> &mut Self {
411 self.packs.push(pack);
412 self
413 }
414
415 pub fn register_resolver(
420 &mut self,
421 name: impl Into<String>,
422 resolver: Box<dyn PackByIdResolver>,
423 ) -> &mut Self {
424 self.resolvers.push((name.into(), resolver));
425 self
426 }
427
428 pub fn with_gate(&mut self, gate: GateRef) -> &mut Self {
435 self.gate = gate;
436 self
437 }
438
439 pub fn with_default_namespace(&mut self, ns: impl Into<String>) -> &mut Self {
444 self.default_namespace = ns.into();
445 self
446 }
447
448 pub fn with_event_store(&mut self, store: Arc<dyn EventStore>) -> &mut Self {
457 self.event_store = Some(store);
458 self
459 }
460
461 pub fn with_dispatch_hook(&mut self, hook: Arc<dyn DispatchHook>) -> &mut Self {
472 self.dispatch_hook = Some(hook);
473 self
474 }
475
476 pub fn build(self) -> Result<VerbRegistry, RuntimeError> {
482 let packs = self.packs;
483 let mut name_to_idx: HashMap<&str, usize> = HashMap::with_capacity(packs.len());
484 for (idx, pack) in packs.iter().enumerate() {
485 if let Some(prev_idx) = name_to_idx.insert(pack.name(), idx) {
486 return Err(RuntimeError::PackRedeclared {
487 name: pack.name().to_string(),
488 first_idx: prev_idx,
489 second_idx: idx,
490 });
491 }
492 }
493
494 let mut missing: Vec<MissingPackDependency> = Vec::new();
495 let mut indegree = vec![0usize; packs.len()];
496 let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); packs.len()];
497
498 for (idx, pack) in packs.iter().enumerate() {
499 for &requires in pack.requires() {
500 match name_to_idx.get(requires).copied() {
501 Some(dep_idx) => {
502 dependents[dep_idx].push(idx);
503 indegree[idx] += 1;
504 }
505 None => missing.push(MissingPackDependency {
506 from: pack.name().to_string(),
507 requires: requires.to_string(),
508 }),
509 }
510 }
511 }
512
513 if !missing.is_empty() {
514 return if missing.len() == 1 {
515 Err(RuntimeError::MissingPackDependency(missing.remove(0)))
516 } else {
517 Err(RuntimeError::MissingPackDependencies(
518 MissingPackDependencies { missing },
519 ))
520 };
521 }
522
523 let mut ready: VecDeque<usize> = indegree
524 .iter()
525 .enumerate()
526 .filter_map(|(idx, degree)| (*degree == 0).then_some(idx))
527 .collect();
528 let mut ordered_indices = Vec::with_capacity(packs.len());
529
530 while let Some(idx) = ready.pop_front() {
531 ordered_indices.push(idx);
532 for &dep_idx in &dependents[idx] {
533 indegree[dep_idx] -= 1;
534 if indegree[dep_idx] == 0 {
535 ready.push_back(dep_idx);
536 }
537 }
538 }
539
540 if ordered_indices.len() != packs.len() {
541 let cycle_nodes: HashSet<usize> = indegree
542 .iter()
543 .enumerate()
544 .filter_map(|(idx, degree)| (*degree > 0).then_some(idx))
545 .collect();
546 let cycle = find_pack_dependency_cycle(&packs, &name_to_idx, &cycle_nodes);
547 return Err(RuntimeError::CircularPackDependency(
548 CircularPackDependency { cycle },
549 ));
550 }
551
552 let mut slots: Vec<Option<Box<dyn PackRuntime>>> = packs.into_iter().map(Some).collect();
553 let ordered_packs: Vec<Box<dyn PackRuntime>> = ordered_indices
554 .into_iter()
555 .map(|idx| slots[idx].take().expect("topological index must exist"))
556 .collect();
557
558 validate_unique_note_kinds(&ordered_packs)?;
559 validate_unique_verb_names(&ordered_packs)?;
560
561 Ok(VerbRegistry {
562 packs: Arc::new(ordered_packs),
563 resolvers: Arc::new(self.resolvers),
564 gate: self.gate,
565 default_namespace: self.default_namespace,
566 visible_namespaces: self.visible_namespaces,
567 actor_id: self.actor_id,
568 event_store: self.event_store,
569 dispatch_hook: self.dispatch_hook,
570 })
571 }
572}
573
574fn validate_unique_note_kinds(packs: &[Box<dyn PackRuntime>]) -> Result<(), RuntimeError> {
580 let mut seen: HashMap<&str, &str> = HashMap::new();
581 for pack in packs {
582 for &kind in pack.note_kinds() {
583 if let Some(first_pack) = seen.insert(kind, pack.name()) {
584 return Err(RuntimeError::InvalidInput(format!(
585 "duplicate note kind {kind:?}: claimed by both {first_pack:?} and {:?}",
586 pack.name()
587 )));
588 }
589 }
590 }
591 Ok(())
592}
593
594fn validate_unique_verb_names(packs: &[Box<dyn PackRuntime>]) -> Result<(), RuntimeError> {
602 let mut seen: HashMap<&str, &str> = HashMap::new();
603 for pack in packs {
604 for handler in pack.handlers() {
605 if !matches!(handler.visibility, Visibility::Verb) {
606 continue;
607 }
608 if let Some(first_pack) = seen.insert(handler.name, pack.name()) {
609 return Err(RuntimeError::VerbCollision {
610 verb: handler.name.to_string(),
611 first_pack: first_pack.to_string(),
612 second_pack: pack.name().to_string(),
613 });
614 }
615 }
616 }
617 Ok(())
618}
619
620fn find_pack_dependency_cycle(
621 packs: &[Box<dyn PackRuntime>],
622 name_to_idx: &HashMap<&str, usize>,
623 cycle_nodes: &HashSet<usize>,
624) -> Vec<String> {
625 fn visit(
626 idx: usize,
627 packs: &[Box<dyn PackRuntime>],
628 name_to_idx: &HashMap<&str, usize>,
629 cycle_nodes: &HashSet<usize>,
630 visiting: &mut Vec<usize>,
631 visited: &mut HashSet<usize>,
632 ) -> Option<Vec<String>> {
633 if let Some(pos) = visiting.iter().position(|&seen| seen == idx) {
634 let mut cycle: Vec<String> = visiting[pos..]
635 .iter()
636 .map(|&i| packs[i].name().to_string())
637 .collect();
638 cycle.push(packs[idx].name().to_string());
639 return Some(cycle);
640 }
641 if !visited.insert(idx) {
642 return None;
643 }
644 visiting.push(idx);
645 for &req in packs[idx].requires() {
646 let Some(&dep_idx) = name_to_idx.get(req) else {
647 continue;
648 };
649 if cycle_nodes.contains(&dep_idx) {
650 if let Some(cycle) =
651 visit(dep_idx, packs, name_to_idx, cycle_nodes, visiting, visited)
652 {
653 return Some(cycle);
654 }
655 }
656 }
657 visiting.pop();
658 None
659 }
660
661 let mut visited = HashSet::new();
662 for &idx in cycle_nodes {
663 let mut visiting = Vec::new();
664 if let Some(cycle) = visit(
665 idx,
666 packs,
667 name_to_idx,
668 cycle_nodes,
669 &mut visiting,
670 &mut visited,
671 ) {
672 return cycle;
673 }
674 }
675 cycle_nodes
676 .iter()
677 .map(|&idx| packs[idx].name().to_string())
678 .collect()
679}
680
681impl Default for VerbRegistryBuilder {
682 fn default() -> Self {
683 Self::new()
684 }
685}
686
687#[derive(Clone)]
691pub struct VerbRegistry {
692 packs: std::sync::Arc<Vec<Box<dyn PackRuntime>>>,
693 resolvers: std::sync::Arc<Vec<(String, Box<dyn PackByIdResolver>)>>,
695 gate: GateRef,
696 default_namespace: String,
697 visible_namespaces: Vec<Namespace>,
704 actor_id: Option<String>,
708 event_store: Option<Arc<dyn EventStore>>,
710 dispatch_hook: Option<Arc<dyn DispatchHook>>,
712}
713
714#[derive(Debug)]
717pub struct PackSchemaCollisionError {
718 pub pack_a: &'static str,
720 pub pack_b: &'static str,
722 pub table: String,
724}
725
726impl std::fmt::Display for PackSchemaCollisionError {
727 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
728 if self.pack_a == self.pack_b {
729 write!(
730 f,
731 "pack schema boot failure for pack {:?}: {}",
732 self.pack_a, self.table
733 )
734 } else {
735 write!(
736 f,
737 "pack schema collision: packs {:?} and {:?} both declare table {:?} \
738 on the same backend — move one pack to a separate backend or rename the table",
739 self.pack_a, self.pack_b, self.table
740 )
741 }
742 }
743}
744
745impl std::error::Error for PackSchemaCollisionError {}
746
747fn extract_table_names(stmt: &str) -> Vec<String> {
753 let normalized = stmt.split_whitespace().collect::<Vec<_>>().join(" ");
754 let upper = normalized.to_ascii_uppercase();
755 let table_name = if let Some(rest) = upper.strip_prefix("CREATE VIRTUAL TABLE IF NOT EXISTS ") {
756 rest.split_whitespace().next()
757 } else if let Some(rest) = upper.strip_prefix("CREATE VIRTUAL TABLE ") {
758 rest.split_whitespace().next()
759 } else if let Some(rest) = upper.strip_prefix("CREATE TABLE IF NOT EXISTS ") {
760 rest.split_whitespace().next()
761 } else if let Some(rest) = upper.strip_prefix("CREATE TABLE ") {
762 rest.split_whitespace().next()
763 } else {
764 None
765 };
766 match table_name {
767 Some(name) => {
768 let clean = name.trim_matches(|c: char| c == '(' || c == ';');
769 if clean.is_empty() {
770 vec![]
771 } else {
772 vec![clean.to_ascii_lowercase()]
773 }
774 }
775 None => vec![],
776 }
777}
778
779impl VerbRegistry {
780 pub fn describe_verb(&self, verb: &str) -> Result<Value, RuntimeError> {
787 for pack in self.packs.iter() {
788 for handler in pack.handlers().iter() {
789 if handler.name == verb {
790 let category = format!("{:?}", handler.category);
791 let params_arr: Vec<Value> = handler
792 .params
793 .iter()
794 .map(|p| {
795 serde_json::json!({
796 "name": p.name,
797 "type": p.param_type,
798 "required": p.required,
799 "description": p.description,
800 })
801 })
802 .collect();
803 if matches!(handler.visibility, Visibility::Subhandler) {
808 return Ok(serde_json::json!({
809 "verb": verb,
810 "pack": pack.name(),
811 "description": handler.description,
812 "category": category,
813 "params": params_arr,
814 "visibility": "internal",
815 "callable_via_mcp": false,
816 "note": "This is an internal subhandler. Calling it via the MCP \
817 request surface returns permission denied. It can only be \
818 invoked by internal runtime callers.",
819 }));
820 }
821 return Ok(serde_json::json!({
822 "verb": verb,
823 "pack": pack.name(),
824 "description": handler.description,
825 "category": category,
826 "params": params_arr,
827 }));
828 }
829 }
830 }
831 let available: Vec<&str> = self
834 .packs
835 .iter()
836 .flat_map(|p| p.handlers().iter())
837 .filter(|h| matches!(h.visibility, Visibility::Verb))
838 .map(|h| h.name)
839 .collect();
840 Err(RuntimeError::InvalidInput(format!(
841 "unknown verb {verb:?}; available: {}",
842 available.join(", ")
843 )))
844 }
845
846 pub fn authorize_namespace(&self, ns: Namespace) -> Result<(), RuntimeError> {
854 let actor = match self.actor_id.as_deref() {
855 Some(id) if !id.trim().is_empty() => ActorRef::new("actor", id),
856 _ => ActorRef::anonymous(),
857 };
858 let req = GateRequest::new(actor, ns, "authorize", serde_json::Value::Null);
859 match self.gate.check(&req) {
860 Ok(decision) if decision.is_allow() => Ok(()),
861 Ok(GateDecision::Deny { reason }) => Err(RuntimeError::PermissionDenied {
862 verb: "authorize".to_string(),
863 reason,
864 }),
865 Ok(_) => Err(RuntimeError::PermissionDenied {
866 verb: "authorize".to_string(),
867 reason: "gate denied".to_string(),
868 }),
869 Err(e) => Err(RuntimeError::Internal(format!("gate error: {e}"))),
870 }
871 }
872
873 pub async fn dispatch(&self, verb: &str, params: Value) -> Result<Value, RuntimeError> {
879 if params.get("help").and_then(Value::as_bool) == Some(true) {
881 return self.describe_verb(verb);
882 }
883 let ns_str: String = params
886 .get("namespace")
887 .and_then(Value::as_str)
888 .map(str::to_string)
889 .unwrap_or_else(|| self.default_namespace.clone());
890 let ns = Namespace::parse(&ns_str)
891 .map_err(|e| RuntimeError::InvalidInput(format!("invalid namespace: {e}")))?;
892 let gate_actor = match self.actor_id.as_deref() {
896 Some(id) if !id.trim().is_empty() => ActorRef::new("actor", id),
897 _ => ActorRef::anonymous(),
898 };
899 let gate_req = GateRequest::new(gate_actor, ns, verb, params.clone());
900
901 let gate_blocked = match self.gate.check(&gate_req) {
907 Ok(decision) => {
908 let is_deny = matches!(decision, GateDecision::Deny { .. });
909
910 let audit = AuditEvent::from_check(&gate_req, &decision, self.gate.impl_name());
912 tracing::info!(
913 audit_event = %serde_json::to_string(&audit)
914 .unwrap_or_else(|_| "{\"error\":\"serialize\"}".into()),
915 "gate.check"
916 );
917
918 if let Some(store) = &self.event_store {
920 let outcome = if is_deny {
921 EventOutcome::Denied
922 } else {
923 EventOutcome::Success
924 };
925 let audit_data = serde_json::to_value(&audit).unwrap_or_else(|e| {
926 tracing::warn!(error = %e, "failed to serialize AuditEvent for EventStore");
927 serde_json::Value::Null
928 });
929 let mut storage_event = Event::new(
930 gate_req.namespace.as_str(),
931 verb,
932 EventKind::Audit,
933 SubstrateKind::Event,
934 format!("{}:{}", gate_req.actor.kind, gate_req.actor.id),
935 )
936 .with_outcome(outcome)
937 .with_payload(audit_data);
938 if let Some(target_id) = target_id_from_args(&gate_req.args) {
939 storage_event = storage_event.with_target(target_id);
940 }
941 if let Err(store_err) = store.append_event(storage_event).await {
942 tracing::warn!(
943 verb,
944 error = %store_err,
945 "audit event store write failed (non-fatal)"
946 );
947 }
948 }
949
950 if is_deny {
951 let reason = match decision {
952 GateDecision::Deny { reason } => reason,
953 _ => String::new(),
954 };
955 Some(reason)
956 } else {
957 None
958 }
959 }
960 Err(err) => {
961 tracing::warn!(verb, error = %err, "gate check failed (fail-open)");
964 None
965 }
966 };
967
968 if let Some(reason) = gate_blocked {
970 return Err(RuntimeError::PermissionDenied {
971 verb: verb.to_string(),
972 reason,
973 });
974 }
975
976 let configured_actor = match self.actor_id.as_deref() {
988 Some(id) if !id.trim().is_empty() => ActorRef::new("actor", id),
989 _ => ActorRef::anonymous(),
990 };
991
992 let token = match params.get("namespace").and_then(Value::as_str) {
998 Some(ns) => {
999 let primary = Namespace::parse(ns)
1001 .map_err(|e| RuntimeError::InvalidInput(format!("invalid namespace: {e}")))?;
1002 NamespaceToken::mint_with_visibility(primary, vec![], configured_actor)
1003 }
1004 None => {
1005 let primary = Namespace::local();
1007 let mut extra_visible = self.visible_namespaces.clone();
1008 extra_visible.push(Namespace::local()); NamespaceToken::mint_with_visibility(primary, extra_visible, configured_actor)
1010 }
1011 };
1012
1013 for pack in self.packs.iter() {
1014 if let Some(handler_def) = pack.handlers().iter().find(|v| v.name == verb) {
1015 let handler_accepts_namespace =
1025 handler_def.params.iter().any(|p| p.name == "namespace");
1026 let params = if !handler_accepts_namespace {
1027 if let Value::Object(mut map) = params {
1028 map.remove("namespace");
1029 Value::Object(map)
1030 } else {
1031 params
1032 }
1033 } else {
1034 params
1035 };
1036 let dispatch_start = Instant::now();
1037 let result = pack.dispatch(verb, params, self, &token).await;
1038 let dispatch_us = dispatch_start.elapsed().as_micros() as i64;
1039
1040 if let (Ok(ref ok_val), Some(hook)) = (&result, &self.dispatch_hook) {
1042 let mut dispatch_event = Event::new(
1043 ns_str.as_str(),
1044 verb,
1045 EventKind::Audit,
1046 SubstrateKind::Event,
1047 pack.name(),
1048 )
1049 .with_outcome(EventOutcome::Success)
1050 .with_duration_us(dispatch_us);
1051
1052 if verb == "memory.recall" {
1056 let first_note_id = ok_val
1057 .as_array()
1058 .and_then(|arr| arr.first())
1059 .and_then(|v| v.get("id"))
1060 .and_then(|v| v.as_str())
1061 .and_then(|s| s.parse::<uuid::Uuid>().ok());
1062 if let Some(note_id) = first_note_id {
1063 dispatch_event = dispatch_event.with_target(note_id);
1064 }
1065 }
1068
1069 let dispatch_view = EventView {
1070 event: dispatch_event,
1071 observations: Vec::new(),
1072 };
1073 let hook = Arc::clone(hook);
1074 hook.on_dispatch(&dispatch_view).await;
1075 }
1076
1077 return result;
1078 }
1079 }
1080 let available: Vec<&str> = self
1083 .packs
1084 .iter()
1085 .flat_map(|p| p.handlers().iter())
1086 .filter(|h| matches!(h.visibility, Visibility::Verb))
1087 .map(|h| h.name)
1088 .collect();
1089 Err(RuntimeError::InvalidInput(format!(
1090 "unknown verb {verb:?}; available: {}",
1091 available.join(", ")
1092 )))
1093 }
1094
1095 pub fn resolvers(&self) -> &[(String, Box<dyn PackByIdResolver>)] {
1101 &self.resolvers
1102 }
1103
1104 pub fn find_kind_hook(&self, kind: &str) -> Option<Arc<dyn KindHook>> {
1111 for pack in self.packs.iter() {
1112 let owns = pack.note_kinds().contains(&kind) || pack.entity_kinds().contains(&kind);
1113 if owns {
1114 if let Some(hook) = pack.kind_hook(kind) {
1115 return Some(hook);
1116 }
1117 }
1118 }
1119 None
1120 }
1121
1122 pub fn all_verbs(&self) -> Vec<&'static HandlerDef> {
1128 self.packs
1129 .iter()
1130 .flat_map(|p| p.handlers().iter())
1131 .filter(|h| matches!(h.visibility, Visibility::Verb))
1132 .collect()
1133 }
1134
1135 pub fn all_verbs_with_names(&self) -> Vec<(&str, &'static HandlerDef)> {
1142 self.packs
1143 .iter()
1144 .flat_map(|p| p.handlers().iter().map(move |v| (p.name(), v)))
1145 .filter(|(_, h)| matches!(h.visibility, Visibility::Verb))
1146 .collect()
1147 }
1148
1149 pub fn all_handlers_with_names(&self) -> Vec<(&str, &'static HandlerDef)> {
1155 self.packs
1156 .iter()
1157 .flat_map(|p| p.handlers().iter().map(move |v| (p.name(), v)))
1158 .collect()
1159 }
1160
1161 pub fn all_note_kinds(&self) -> Vec<&'static str> {
1164 let mut seen = std::collections::HashSet::new();
1165 self.packs
1166 .iter()
1167 .flat_map(|p| p.note_kinds().iter().copied())
1168 .filter(|k| seen.insert(*k))
1169 .collect()
1170 }
1171
1172 pub fn all_entity_kinds(&self) -> Vec<&'static str> {
1175 let mut seen = std::collections::HashSet::new();
1176 self.packs
1177 .iter()
1178 .flat_map(|p| p.entity_kinds().iter().copied())
1179 .filter(|k| seen.insert(*k))
1180 .collect()
1181 }
1182
1183 pub fn pack_names(&self) -> Vec<&str> {
1185 self.packs.iter().map(|p| p.name()).collect()
1186 }
1187
1188 pub fn pack_requires(&self, name: &str) -> Option<&'static [&'static str]> {
1190 self.packs
1191 .iter()
1192 .find(|p| p.name() == name)
1193 .map(|p| p.requires())
1194 }
1195
1196 pub fn pack_note_kinds(&self, name: &str) -> Option<&'static [&'static str]> {
1201 self.packs
1202 .iter()
1203 .find(|p| p.name() == name)
1204 .map(|p| p.note_kinds())
1205 }
1206
1207 pub fn pack_entity_kinds(&self, name: &str) -> Option<&'static [&'static str]> {
1212 self.packs
1213 .iter()
1214 .find(|p| p.name() == name)
1215 .map(|p| p.entity_kinds())
1216 }
1217
1218 pub fn pack_verbs(&self, name: &str) -> Option<&'static [HandlerDef]> {
1223 self.packs
1224 .iter()
1225 .find(|p| p.name() == name)
1226 .map(|p| p.handlers())
1227 }
1228
1229 pub fn all_edge_rules(&self) -> Vec<EdgeEndpointRule> {
1235 self.packs
1236 .iter()
1237 .flat_map(|p| p.edge_rules().iter().copied())
1238 .collect()
1239 }
1240
1241 pub fn all_note_kind_specs(&self) -> Vec<&'static NoteKindSpec> {
1245 self.packs
1246 .iter()
1247 .flat_map(|p| p.note_kind_specs().iter())
1248 .collect()
1249 }
1250
1251 pub fn all_validation_rules(&self) -> Vec<&'static ValidationRule> {
1257 self.packs
1258 .iter()
1259 .flat_map(|p| p.validation_rules().iter())
1260 .collect()
1261 }
1262
1263 pub fn all_schema_plans(&self) -> Vec<SchemaPlan> {
1270 self.packs.iter().map(|p| p.schema_plan()).collect()
1271 }
1272
1273 pub fn call_register_embedders(&self, runtime: &KhiveRuntime) {
1283 for pack in self.packs.iter() {
1284 pack.register_embedders(runtime);
1285 }
1286 }
1287
1288 pub fn call_register_entity_type_validators(&self, runtime: &KhiveRuntime) {
1298 for pack in self.packs.iter() {
1299 pack.register_entity_type_validator(runtime);
1300 }
1301 }
1302
1303 pub async fn call_warm_all(&self) {
1307 for pack in self.packs.iter() {
1308 pack.warm().await;
1309 }
1310 }
1311
1312 pub fn presentation_policy_for(&self, verb: &str) -> khive_types::VerbPresentationPolicy {
1319 for pack in self.packs.iter() {
1320 if let Some(handler) = pack.handlers().iter().find(|h| h.name == verb) {
1321 return handler.presentation_policy();
1322 }
1323 }
1324 khive_types::VerbPresentationPolicy::Standard
1325 }
1326
1327 pub fn is_subhandler_verb(&self, verb: &str) -> bool {
1334 for pack in self.packs.iter() {
1335 if let Some(handler) = pack.handlers().iter().find(|h| h.name == verb) {
1336 return matches!(handler.visibility, Visibility::Subhandler);
1337 }
1338 }
1339 false
1340 }
1341
1342 pub fn apply_schema_plans(&self, backend: &khive_db::StorageBackend) {
1355 for plan in self.all_schema_plans() {
1356 if plan.is_empty() {
1357 continue;
1358 }
1359 if let Err(e) = backend.apply_pack_ddl_statements(plan.statements) {
1360 tracing::warn!(
1361 pack = plan.pack,
1362 error = %e,
1363 "failed to apply pack schema plan at startup (non-fatal)"
1364 );
1365 }
1366 }
1367 }
1368
1369 pub fn all_schema_plans_named(&self) -> Vec<(&'static str, SchemaPlan)> {
1375 self.packs
1376 .iter()
1377 .map(|p| {
1378 let plan = p.schema_plan();
1379 (plan.pack, plan)
1380 })
1381 .collect()
1382 }
1383
1384 pub fn apply_schema_plans_with_map(
1397 &self,
1398 backend_for_pack: &HashMap<&str, &khive_db::StorageBackend>,
1399 default_backend: &khive_db::StorageBackend,
1400 ) -> Result<(), crate::PackSchemaCollisionError> {
1401 let mut claimed: HashMap<(*const (), String), &'static str> = HashMap::new();
1404
1405 for (pack_name, plan) in self.all_schema_plans_named() {
1406 if plan.is_empty() {
1407 continue;
1408 }
1409 let backend = backend_for_pack
1410 .get(pack_name)
1411 .copied()
1412 .unwrap_or(default_backend);
1413 let backend_ptr = std::sync::Arc::as_ptr(&backend.pool_arc()) as *const ();
1414
1415 for stmt in plan.statements {
1417 for table_name in extract_table_names(stmt) {
1418 let key = (backend_ptr, table_name.clone());
1419 match claimed.entry(key) {
1420 std::collections::hash_map::Entry::Vacant(e) => {
1421 e.insert(pack_name);
1422 }
1423 std::collections::hash_map::Entry::Occupied(e) => {
1424 let prior_pack = *e.get();
1425 return Err(crate::PackSchemaCollisionError {
1426 pack_a: prior_pack,
1427 pack_b: pack_name,
1428 table: table_name,
1429 });
1430 }
1431 }
1432 }
1433 }
1434
1435 backend
1436 .apply_pack_ddl_statements(plan.statements)
1437 .map_err(|e| crate::PackSchemaCollisionError {
1438 pack_a: pack_name,
1439 pack_b: pack_name,
1440 table: format!("DDL error: {e}"),
1441 })?;
1442 }
1443 Ok(())
1444 }
1445}
1446
1447pub trait PackFactory: Send + Sync + 'static {
1457 fn name(&self) -> &'static str;
1459
1460 fn requires(&self) -> &'static [&'static str] {
1467 &[]
1468 }
1469
1470 fn create(&self, runtime: KhiveRuntime) -> Box<dyn PackRuntime>;
1472
1473 fn create_resolver(&self, _runtime: KhiveRuntime) -> Option<Box<dyn PackByIdResolver>> {
1479 None
1480 }
1481}
1482
1483pub struct PackRegistration(pub &'static dyn PackFactory);
1487
1488inventory::collect!(PackRegistration);
1489
1490#[derive(Debug)]
1492pub enum PackLoadError {
1493 UnknownPack(String),
1495 MissingDependency {
1497 pack: String,
1499 dep: String,
1501 },
1502}
1503
1504impl std::fmt::Display for PackLoadError {
1505 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1506 match self {
1507 PackLoadError::UnknownPack(name) => write!(f, "unknown pack {name:?}"),
1508 PackLoadError::MissingDependency { pack, dep } => write!(
1509 f,
1510 "pack {pack:?} requires {dep:?}, which is not in the requested pack list; \
1511 add --pack {dep} before --pack {pack}"
1512 ),
1513 }
1514 }
1515}
1516
1517impl std::error::Error for PackLoadError {}
1518
1519pub struct PackRegistry;
1524
1525impl PackRegistry {
1526 pub fn discovered_names() -> Vec<&'static str> {
1528 inventory::iter::<PackRegistration>
1529 .into_iter()
1530 .map(|r| r.0.name())
1531 .collect()
1532 }
1533
1534 pub fn register_packs(
1547 names: &[String],
1548 runtime: KhiveRuntime,
1549 builder: &mut VerbRegistryBuilder,
1550 ) -> Result<(), PackLoadError> {
1551 let all: Vec<&'static dyn PackFactory> = inventory::iter::<PackRegistration>
1553 .into_iter()
1554 .map(|r| r.0)
1555 .collect();
1556 let factory_for = |name: &str| -> Option<&'static dyn PackFactory> {
1557 all.iter().copied().find(|f| f.name() == name)
1558 };
1559
1560 let requested: std::collections::HashSet<&str> = names.iter().map(String::as_str).collect();
1562 for name in names {
1563 factory_for(name.as_str()).ok_or_else(|| PackLoadError::UnknownPack(name.clone()))?;
1564 }
1565
1566 for name in names {
1569 let factory = factory_for(name.as_str()).unwrap(); for &dep in factory.requires() {
1571 if !requested.contains(dep) {
1572 return Err(PackLoadError::MissingDependency {
1573 pack: name.clone(),
1574 dep: dep.to_string(),
1575 });
1576 }
1577 }
1578 }
1579
1580 for name in names {
1583 let factory = factory_for(name.as_str()).unwrap(); builder.register_boxed(factory.create(runtime.clone()));
1585 if let Some(resolver) = factory.create_resolver(runtime.clone()) {
1586 builder.register_resolver(name.clone(), resolver);
1587 }
1588 }
1589
1590 Ok(())
1591 }
1592
1593 pub fn register_packs_with_runtimes(
1603 names: &[String],
1604 runtimes: &HashMap<String, KhiveRuntime>,
1605 default_runtime: &KhiveRuntime,
1606 builder: &mut VerbRegistryBuilder,
1607 ) -> Result<(), PackLoadError> {
1608 let all: Vec<&'static dyn PackFactory> = inventory::iter::<PackRegistration>
1609 .into_iter()
1610 .map(|r| r.0)
1611 .collect();
1612 let factory_for = |name: &str| -> Option<&'static dyn PackFactory> {
1613 all.iter().copied().find(|f| f.name() == name)
1614 };
1615
1616 let requested: std::collections::HashSet<&str> = names.iter().map(String::as_str).collect();
1617 for name in names {
1618 factory_for(name.as_str()).ok_or_else(|| PackLoadError::UnknownPack(name.clone()))?;
1619 }
1620
1621 for name in names {
1622 let factory = factory_for(name.as_str()).unwrap();
1623 for &dep in factory.requires() {
1624 if !requested.contains(dep) {
1625 return Err(PackLoadError::MissingDependency {
1626 pack: name.clone(),
1627 dep: dep.to_string(),
1628 });
1629 }
1630 }
1631 }
1632
1633 for name in names {
1634 let factory = factory_for(name.as_str()).unwrap();
1635 let runtime = runtimes
1636 .get(name.as_str())
1637 .cloned()
1638 .unwrap_or_else(|| default_runtime.clone());
1639 builder.register_boxed(factory.create(runtime.clone()));
1640 if let Some(resolver) = factory.create_resolver(runtime) {
1641 builder.register_resolver(name.clone(), resolver);
1642 }
1643 }
1644
1645 Ok(())
1646 }
1647}
1648
1649fn target_id_from_args(args: &serde_json::Value) -> Option<uuid::Uuid> {
1650 args.get("target_id")
1651 .and_then(serde_json::Value::as_str)
1652 .and_then(|s| s.parse::<uuid::Uuid>().ok())
1653}
1654
1655#[cfg(test)]
1661mod tests {
1662 use super::*;
1663 use khive_types::Pack;
1664
1665 struct AlphaPack;
1666
1667 impl Pack for AlphaPack {
1668 const NAME: &'static str = "alpha";
1669 const NOTE_KINDS: &'static [&'static str] = &["memo", "log"];
1670 const ENTITY_KINDS: &'static [&'static str] = &["widget"];
1671 const HANDLERS: &'static [HandlerDef] = &[
1672 HandlerDef {
1673 name: "create",
1674 description: "create a widget",
1675 visibility: Visibility::Verb,
1676 category: VerbCategory::Commissive,
1677 params: &[],
1678 },
1679 HandlerDef {
1680 name: "list",
1681 description: "list widgets",
1682 visibility: Visibility::Verb,
1683 category: VerbCategory::Assertive,
1684 params: &[],
1685 },
1686 ];
1687 }
1688
1689 #[async_trait]
1690 impl PackRuntime for AlphaPack {
1691 fn name(&self) -> &str {
1692 AlphaPack::NAME
1693 }
1694 fn note_kinds(&self) -> &'static [&'static str] {
1695 AlphaPack::NOTE_KINDS
1696 }
1697 fn entity_kinds(&self) -> &'static [&'static str] {
1698 AlphaPack::ENTITY_KINDS
1699 }
1700 fn handlers(&self) -> &'static [HandlerDef] {
1701 AlphaPack::HANDLERS
1702 }
1703 async fn dispatch(
1704 &self,
1705 verb: &str,
1706 _params: Value,
1707 _registry: &VerbRegistry,
1708 _token: &NamespaceToken,
1709 ) -> Result<Value, RuntimeError> {
1710 Ok(serde_json::json!({ "pack": "alpha", "verb": verb }))
1711 }
1712 }
1713
1714 struct BetaPack;
1715
1716 impl Pack for BetaPack {
1717 const NAME: &'static str = "beta";
1718 const NOTE_KINDS: &'static [&'static str] = &["alert"];
1719 const ENTITY_KINDS: &'static [&'static str] = &["widget", "gadget"];
1720 const HANDLERS: &'static [HandlerDef] = &[
1721 HandlerDef {
1722 name: "notify",
1723 description: "send alert",
1724 visibility: Visibility::Verb,
1725 category: VerbCategory::Commissive,
1726 params: &[],
1727 },
1728 HandlerDef {
1732 name: "create",
1733 description: "beta internal create (subhandler)",
1734 visibility: Visibility::Subhandler,
1735 category: VerbCategory::Commissive,
1736 params: &[],
1737 },
1738 ];
1739 }
1740
1741 fn build_registry() -> VerbRegistry {
1747 let mut builder = VerbRegistryBuilder::new();
1748 builder.register(AlphaPack);
1749 builder.register(BetaPack);
1750 builder.build().expect("registry builds without collision")
1751 }
1752
1753 struct CollidingPack;
1756
1757 impl Pack for CollidingPack {
1758 const NAME: &'static str = "colliding";
1759 const NOTE_KINDS: &'static [&'static str] = &[];
1760 const ENTITY_KINDS: &'static [&'static str] = &[];
1761 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
1762 name: "create",
1763 description: "duplicate Verb-visibility create",
1764 visibility: Visibility::Verb,
1765 category: VerbCategory::Commissive,
1766 params: &[],
1767 }];
1768 }
1769
1770 #[async_trait]
1771 impl PackRuntime for CollidingPack {
1772 fn name(&self) -> &str {
1773 Self::NAME
1774 }
1775 fn note_kinds(&self) -> &'static [&'static str] {
1776 Self::NOTE_KINDS
1777 }
1778 fn entity_kinds(&self) -> &'static [&'static str] {
1779 Self::ENTITY_KINDS
1780 }
1781 fn handlers(&self) -> &'static [HandlerDef] {
1782 Self::HANDLERS
1783 }
1784 async fn dispatch(
1785 &self,
1786 verb: &str,
1787 _params: Value,
1788 _registry: &VerbRegistry,
1789 _token: &NamespaceToken,
1790 ) -> Result<Value, RuntimeError> {
1791 Ok(serde_json::json!({ "pack": "colliding", "verb": verb }))
1792 }
1793 }
1794
1795 #[async_trait]
1796 impl PackRuntime for BetaPack {
1797 fn name(&self) -> &str {
1798 BetaPack::NAME
1799 }
1800 fn note_kinds(&self) -> &'static [&'static str] {
1801 BetaPack::NOTE_KINDS
1802 }
1803 fn entity_kinds(&self) -> &'static [&'static str] {
1804 BetaPack::ENTITY_KINDS
1805 }
1806 fn handlers(&self) -> &'static [HandlerDef] {
1807 BetaPack::HANDLERS
1808 }
1809 async fn dispatch(
1810 &self,
1811 verb: &str,
1812 _params: Value,
1813 _registry: &VerbRegistry,
1814 _token: &NamespaceToken,
1815 ) -> Result<Value, RuntimeError> {
1816 Ok(serde_json::json!({ "pack": "beta", "verb": verb }))
1817 }
1818 }
1819
1820 #[tokio::test]
1821 async fn dispatch_routes_to_correct_pack() {
1822 let reg = build_registry();
1823
1824 let res = reg.dispatch("list", Value::Null).await.unwrap();
1825 assert_eq!(res["pack"], "alpha");
1826
1827 let res = reg.dispatch("notify", Value::Null).await.unwrap();
1828 assert_eq!(res["pack"], "beta");
1829 }
1830
1831 #[test]
1835 fn verb_collision_is_boot_time_error() {
1836 let mut builder = VerbRegistryBuilder::new();
1837 builder.register(AlphaPack);
1838 builder.register(CollidingPack);
1839 let err = builder
1840 .build()
1841 .err()
1842 .expect("duplicate Verb-visibility handler must be rejected at build time");
1843 assert!(
1844 matches!(err, RuntimeError::VerbCollision { ref verb, .. } if verb == "create"),
1845 "expected VerbCollision for 'create', got {err:?}"
1846 );
1847 let msg = err.to_string();
1848 assert!(
1849 msg.contains("create"),
1850 "error must name the colliding verb: {msg}"
1851 );
1852 assert!(
1853 msg.contains("alpha") || msg.contains("colliding"),
1854 "error must name one of the conflicting packs: {msg}"
1855 );
1856 }
1857
1858 #[test]
1862 fn subhandler_same_name_across_packs_is_not_a_collision() {
1863 struct SubhandlerPack;
1864 impl Pack for SubhandlerPack {
1865 const NAME: &'static str = "subhandler_pack";
1866 const NOTE_KINDS: &'static [&'static str] = &[];
1867 const ENTITY_KINDS: &'static [&'static str] = &[];
1868 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
1869 name: "create",
1870 description: "internal create",
1871 visibility: Visibility::Subhandler,
1872 category: VerbCategory::Commissive,
1873 params: &[],
1874 }];
1875 }
1876 #[async_trait]
1877 impl PackRuntime for SubhandlerPack {
1878 fn name(&self) -> &str {
1879 Self::NAME
1880 }
1881 fn note_kinds(&self) -> &'static [&'static str] {
1882 Self::NOTE_KINDS
1883 }
1884 fn entity_kinds(&self) -> &'static [&'static str] {
1885 Self::ENTITY_KINDS
1886 }
1887 fn handlers(&self) -> &'static [HandlerDef] {
1888 Self::HANDLERS
1889 }
1890 async fn dispatch(
1891 &self,
1892 verb: &str,
1893 _: Value,
1894 _: &VerbRegistry,
1895 _: &NamespaceToken,
1896 ) -> Result<Value, RuntimeError> {
1897 Ok(serde_json::json!({"pack": "subhandler_pack", "verb": verb}))
1898 }
1899 }
1900 let mut builder = VerbRegistryBuilder::new();
1901 builder.register(AlphaPack); builder.register(SubhandlerPack); builder
1904 .build()
1905 .expect("subhandler same name must NOT be a collision");
1906 }
1907
1908 #[tokio::test]
1909 async fn dispatch_unknown_verb_returns_error() {
1910 let reg = build_registry();
1911
1912 let err = reg.dispatch("explode", Value::Null).await.unwrap_err();
1913 let msg = err.to_string();
1914 assert!(msg.contains("explode"));
1915 assert!(msg.contains("create"));
1916 }
1917
1918 #[test]
1923 fn all_verbs_aggregates_across_packs_excludes_subhandlers() {
1924 let reg = build_registry();
1925 let verbs: Vec<&str> = reg.all_verbs().iter().map(|v| v.name).collect();
1926 assert_eq!(verbs, vec!["create", "list", "notify"]);
1928 }
1929
1930 #[test]
1931 fn all_verbs_with_names_pairs_pack_name_excludes_subhandlers() {
1932 let reg = build_registry();
1933 let pairs: Vec<(&str, &str)> = reg
1934 .all_verbs_with_names()
1935 .iter()
1936 .map(|(pack, v)| (*pack, v.name))
1937 .collect();
1938 assert_eq!(
1940 pairs,
1941 vec![("alpha", "create"), ("alpha", "list"), ("beta", "notify"),]
1942 );
1943 }
1944
1945 #[test]
1946 fn all_handlers_with_names_includes_subhandlers() {
1947 let reg = build_registry();
1948 let pairs: Vec<(&str, &str)> = reg
1949 .all_handlers_with_names()
1950 .iter()
1951 .map(|(pack, v)| (*pack, v.name))
1952 .collect();
1953 assert_eq!(
1955 pairs,
1956 vec![
1957 ("alpha", "create"),
1958 ("alpha", "list"),
1959 ("beta", "notify"),
1960 ("beta", "create"),
1961 ]
1962 );
1963 }
1964
1965 #[test]
1966 fn note_kinds_are_ordered() {
1967 let reg = build_registry();
1968 let kinds = reg.all_note_kinds();
1969 assert_eq!(kinds, vec!["memo", "log", "alert"]);
1970 }
1971
1972 #[test]
1973 fn note_kind_duplicate_rejected_at_build_time() {
1974 struct DupPack;
1975
1976 impl khive_types::Pack for DupPack {
1977 const NAME: &'static str = "dup";
1978 const NOTE_KINDS: &'static [&'static str] = &["memo"];
1980 const ENTITY_KINDS: &'static [&'static str] = &[];
1981 const HANDLERS: &'static [HandlerDef] = &[];
1982 }
1983
1984 #[async_trait]
1985 impl PackRuntime for DupPack {
1986 fn name(&self) -> &str {
1987 Self::NAME
1988 }
1989 fn note_kinds(&self) -> &'static [&'static str] {
1990 Self::NOTE_KINDS
1991 }
1992 fn entity_kinds(&self) -> &'static [&'static str] {
1993 Self::ENTITY_KINDS
1994 }
1995 fn handlers(&self) -> &'static [HandlerDef] {
1996 Self::HANDLERS
1997 }
1998 async fn dispatch(
1999 &self,
2000 _verb: &str,
2001 _params: Value,
2002 _registry: &VerbRegistry,
2003 _token: &NamespaceToken,
2004 ) -> Result<Value, RuntimeError> {
2005 Ok(Value::Null)
2006 }
2007 }
2008
2009 let mut builder = VerbRegistryBuilder::new();
2010 builder.register(AlphaPack);
2011 builder.register(DupPack);
2012 let err = builder
2013 .build()
2014 .err()
2015 .expect("duplicate note kind must be rejected");
2016 let msg = err.to_string();
2017 assert!(
2018 msg.contains("memo"),
2019 "error must name the duplicate kind: {msg}"
2020 );
2021 assert!(
2022 msg.contains("alpha") || msg.contains("dup"),
2023 "error must name one of the conflicting packs: {msg}"
2024 );
2025 }
2026
2027 #[test]
2028 fn entity_kinds_are_deduplicated() {
2029 let reg = build_registry();
2030 let kinds = reg.all_entity_kinds();
2031 assert_eq!(kinds, vec!["widget", "gadget"]);
2032 }
2033
2034 use khive_gate::{Gate, GateError};
2037 use std::sync::atomic::{AtomicUsize, Ordering};
2038 use std::sync::Arc;
2039
2040 #[derive(Default, Debug)]
2041 struct CountingGate {
2042 calls: AtomicUsize,
2043 deny_verb: Option<&'static str>,
2044 }
2045
2046 impl Gate for CountingGate {
2047 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
2048 self.calls.fetch_add(1, Ordering::SeqCst);
2049 if Some(req.verb.as_str()) == self.deny_verb {
2050 Ok(GateDecision::deny(format!("test deny for {}", req.verb)))
2051 } else {
2052 Ok(GateDecision::allow())
2053 }
2054 }
2055 }
2056
2057 #[tokio::test]
2058 async fn dispatch_consults_the_gate() {
2059 let gate = Arc::new(CountingGate::default());
2060 let mut builder = VerbRegistryBuilder::new();
2061 builder.register(AlphaPack);
2062 builder.with_gate(gate.clone());
2063 let reg = builder.build().expect("registry builds");
2064
2065 reg.dispatch("list", Value::Null).await.unwrap();
2066 reg.dispatch("create", Value::Null).await.unwrap();
2067 assert_eq!(
2068 gate.calls.load(Ordering::SeqCst),
2069 2,
2070 "gate should be consulted once per dispatch"
2071 );
2072 }
2073
2074 #[tokio::test]
2075 async fn dispatch_returns_permission_denied_on_deny_v03() {
2076 let gate = Arc::new(CountingGate {
2077 calls: AtomicUsize::new(0),
2078 deny_verb: Some("create"),
2079 });
2080 let mut builder = VerbRegistryBuilder::new();
2081 builder.register(AlphaPack);
2082 builder.with_gate(gate.clone());
2083 let reg = builder.build().expect("registry builds");
2084
2085 let err = reg.dispatch("create", Value::Null).await.unwrap_err();
2087 assert!(
2088 matches!(err, RuntimeError::PermissionDenied { ref verb, .. } if verb == "create"),
2089 "expected PermissionDenied, got {err:?}"
2090 );
2091 let msg = err.to_string();
2092 assert!(
2093 msg.contains("create"),
2094 "error message must name the verb: {msg}"
2095 );
2096 assert!(
2097 msg.contains("test deny for create"),
2098 "error message must carry the deny reason: {msg}"
2099 );
2100 assert_eq!(gate.calls.load(Ordering::SeqCst), 1);
2101 }
2102
2103 #[tokio::test]
2104 async fn dispatch_allow_verb_succeeds_even_with_deny_gate_for_other_verb() {
2105 let gate = Arc::new(CountingGate {
2107 calls: AtomicUsize::new(0),
2108 deny_verb: Some("create"),
2109 });
2110 let mut builder = VerbRegistryBuilder::new();
2111 builder.register(AlphaPack);
2112 builder.with_gate(gate.clone());
2113 let reg = builder.build().expect("registry builds");
2114
2115 let res = reg.dispatch("list", Value::Null).await.unwrap();
2116 assert_eq!(res["pack"], "alpha");
2117 }
2118
2119 #[tokio::test]
2120 async fn dispatch_uses_allow_all_gate_by_default() {
2121 let reg = build_registry();
2123 let res = reg.dispatch("list", Value::Null).await.unwrap();
2124 assert_eq!(res["pack"], "alpha");
2125 }
2126
2127 #[derive(Default, Debug)]
2130 struct NamespaceCapturingGate {
2131 seen: std::sync::Mutex<Vec<String>>,
2132 }
2133
2134 impl Gate for NamespaceCapturingGate {
2135 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
2136 self.seen
2137 .lock()
2138 .unwrap()
2139 .push(req.namespace.as_str().to_string());
2140 Ok(GateDecision::allow())
2141 }
2142 }
2143
2144 #[tokio::test]
2145 async fn dispatch_propagates_params_namespace_to_gate() {
2146 let gate = Arc::new(NamespaceCapturingGate::default());
2147 let mut builder = VerbRegistryBuilder::new();
2148 builder.register(AlphaPack);
2149 builder.with_gate(gate.clone());
2150 builder.with_default_namespace("tenant-x");
2151 let reg = builder.build().expect("registry builds");
2152
2153 reg.dispatch("list", serde_json::json!({"namespace": "tenant-y"}))
2155 .await
2156 .unwrap();
2157 reg.dispatch("list", Value::Null).await.unwrap();
2159 let err = reg
2161 .dispatch("list", serde_json::json!({"namespace": ""}))
2162 .await
2163 .unwrap_err();
2164 assert!(
2165 matches!(err, RuntimeError::InvalidInput(_)),
2166 "empty namespace must return InvalidInput, got {err:?}"
2167 );
2168
2169 let seen = gate.seen.lock().unwrap().clone();
2170 assert_eq!(seen, vec!["tenant-y", "tenant-x"]);
2171 }
2172
2173 #[tokio::test]
2174 async fn dispatch_falls_back_to_local_when_no_default_set() {
2175 let gate = Arc::new(NamespaceCapturingGate::default());
2177 let mut builder = VerbRegistryBuilder::new();
2178 builder.register(AlphaPack);
2179 builder.with_gate(gate.clone());
2180 let reg = builder.build().expect("registry builds");
2181
2182 reg.dispatch("list", Value::Null).await.unwrap();
2183 let seen = gate.seen.lock().unwrap().clone();
2184 assert_eq!(seen, vec!["local"]);
2185 }
2186
2187 use khive_gate::{AuditDecision, AuditEvent, Obligation};
2190
2191 #[derive(Default, Debug)]
2193 struct AuditCapturingGate {
2194 events: std::sync::Mutex<Vec<AuditEvent>>,
2195 deny_verb: Option<&'static str>,
2196 }
2197
2198 impl Gate for AuditCapturingGate {
2199 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
2200 let decision = if Some(req.verb.as_str()) == self.deny_verb {
2201 GateDecision::deny("test deny")
2202 } else {
2203 GateDecision::allow_with(vec![Obligation::Audit {
2204 tag: format!("{}.check", req.verb),
2205 }])
2206 };
2207 let ev = AuditEvent::from_check(req, &decision, self.impl_name());
2209 self.events.lock().unwrap().push(ev);
2210 Ok(decision)
2211 }
2212
2213 fn impl_name(&self) -> &'static str {
2214 "AuditCapturingGate"
2215 }
2216 }
2217
2218 #[tokio::test]
2219 async fn dispatch_emits_one_audit_event_per_call() {
2220 let gate = Arc::new(AuditCapturingGate::default());
2221 let mut builder = VerbRegistryBuilder::new();
2222 builder.register(AlphaPack);
2223 builder.with_gate(gate.clone());
2224 let reg = builder.build().expect("registry builds");
2225
2226 reg.dispatch("list", Value::Null).await.unwrap();
2227 reg.dispatch("create", Value::Null).await.unwrap();
2228
2229 let evs = gate.events.lock().unwrap();
2230 assert_eq!(evs.len(), 2, "exactly one audit event per dispatch call");
2231 }
2232
2233 #[tokio::test]
2234 async fn dispatch_audit_event_allow_carries_obligations() {
2235 let gate = Arc::new(AuditCapturingGate::default());
2236 let mut builder = VerbRegistryBuilder::new();
2237 builder.register(AlphaPack);
2238 builder.with_gate(gate.clone());
2239 let reg = builder.build().expect("registry builds");
2240
2241 reg.dispatch("list", Value::Null).await.unwrap();
2242
2243 let evs = gate.events.lock().unwrap();
2244 let ev = &evs[0];
2245 assert_eq!(ev.verb, "list");
2246 assert_eq!(ev.decision, AuditDecision::Allow);
2247 assert!(ev.deny_reason.is_none());
2248 assert_eq!(ev.obligations.len(), 1);
2249 assert_eq!(ev.gate_impl, "AuditCapturingGate");
2250 }
2251
2252 #[tokio::test]
2253 async fn dispatch_audit_event_deny_carries_reason() {
2254 let gate = Arc::new(AuditCapturingGate {
2255 events: Default::default(),
2256 deny_verb: Some("create"),
2257 });
2258 let mut builder = VerbRegistryBuilder::new();
2259 builder.register(AlphaPack);
2260 builder.with_gate(gate.clone());
2261 let reg = builder.build().expect("registry builds");
2262
2263 let err = reg.dispatch("create", Value::Null).await.unwrap_err();
2266 assert!(matches!(err, RuntimeError::PermissionDenied { .. }));
2267
2268 let evs = gate.events.lock().unwrap();
2269 let ev = &evs[0];
2270 assert_eq!(ev.verb, "create");
2271 assert_eq!(ev.decision, AuditDecision::Deny);
2272 assert_eq!(ev.deny_reason.as_deref(), Some("test deny"));
2273 assert!(ev.obligations.is_empty());
2274 }
2275
2276 #[tokio::test]
2277 async fn dispatch_audit_event_fields_match_gate_request() {
2278 let gate = Arc::new(AuditCapturingGate::default());
2279 let mut builder = VerbRegistryBuilder::new();
2280 builder.register(AlphaPack);
2281 builder.with_gate(gate.clone());
2282 builder.with_default_namespace("tenant-z");
2283 let reg = builder.build().expect("registry builds");
2284
2285 reg.dispatch("list", serde_json::json!({"namespace": "tenant-q"}))
2286 .await
2287 .unwrap();
2288
2289 let evs = gate.events.lock().unwrap();
2290 let ev = &evs[0];
2291 assert_eq!(ev.namespace, "tenant-q");
2293 assert_eq!(ev.verb, "list");
2294 assert_eq!(ev.actor.kind, "anonymous");
2295 }
2296
2297 #[derive(Default, Debug)]
2301 struct ActorCapturingGate {
2302 requests: std::sync::Mutex<Vec<GateRequest>>,
2303 }
2304
2305 impl Gate for ActorCapturingGate {
2306 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
2307 self.requests.lock().unwrap().push(req.clone());
2308 Ok(GateDecision::allow())
2309 }
2310 }
2311
2312 #[tokio::test]
2316 async fn gate_request_carries_configured_actor_when_actor_id_is_set() {
2317 let gate = Arc::new(ActorCapturingGate::default());
2318 let mut builder = VerbRegistryBuilder::new();
2319 builder.register(AlphaPack);
2320 builder.with_gate(gate.clone());
2321 builder.with_actor_id(Some("team-abc:implementer".to_string()));
2322 let reg = builder.build().expect("registry builds");
2323
2324 reg.dispatch("list", Value::Null).await.unwrap();
2325
2326 let reqs = gate.requests.lock().unwrap();
2327 assert_eq!(reqs.len(), 1);
2328 let req = &reqs[0];
2329 assert_eq!(
2330 req.actor.kind, "actor",
2331 "gate request must carry kind='actor' when actor_id is configured"
2332 );
2333 assert_eq!(
2334 req.actor.id, "team-abc:implementer",
2335 "gate request must carry the configured actor id"
2336 );
2337 }
2338
2339 #[tokio::test]
2342 async fn gate_request_carries_anonymous_when_no_actor_id_configured() {
2343 let gate = Arc::new(ActorCapturingGate::default());
2344 let mut builder = VerbRegistryBuilder::new();
2345 builder.register(AlphaPack);
2346 builder.with_gate(gate.clone());
2347 let reg = builder.build().expect("registry builds");
2349
2350 reg.dispatch("list", Value::Null).await.unwrap();
2351
2352 let reqs = gate.requests.lock().unwrap();
2353 assert_eq!(reqs.len(), 1);
2354 let req = &reqs[0];
2355 assert_eq!(
2356 req.actor.kind, "anonymous",
2357 "gate request must carry anonymous actor when no actor_id is configured"
2358 );
2359 assert_eq!(req.actor.id, "local");
2360 }
2361
2362 #[tokio::test]
2375 async fn rego_gate_missing_entrypoint_returns_permission_denied() {
2376 use khive_gate_rego::RegoGate;
2377
2378 let policy = r#"
2384 package khive.gate
2385 import rego.v1
2386 verdict := "allow"
2387 "#;
2388 let gate = Arc::new(RegoGate::from_policy_str(policy).expect("policy compiles"));
2389
2390 let mut builder = VerbRegistryBuilder::new();
2391 builder.register(AlphaPack);
2392 builder.with_gate(gate);
2393 let reg = builder.build().expect("registry builds");
2394
2395 let err = reg.dispatch("create", Value::Null).await.unwrap_err();
2396 assert!(
2397 matches!(err, RuntimeError::PermissionDenied { ref verb, .. } if verb == "create"),
2398 "expected PermissionDenied for missing rego entrypoint, got {err:?}"
2399 );
2400 }
2401
2402 use std::sync::{Mutex as StdMutex, Once, OnceLock};
2414
2415 use serial_test::serial;
2416 use tracing::field::{Field, Visit};
2417
2418 #[derive(Clone, Debug, Default)]
2419 struct CapturedEvent {
2420 message: Option<String>,
2421 audit_event: Option<String>,
2422 }
2423
2424 #[derive(Default)]
2425 struct CapturedEventVisitor(CapturedEvent);
2426
2427 impl Visit for CapturedEventVisitor {
2428 fn record_str(&mut self, field: &Field, value: &str) {
2429 match field.name() {
2430 "message" => self.0.message = Some(value.to_string()),
2431 "audit_event" => self.0.audit_event = Some(value.to_string()),
2432 _ => {}
2433 }
2434 }
2435
2436 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
2437 let formatted = format!("{value:?}");
2443 let cleaned = formatted
2444 .trim_start_matches('"')
2445 .trim_end_matches('"')
2446 .to_string();
2447 match field.name() {
2448 "message" => self.0.message = Some(cleaned),
2449 "audit_event" => self.0.audit_event = Some(cleaned),
2450 _ => {}
2451 }
2452 }
2453 }
2454
2455 struct CaptureSubscriber {
2468 events: Arc<StdMutex<Vec<CapturedEvent>>>,
2469 }
2470
2471 impl CaptureSubscriber {
2472 fn new(events: Arc<StdMutex<Vec<CapturedEvent>>>) -> Self {
2473 Self { events }
2474 }
2475 }
2476
2477 impl tracing::Subscriber for CaptureSubscriber {
2478 fn enabled(&self, _: &tracing::Metadata<'_>) -> bool {
2479 true
2480 }
2481 fn new_span(&self, _: &tracing::span::Attributes<'_>) -> tracing::span::Id {
2482 tracing::span::Id::from_u64(1)
2483 }
2484 fn record(&self, _: &tracing::span::Id, _: &tracing::span::Record<'_>) {}
2485 fn record_follows_from(&self, _: &tracing::span::Id, _: &tracing::span::Id) {}
2486 fn event(&self, event: &tracing::Event<'_>) {
2487 let mut visitor = CapturedEventVisitor::default();
2488 event.record(&mut visitor);
2489 self.events.lock().unwrap().push(visitor.0);
2490 }
2491 fn enter(&self, _: &tracing::span::Id) {}
2492 fn exit(&self, _: &tracing::span::Id) {}
2493 }
2494
2495 static GLOBAL_CAPTURE: OnceLock<Arc<StdMutex<Vec<CapturedEvent>>>> = OnceLock::new();
2505 static GLOBAL_INIT: Once = Once::new();
2506
2507 fn global_capture() -> Arc<StdMutex<Vec<CapturedEvent>>> {
2508 GLOBAL_INIT.call_once(|| {
2509 let buffer = Arc::new(StdMutex::new(Vec::new()));
2510 let subscriber = CaptureSubscriber::new(Arc::clone(&buffer));
2511 let _ = tracing::subscriber::set_global_default(subscriber);
2516 let _ = GLOBAL_CAPTURE.set(buffer);
2517 });
2518 Arc::clone(GLOBAL_CAPTURE.get().expect("global capture initialized"))
2519 }
2520
2521 fn capture_dispatch_events<Fut>(future: Fut) -> Vec<CapturedEvent>
2526 where
2527 Fut: std::future::Future<Output = ()>,
2528 {
2529 let buffer = global_capture();
2530 buffer.lock().unwrap().clear();
2531
2532 let rt = tokio::runtime::Builder::new_current_thread()
2533 .enable_all()
2534 .build()
2535 .expect("build current-thread tokio runtime");
2536 rt.block_on(future);
2537
2538 let result = buffer.lock().unwrap().clone();
2539 result
2540 }
2541
2542 fn gate_check_events_for(events: &[CapturedEvent], gate_impl: &str) -> Vec<CapturedEvent> {
2549 events
2550 .iter()
2551 .filter(|e| e.message.as_deref() == Some("gate.check"))
2552 .filter(|e| {
2553 e.audit_event
2554 .as_deref()
2555 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
2556 .and_then(|v| {
2557 v.get("gate_impl")
2558 .and_then(|g| g.as_str().map(|s| s.to_string()))
2559 })
2560 .as_deref()
2561 == Some(gate_impl)
2562 })
2563 .cloned()
2564 .collect()
2565 }
2566
2567 #[test]
2568 #[serial]
2569 fn dispatch_tracing_emits_one_gate_check_event_on_allow() {
2570 #[derive(Debug)]
2571 struct TracingAllowGate;
2572 impl Gate for TracingAllowGate {
2573 fn check(&self, _: &GateRequest) -> Result<GateDecision, GateError> {
2574 Ok(GateDecision::allow())
2575 }
2576 fn impl_name(&self) -> &'static str {
2577 "TracingAllowGate"
2578 }
2579 }
2580
2581 let events = capture_dispatch_events(async {
2582 let mut builder = VerbRegistryBuilder::new();
2583 builder.register(AlphaPack);
2584 builder.with_gate(Arc::new(TracingAllowGate));
2585 builder.with_default_namespace("tenant-default");
2586 let reg = builder.build().expect("registry builds");
2587 reg.dispatch("list", serde_json::json!({"namespace": "tenant-q"}))
2588 .await
2589 .unwrap();
2590 });
2591
2592 let gate_events = gate_check_events_for(&events, "TracingAllowGate");
2593 assert_eq!(
2594 gate_events.len(),
2595 1,
2596 "exactly one gate.check tracing event per dispatch (allow); got {gate_events:?}"
2597 );
2598 let payload = gate_events[0]
2599 .audit_event
2600 .as_ref()
2601 .expect("gate.check event must carry an audit_event field");
2602 let audit: khive_gate::AuditEvent =
2603 serde_json::from_str(payload).expect("audit_event payload must decode to AuditEvent");
2604 assert_eq!(audit.decision, AuditDecision::Allow);
2605 assert_eq!(audit.verb, "list");
2606 assert_eq!(audit.namespace, "tenant-q");
2607 assert_eq!(audit.gate_impl, "TracingAllowGate");
2608 assert!(
2609 audit.deny_reason.is_none(),
2610 "deny_reason must be None on Allow"
2611 );
2612 }
2613
2614 use crate::runtime::NamespaceToken;
2617 use async_trait::async_trait;
2618 use khive_storage::{
2619 BatchWriteSummary, Event, EventFilter, EventStore, Page, PageRequest, SubstrateKind,
2620 };
2621 use khive_types::EventOutcome;
2622
2623 #[derive(Default, Debug)]
2625 struct MemoryEventStore {
2626 events: std::sync::Mutex<Vec<Event>>,
2627 }
2628
2629 #[async_trait]
2630 impl EventStore for MemoryEventStore {
2631 async fn append_event(&self, event: Event) -> khive_storage::StorageResult<()> {
2632 self.events.lock().unwrap().push(event);
2633 Ok(())
2634 }
2635 async fn append_events(
2636 &self,
2637 events: Vec<Event>,
2638 ) -> khive_storage::StorageResult<BatchWriteSummary> {
2639 let attempted = events.len() as u64;
2640 let affected = attempted;
2641 self.events.lock().unwrap().extend(events);
2642 Ok(BatchWriteSummary {
2643 attempted,
2644 affected,
2645 failed: 0,
2646 first_error: String::new(),
2647 })
2648 }
2649 async fn get_event(&self, id: uuid::Uuid) -> khive_storage::StorageResult<Option<Event>> {
2650 Ok(self
2651 .events
2652 .lock()
2653 .unwrap()
2654 .iter()
2655 .find(|e| e.id == id)
2656 .cloned())
2657 }
2658 async fn query_events(
2659 &self,
2660 _filter: EventFilter,
2661 _page: PageRequest,
2662 ) -> khive_storage::StorageResult<Page<Event>> {
2663 let items = self.events.lock().unwrap().clone();
2664 let total = items.len() as u64;
2665 Ok(Page {
2666 items,
2667 total: Some(total),
2668 })
2669 }
2670 async fn count_events(&self, _filter: EventFilter) -> khive_storage::StorageResult<u64> {
2671 Ok(self.events.lock().unwrap().len() as u64)
2672 }
2673 }
2674
2675 #[tokio::test]
2676 async fn allow_all_gate_default_remains_backward_compatible() {
2677 let mut builder = VerbRegistryBuilder::new();
2679 builder.register(AlphaPack);
2680 let reg = builder.build().expect("registry builds");
2681
2682 let res = reg.dispatch("list", Value::Null).await.unwrap();
2683 assert_eq!(
2684 res["pack"], "alpha",
2685 "AllowAllGate must allow every verb — backward compat guarantee"
2686 );
2687 let res = reg.dispatch("create", Value::Null).await.unwrap();
2688 assert_eq!(res["pack"], "alpha");
2689 }
2690
2691 #[tokio::test]
2692 async fn deny_gate_returns_permission_denied_pack_never_invoked() {
2693 #[derive(Debug)]
2694 struct AlwaysDenyGate;
2695 impl Gate for AlwaysDenyGate {
2696 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2697 Ok(GateDecision::deny("test: always deny"))
2698 }
2699 }
2700
2701 #[derive(Debug)]
2703 struct TrackedPack {
2704 invoked: Arc<AtomicUsize>,
2705 }
2706
2707 impl khive_types::Pack for TrackedPack {
2708 const NAME: &'static str = "tracked";
2709 const NOTE_KINDS: &'static [&'static str] = &[];
2710 const ENTITY_KINDS: &'static [&'static str] = &[];
2711 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
2712 name: "guarded",
2713 description: "a guarded verb",
2714 visibility: Visibility::Verb,
2715 category: VerbCategory::Assertive,
2716 params: &[],
2717 }];
2718 }
2719
2720 #[async_trait]
2721 impl PackRuntime for TrackedPack {
2722 fn name(&self) -> &str {
2723 Self::NAME
2724 }
2725 fn note_kinds(&self) -> &'static [&'static str] {
2726 Self::NOTE_KINDS
2727 }
2728 fn entity_kinds(&self) -> &'static [&'static str] {
2729 Self::ENTITY_KINDS
2730 }
2731 fn handlers(&self) -> &'static [HandlerDef] {
2732 Self::HANDLERS
2733 }
2734 async fn dispatch(
2735 &self,
2736 _verb: &str,
2737 _params: Value,
2738 _registry: &VerbRegistry,
2739 _token: &NamespaceToken,
2740 ) -> Result<Value, RuntimeError> {
2741 self.invoked.fetch_add(1, Ordering::SeqCst);
2742 Ok(serde_json::json!({"invoked": true}))
2743 }
2744 }
2745
2746 let invoked = Arc::new(AtomicUsize::new(0));
2747 let mut builder = VerbRegistryBuilder::new();
2748 builder.register(TrackedPack {
2749 invoked: invoked.clone(),
2750 });
2751 builder.with_gate(Arc::new(AlwaysDenyGate));
2752 let reg = builder.build().expect("registry builds");
2753
2754 let err = reg.dispatch("guarded", Value::Null).await.unwrap_err();
2755 assert!(
2756 matches!(err, RuntimeError::PermissionDenied { ref verb, ref reason } if verb == "guarded" && reason.contains("always deny")),
2757 "expected PermissionDenied with verb=guarded and reason, got: {err:?}"
2758 );
2759 assert_eq!(
2760 invoked.load(Ordering::SeqCst),
2761 0,
2762 "pack dispatch MUST NOT be invoked when gate denies"
2763 );
2764 }
2765
2766 #[tokio::test]
2767 async fn audit_event_persists_to_event_store_on_allow() {
2768 let store = Arc::new(MemoryEventStore::default());
2769 let mut builder = VerbRegistryBuilder::new();
2770 builder.register(AlphaPack);
2771 builder.with_event_store(store.clone());
2772 let reg = builder.build().expect("registry builds");
2773
2774 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2775 .await
2776 .unwrap();
2777
2778 let count = store.count_events(EventFilter::default()).await.unwrap();
2779 assert_eq!(count, 1, "one audit event persisted to EventStore on allow");
2780
2781 let page = store
2782 .query_events(
2783 EventFilter::default(),
2784 PageRequest {
2785 limit: 10,
2786 offset: 0,
2787 },
2788 )
2789 .await
2790 .unwrap();
2791 let ev = &page.items[0];
2792 assert_eq!(ev.verb, "list");
2793 assert_eq!(ev.namespace, "test-ns");
2794 assert_eq!(ev.substrate, SubstrateKind::Event);
2795 assert_eq!(ev.outcome, EventOutcome::Success);
2796 }
2797
2798 #[tokio::test]
2799 async fn audit_event_persists_to_event_store_on_deny() {
2800 #[derive(Debug)]
2801 struct AlwaysDenyGate;
2802 impl Gate for AlwaysDenyGate {
2803 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2804 Ok(GateDecision::deny("denied by test"))
2805 }
2806 }
2807
2808 let store = Arc::new(MemoryEventStore::default());
2809 let mut builder = VerbRegistryBuilder::new();
2810 builder.register(AlphaPack);
2811 builder.with_gate(Arc::new(AlwaysDenyGate));
2812 builder.with_event_store(store.clone());
2813 let reg = builder.build().expect("registry builds");
2814
2815 let err = reg
2817 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2818 .await
2819 .unwrap_err();
2820 assert!(matches!(err, RuntimeError::PermissionDenied { .. }));
2821
2822 let count = store.count_events(EventFilter::default()).await.unwrap();
2823 assert_eq!(count, 1, "one audit event persisted to EventStore on deny");
2824
2825 let page = store
2826 .query_events(
2827 EventFilter::default(),
2828 PageRequest {
2829 limit: 10,
2830 offset: 0,
2831 },
2832 )
2833 .await
2834 .unwrap();
2835 let ev = &page.items[0];
2836 assert_eq!(ev.verb, "list");
2837 assert_eq!(ev.outcome, EventOutcome::Denied);
2838 }
2839
2840 #[tokio::test]
2841 async fn gate_error_does_not_persist_to_event_store() {
2842 #[derive(Debug)]
2843 struct FailingGate;
2844 impl Gate for FailingGate {
2845 fn check(&self, _req: &GateRequest) -> Result<GateDecision, khive_gate::GateError> {
2846 Err(khive_gate::GateError::Internal("gate broken".into()))
2847 }
2848 }
2849
2850 let store = Arc::new(MemoryEventStore::default());
2851 let mut builder = VerbRegistryBuilder::new();
2852 builder.register(AlphaPack);
2853 builder.with_gate(Arc::new(FailingGate));
2854 builder.with_event_store(store.clone());
2855 let reg = builder.build().expect("registry builds");
2856
2857 let res = reg.dispatch("list", Value::Null).await.unwrap();
2859 assert_eq!(
2860 res["pack"], "alpha",
2861 "gate error must fail-open, not block dispatch"
2862 );
2863
2864 let count = store.count_events(EventFilter::default()).await.unwrap();
2865 assert_eq!(
2866 count, 0,
2867 "gate infrastructure error must NOT produce an audit event in EventStore"
2868 );
2869 }
2870
2871 #[tokio::test]
2872 async fn no_event_store_configured_tracing_only() {
2873 let mut builder = VerbRegistryBuilder::new();
2877 builder.register(AlphaPack);
2878 let reg = builder.build().expect("registry builds");
2879
2880 let res = reg.dispatch("list", Value::Null).await.unwrap();
2881 assert_eq!(res["pack"], "alpha");
2882 }
2883
2884 #[test]
2885 #[serial]
2886 fn dispatch_tracing_emits_gate_check_event_with_deny_payload() {
2887 #[derive(Debug)]
2888 struct TracingDenyGate;
2889 impl Gate for TracingDenyGate {
2890 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2891 Ok(GateDecision::deny("denied by test gate"))
2892 }
2893 fn impl_name(&self) -> &'static str {
2894 "TracingDenyGate"
2895 }
2896 }
2897
2898 let events = capture_dispatch_events(async {
2899 let mut builder = VerbRegistryBuilder::new();
2900 builder.register(AlphaPack);
2901 builder.with_gate(Arc::new(TracingDenyGate));
2902 let reg = builder.build().expect("registry builds");
2903 let _ = reg.dispatch("create", serde_json::Value::Null).await;
2906 });
2907
2908 let gate_events = gate_check_events_for(&events, "TracingDenyGate");
2909 assert_eq!(
2910 gate_events.len(),
2911 1,
2912 "exactly one gate.check tracing event per dispatch (deny); got {gate_events:?}"
2913 );
2914 let payload = gate_events[0]
2915 .audit_event
2916 .as_ref()
2917 .expect("gate.check event must carry an audit_event field on Deny");
2918 let audit: khive_gate::AuditEvent =
2919 serde_json::from_str(payload).expect("audit_event payload must decode to AuditEvent");
2920 assert_eq!(audit.decision, AuditDecision::Deny);
2921 assert_eq!(audit.deny_reason.as_deref(), Some("denied by test gate"));
2922 assert_eq!(audit.gate_impl, "TracingDenyGate");
2923 let payload_json: serde_json::Value =
2927 serde_json::from_str(payload).expect("payload must be valid JSON");
2928 assert_eq!(
2929 payload_json["obligations"],
2930 serde_json::Value::Array(Vec::new()),
2931 "obligations must be `[]` on Deny on the tracing payload, not omitted"
2932 );
2933 }
2934
2935 #[tokio::test]
2943 async fn audit_envelope_round_trips_deny_reason_and_gate_impl_through_event_store() {
2944 #[derive(Debug)]
2945 struct DenyGateWithName;
2946 impl Gate for DenyGateWithName {
2947 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2948 Ok(GateDecision::deny("policy: write forbidden for anon"))
2949 }
2950 fn impl_name(&self) -> &'static str {
2951 "DenyGateWithName"
2952 }
2953 }
2954
2955 let store = Arc::new(MemoryEventStore::default());
2956 let mut builder = VerbRegistryBuilder::new();
2957 builder.register(AlphaPack);
2958 builder.with_gate(Arc::new(DenyGateWithName));
2959 builder.with_event_store(store.clone());
2960 let reg = builder.build().expect("registry builds");
2961
2962 let err = reg
2964 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2965 .await
2966 .unwrap_err();
2967 assert!(
2968 matches!(err, RuntimeError::PermissionDenied { .. }),
2969 "expected PermissionDenied, got {err:?}"
2970 );
2971
2972 let page = store
2974 .query_events(
2975 EventFilter::default(),
2976 PageRequest {
2977 limit: 10,
2978 offset: 0,
2979 },
2980 )
2981 .await
2982 .unwrap();
2983 assert_eq!(
2984 page.items.len(),
2985 1,
2986 "one audit event must be persisted on deny"
2987 );
2988
2989 let ev = &page.items[0];
2990 assert_eq!(ev.outcome, EventOutcome::Denied);
2991
2992 let data = &ev.payload;
2994
2995 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
2996 .expect("Event.payload must deserialize to AuditEvent");
2997
2998 assert_eq!(
2999 audit.deny_reason.as_deref(),
3000 Some("policy: write forbidden for anon"),
3001 "deny_reason must be preserved through EventStore"
3002 );
3003 assert_eq!(
3004 audit.gate_impl, "DenyGateWithName",
3005 "gate_impl must be preserved through EventStore"
3006 );
3007 assert_eq!(
3008 audit.decision,
3009 khive_gate::AuditDecision::Deny,
3010 "decision field must be preserved through EventStore"
3011 );
3012 }
3013
3014 #[tokio::test]
3015 async fn audit_envelope_round_trips_obligations_through_event_store() {
3016 use khive_gate::Obligation;
3017
3018 #[derive(Debug)]
3019 struct ObligationGate;
3020 impl Gate for ObligationGate {
3021 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
3022 Ok(GateDecision::allow_with(vec![Obligation::Audit {
3023 tag: "billing.meter".into(),
3024 }]))
3025 }
3026 fn impl_name(&self) -> &'static str {
3027 "ObligationGate"
3028 }
3029 }
3030
3031 let store = Arc::new(MemoryEventStore::default());
3032 let mut builder = VerbRegistryBuilder::new();
3033 builder.register(AlphaPack);
3034 builder.with_gate(Arc::new(ObligationGate));
3035 builder.with_event_store(store.clone());
3036 let reg = builder.build().expect("registry builds");
3037
3038 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
3039 .await
3040 .unwrap();
3041
3042 let page = store
3043 .query_events(
3044 EventFilter::default(),
3045 PageRequest {
3046 limit: 10,
3047 offset: 0,
3048 },
3049 )
3050 .await
3051 .unwrap();
3052 assert_eq!(page.items.len(), 1);
3053
3054 let ev = &page.items[0];
3055 assert_eq!(ev.outcome, EventOutcome::Success);
3056
3057 let data = &ev.payload;
3058
3059 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
3060 .expect("Event.payload must deserialize to AuditEvent");
3061
3062 assert_eq!(audit.gate_impl, "ObligationGate");
3063 assert_eq!(
3064 audit.obligations.len(),
3065 1,
3066 "obligations must be preserved through EventStore"
3067 );
3068 match &audit.obligations[0] {
3069 Obligation::Audit { tag } => assert_eq!(tag, "billing.meter"),
3070 other => panic!("expected Audit obligation, got {other:?}"),
3071 }
3072 }
3073
3074 #[tokio::test]
3082 async fn sql_backed_audit_envelope_round_trips_deny_reason_gate_impl_and_obligations() {
3083 #[derive(Debug)]
3084 struct SqlTestDenyGate;
3085 impl Gate for SqlTestDenyGate {
3086 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
3087 Ok(GateDecision::deny("sql-path: write denied"))
3088 }
3089 fn impl_name(&self) -> &'static str {
3090 "SqlTestDenyGate"
3091 }
3092 }
3093
3094 let rt = KhiveRuntime::memory().expect("in-memory runtime");
3098 let test_tok = NamespaceToken::for_namespace(Namespace::parse("test-ns").unwrap());
3099 let sql_store = rt
3100 .events(&test_tok)
3101 .expect("events_for_namespace must succeed");
3102
3103 let mut builder = VerbRegistryBuilder::new();
3104 builder.register(AlphaPack);
3105 builder.with_gate(Arc::new(SqlTestDenyGate));
3106 builder.with_event_store(sql_store.clone());
3107 let reg = builder.build().expect("registry builds");
3108
3109 let err = reg
3111 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
3112 .await
3113 .unwrap_err();
3114 assert!(
3115 matches!(err, RuntimeError::PermissionDenied { .. }),
3116 "expected PermissionDenied, got {err:?}"
3117 );
3118
3119 let page = sql_store
3121 .query_events(
3122 EventFilter::default(),
3123 PageRequest {
3124 limit: 10,
3125 offset: 0,
3126 },
3127 )
3128 .await
3129 .unwrap();
3130 assert_eq!(
3131 page.items.len(),
3132 1,
3133 "one audit event must be persisted on deny through SqlEventStore"
3134 );
3135
3136 let ev = &page.items[0];
3137 assert_eq!(ev.outcome, EventOutcome::Denied);
3138
3139 let data = &ev.payload;
3143
3144 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
3145 .expect("Event.payload must deserialize to AuditEvent after SQL round-trip");
3146
3147 assert_eq!(
3148 audit.deny_reason.as_deref(),
3149 Some("sql-path: write denied"),
3150 "deny_reason must survive the SQL text round-trip"
3151 );
3152 assert_eq!(
3153 audit.gate_impl, "SqlTestDenyGate",
3154 "gate_impl must survive the SQL text round-trip"
3155 );
3156 assert_eq!(
3157 audit.decision,
3158 khive_gate::AuditDecision::Deny,
3159 "decision field must survive the SQL text round-trip"
3160 );
3161 assert!(
3164 audit.obligations.is_empty(),
3165 "obligations must be preserved as empty [] through SQL round-trip"
3166 );
3167 }
3168
3169 #[tokio::test]
3181 async fn sql_backed_audit_envelope_round_trips_non_empty_obligations() {
3182 use khive_gate::Obligation;
3183
3184 #[derive(Debug)]
3185 struct SqlTestAllowWithObligationGate;
3186 impl Gate for SqlTestAllowWithObligationGate {
3187 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
3188 Ok(GateDecision::allow_with(vec![Obligation::Audit {
3189 tag: "sql-path-billing.meter".into(),
3190 }]))
3191 }
3192 fn impl_name(&self) -> &'static str {
3193 "SqlTestAllowWithObligationGate"
3194 }
3195 }
3196
3197 let rt = KhiveRuntime::memory().expect("in-memory runtime");
3198 let test_tok = NamespaceToken::for_namespace(Namespace::parse("test-ns").unwrap());
3199 let sql_store = rt
3200 .events(&test_tok)
3201 .expect("events_for_namespace must succeed");
3202
3203 let mut builder = VerbRegistryBuilder::new();
3204 builder.register(AlphaPack);
3205 builder.with_gate(Arc::new(SqlTestAllowWithObligationGate));
3206 builder.with_event_store(sql_store.clone());
3207 let reg = builder.build().expect("registry builds");
3208
3209 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
3211 .await
3212 .expect("dispatch must succeed when gate allows");
3213
3214 let page = sql_store
3216 .query_events(
3217 EventFilter::default(),
3218 PageRequest {
3219 limit: 10,
3220 offset: 0,
3221 },
3222 )
3223 .await
3224 .unwrap();
3225 assert_eq!(
3226 page.items.len(),
3227 1,
3228 "one audit event must be persisted on allow through SqlEventStore"
3229 );
3230
3231 let ev = &page.items[0];
3232 assert_eq!(ev.outcome, EventOutcome::Success);
3233
3234 let data = &ev.payload;
3235
3236 let obligations_raw = data
3241 .get("obligations")
3242 .expect("Event.data JSON must contain 'obligations' key");
3243 let obligations_arr = obligations_raw
3244 .as_array()
3245 .expect("'obligations' must be a JSON array");
3246 assert!(
3247 !obligations_arr.is_empty(),
3248 "raw Event.data['obligations'] must be non-empty after SQL round-trip"
3249 );
3250
3251 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
3254 .expect("Event.data must deserialize to AuditEvent after SQL round-trip");
3255
3256 assert_eq!(
3257 audit.gate_impl, "SqlTestAllowWithObligationGate",
3258 "gate_impl must survive the SQL text round-trip"
3259 );
3260 assert_eq!(
3261 audit.decision,
3262 khive_gate::AuditDecision::Allow,
3263 "decision field must survive the SQL text round-trip"
3264 );
3265 assert_eq!(
3266 audit.obligations.len(),
3267 1,
3268 "obligations must be non-empty after SQL round-trip (not silently defaulted to [])"
3269 );
3270 match &audit.obligations[0] {
3271 Obligation::Audit { tag } => assert_eq!(
3272 tag, "sql-path-billing.meter",
3273 "Audit obligation tag must survive the SQL text round-trip"
3274 ),
3275 other => panic!("expected Audit obligation, got {other:?}"),
3276 }
3277 }
3278
3279 #[tokio::test]
3287 async fn audit_event_payload_shape_for_create_verb() {
3288 let store = Arc::new(MemoryEventStore::default());
3289 let mut builder = VerbRegistryBuilder::new();
3290 builder.register(AlphaPack);
3291 builder.with_event_store(store.clone());
3292 builder.with_default_namespace("test-ns");
3293 let reg = builder.build().expect("registry builds");
3294
3295 reg.dispatch("create", serde_json::json!({"namespace": "test-ns"}))
3298 .await
3299 .unwrap();
3300
3301 let count = store.count_events(EventFilter::default()).await.unwrap();
3302 assert_eq!(count, 1, "exactly one audit event for one dispatch");
3303
3304 let page = store
3305 .query_events(
3306 EventFilter::default(),
3307 PageRequest {
3308 limit: 10,
3309 offset: 0,
3310 },
3311 )
3312 .await
3313 .unwrap();
3314 let ev = &page.items[0];
3315
3316 assert_eq!(ev.verb, "create", "ev.verb must be the dispatched verb");
3318 assert_eq!(
3319 ev.outcome,
3320 EventOutcome::Success,
3321 "ev.outcome must be Success on allow"
3322 );
3323 assert_eq!(
3324 ev.namespace, "test-ns",
3325 "ev.namespace must match the dispatch namespace"
3326 );
3327
3328 let data = &ev.payload;
3330
3331 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
3332 .expect("ev.payload must deserialize to AuditEvent");
3333
3334 assert_eq!(
3335 audit.decision,
3336 khive_gate::AuditDecision::Allow,
3337 "AuditEvent.decision must be Allow"
3338 );
3339 assert_eq!(audit.verb, "create", "AuditEvent.verb must be 'create'");
3340 assert_eq!(
3341 audit.namespace, "test-ns",
3342 "AuditEvent.namespace must be preserved"
3343 );
3344 assert_eq!(
3345 audit.gate_impl, "AllowAllGate",
3346 "AuditEvent.gate_impl must name the gate implementation"
3347 );
3348 assert!(
3349 audit.deny_reason.is_none(),
3350 "AuditEvent.deny_reason must be None on Allow"
3351 );
3352 let payload_json: serde_json::Value =
3354 serde_json::from_value(data.clone()).expect("data must be valid JSON");
3355 assert_eq!(
3356 payload_json["obligations"],
3357 serde_json::Value::Array(Vec::new()),
3358 "obligations must be [] on AllowAllGate"
3359 );
3360 }
3361
3362 #[tokio::test]
3364 async fn audit_event_threads_target_id_from_dispatch_args() {
3365 let store = Arc::new(MemoryEventStore::default());
3366 let target = uuid::Uuid::new_v4();
3367 let mut builder = VerbRegistryBuilder::new();
3368 builder.register(AlphaPack);
3369 builder.with_event_store(store.clone());
3370 builder.with_default_namespace("test-ns");
3371 let reg = builder.build().expect("registry builds");
3372
3373 reg.dispatch(
3374 "create",
3375 serde_json::json!({"namespace": "test-ns", "target_id": target}),
3376 )
3377 .await
3378 .unwrap();
3379
3380 let page = store
3381 .query_events(
3382 EventFilter::default(),
3383 PageRequest {
3384 offset: 0,
3385 limit: 10,
3386 },
3387 )
3388 .await
3389 .unwrap();
3390 assert_eq!(
3391 page.items[0].target_id,
3392 Some(target),
3393 "#282: audit event must carry target_id from dispatch params"
3394 );
3395 }
3396}
3397
3398#[cfg(test)]
3401mod dep_tests {
3402 use super::*;
3403 use async_trait::async_trait;
3404 use khive_types::Pack;
3405 use serde_json::Value;
3406
3407 struct KgDepPack;
3408 struct MemoryDepPack;
3409 struct ADepPack;
3410 struct BDepPack;
3411
3412 impl Pack for KgDepPack {
3413 const NAME: &'static str = "kg_dep";
3414 const NOTE_KINDS: &'static [&'static str] = &["observation"];
3415 const ENTITY_KINDS: &'static [&'static str] = &["concept"];
3416 const HANDLERS: &'static [HandlerDef] = &[];
3417 }
3418
3419 impl Pack for MemoryDepPack {
3420 const NAME: &'static str = "memory_dep";
3421 const NOTE_KINDS: &'static [&'static str] = &["memory"];
3422 const ENTITY_KINDS: &'static [&'static str] = &[];
3423 const HANDLERS: &'static [HandlerDef] = &[];
3424 const REQUIRES: &'static [&'static str] = &["kg_dep"];
3425 }
3426
3427 impl Pack for ADepPack {
3428 const NAME: &'static str = "pack_a";
3429 const NOTE_KINDS: &'static [&'static str] = &[];
3430 const ENTITY_KINDS: &'static [&'static str] = &[];
3431 const HANDLERS: &'static [HandlerDef] = &[];
3432 const REQUIRES: &'static [&'static str] = &["pack_b"];
3433 }
3434
3435 impl Pack for BDepPack {
3436 const NAME: &'static str = "pack_b";
3437 const NOTE_KINDS: &'static [&'static str] = &[];
3438 const ENTITY_KINDS: &'static [&'static str] = &[];
3439 const HANDLERS: &'static [HandlerDef] = &[];
3440 const REQUIRES: &'static [&'static str] = &["pack_a"];
3441 }
3442
3443 #[async_trait]
3444 impl PackRuntime for KgDepPack {
3445 fn name(&self) -> &str {
3446 Self::NAME
3447 }
3448 fn note_kinds(&self) -> &'static [&'static str] {
3449 Self::NOTE_KINDS
3450 }
3451 fn entity_kinds(&self) -> &'static [&'static str] {
3452 Self::ENTITY_KINDS
3453 }
3454 fn handlers(&self) -> &'static [HandlerDef] {
3455 Self::HANDLERS
3456 }
3457 async fn dispatch(
3458 &self,
3459 verb: &str,
3460 _: Value,
3461 _: &VerbRegistry,
3462 _: &NamespaceToken,
3463 ) -> Result<Value, RuntimeError> {
3464 Err(RuntimeError::InvalidInput(format!(
3465 "KgDepPack has no verbs: {verb}"
3466 )))
3467 }
3468 }
3469
3470 #[async_trait]
3471 impl PackRuntime for MemoryDepPack {
3472 fn name(&self) -> &str {
3473 Self::NAME
3474 }
3475 fn note_kinds(&self) -> &'static [&'static str] {
3476 Self::NOTE_KINDS
3477 }
3478 fn entity_kinds(&self) -> &'static [&'static str] {
3479 Self::ENTITY_KINDS
3480 }
3481 fn handlers(&self) -> &'static [HandlerDef] {
3482 Self::HANDLERS
3483 }
3484 fn requires(&self) -> &'static [&'static str] {
3485 Self::REQUIRES
3486 }
3487 async fn dispatch(
3488 &self,
3489 verb: &str,
3490 _: Value,
3491 _: &VerbRegistry,
3492 _: &NamespaceToken,
3493 ) -> Result<Value, RuntimeError> {
3494 Err(RuntimeError::InvalidInput(format!(
3495 "MemoryDepPack has no verbs: {verb}"
3496 )))
3497 }
3498 }
3499
3500 #[async_trait]
3501 impl PackRuntime for ADepPack {
3502 fn name(&self) -> &str {
3503 Self::NAME
3504 }
3505 fn note_kinds(&self) -> &'static [&'static str] {
3506 Self::NOTE_KINDS
3507 }
3508 fn entity_kinds(&self) -> &'static [&'static str] {
3509 Self::ENTITY_KINDS
3510 }
3511 fn handlers(&self) -> &'static [HandlerDef] {
3512 Self::HANDLERS
3513 }
3514 fn requires(&self) -> &'static [&'static str] {
3515 Self::REQUIRES
3516 }
3517 async fn dispatch(
3518 &self,
3519 verb: &str,
3520 _: Value,
3521 _: &VerbRegistry,
3522 _: &NamespaceToken,
3523 ) -> Result<Value, RuntimeError> {
3524 Err(RuntimeError::InvalidInput(format!(
3525 "ADepPack has no verbs: {verb}"
3526 )))
3527 }
3528 }
3529
3530 #[async_trait]
3531 impl PackRuntime for BDepPack {
3532 fn name(&self) -> &str {
3533 Self::NAME
3534 }
3535 fn note_kinds(&self) -> &'static [&'static str] {
3536 Self::NOTE_KINDS
3537 }
3538 fn entity_kinds(&self) -> &'static [&'static str] {
3539 Self::ENTITY_KINDS
3540 }
3541 fn handlers(&self) -> &'static [HandlerDef] {
3542 Self::HANDLERS
3543 }
3544 fn requires(&self) -> &'static [&'static str] {
3545 Self::REQUIRES
3546 }
3547 async fn dispatch(
3548 &self,
3549 verb: &str,
3550 _: Value,
3551 _: &VerbRegistry,
3552 _: &NamespaceToken,
3553 ) -> Result<Value, RuntimeError> {
3554 Err(RuntimeError::InvalidInput(format!(
3555 "BDepPack has no verbs: {verb}"
3556 )))
3557 }
3558 }
3559
3560 #[test]
3561 fn test_pack_deps_happy_path() {
3562 let mut builder = VerbRegistryBuilder::new();
3563 builder.register(MemoryDepPack);
3564 builder.register(KgDepPack);
3565 let reg = builder
3566 .build()
3567 .expect("kg_dep satisfies memory_dep dependency");
3568 assert_eq!(reg.pack_requires("memory_dep").unwrap(), &["kg_dep"]);
3569 let names = reg.pack_names();
3570 let kg_pos = names.iter().position(|&n| n == "kg_dep").unwrap();
3571 let mem_pos = names.iter().position(|&n| n == "memory_dep").unwrap();
3572 assert!(
3573 kg_pos < mem_pos,
3574 "kg_dep must be loaded before memory_dep; order: {names:?}"
3575 );
3576 }
3577
3578 #[test]
3579 fn test_pack_deps_missing() {
3580 let mut builder = VerbRegistryBuilder::new();
3581 builder.register(MemoryDepPack);
3582 let err = match builder.build() {
3583 Ok(_) => panic!("expected Err, got Ok"),
3584 Err(e) => e,
3585 };
3586 assert!(
3587 matches!(err, RuntimeError::MissingPackDependency(_)),
3588 "expected MissingPackDependency, got {err:?}"
3589 );
3590 let msg = err.to_string();
3591 assert!(
3592 msg.contains("memory_dep"),
3593 "error must name the dependent pack: {msg}"
3594 );
3595 assert!(
3596 msg.contains("kg_dep"),
3597 "error must name the missing dep: {msg}"
3598 );
3599 }
3600
3601 #[test]
3602 fn test_pack_deps_circular() {
3603 let mut builder = VerbRegistryBuilder::new();
3604 builder.register(ADepPack);
3605 builder.register(BDepPack);
3606 let err = match builder.build() {
3607 Ok(_) => panic!("expected Err, got Ok"),
3608 Err(e) => e,
3609 };
3610 assert!(
3611 matches!(err, RuntimeError::CircularPackDependency(_)),
3612 "expected CircularPackDependency, got {err:?}"
3613 );
3614 let msg = err.to_string();
3615 assert!(msg.contains("pack_a"), "error must name pack_a: {msg}");
3616 assert!(msg.contains("pack_b"), "error must name pack_b: {msg}");
3617 }
3618
3619 #[test]
3620 fn test_pack_deps_no_deps() {
3621 struct NoDepsA;
3622 struct NoDepsB;
3623
3624 impl Pack for NoDepsA {
3625 const NAME: &'static str = "no_deps_a";
3626 const NOTE_KINDS: &'static [&'static str] = &[];
3627 const ENTITY_KINDS: &'static [&'static str] = &[];
3628 const HANDLERS: &'static [HandlerDef] = &[];
3629 }
3630
3631 impl Pack for NoDepsB {
3632 const NAME: &'static str = "no_deps_b";
3633 const NOTE_KINDS: &'static [&'static str] = &[];
3634 const ENTITY_KINDS: &'static [&'static str] = &[];
3635 const HANDLERS: &'static [HandlerDef] = &[];
3636 }
3637
3638 #[async_trait]
3639 impl PackRuntime for NoDepsA {
3640 fn name(&self) -> &str {
3641 Self::NAME
3642 }
3643 fn note_kinds(&self) -> &'static [&'static str] {
3644 Self::NOTE_KINDS
3645 }
3646 fn entity_kinds(&self) -> &'static [&'static str] {
3647 Self::ENTITY_KINDS
3648 }
3649 fn handlers(&self) -> &'static [HandlerDef] {
3650 Self::HANDLERS
3651 }
3652 async fn dispatch(
3653 &self,
3654 verb: &str,
3655 _: Value,
3656 _: &VerbRegistry,
3657 _: &NamespaceToken,
3658 ) -> Result<Value, RuntimeError> {
3659 Err(RuntimeError::InvalidInput(format!("NoDepsA: {verb}")))
3660 }
3661 }
3662
3663 #[async_trait]
3664 impl PackRuntime for NoDepsB {
3665 fn name(&self) -> &str {
3666 Self::NAME
3667 }
3668 fn note_kinds(&self) -> &'static [&'static str] {
3669 Self::NOTE_KINDS
3670 }
3671 fn entity_kinds(&self) -> &'static [&'static str] {
3672 Self::ENTITY_KINDS
3673 }
3674 fn handlers(&self) -> &'static [HandlerDef] {
3675 Self::HANDLERS
3676 }
3677 async fn dispatch(
3678 &self,
3679 verb: &str,
3680 _: Value,
3681 _: &VerbRegistry,
3682 _: &NamespaceToken,
3683 ) -> Result<Value, RuntimeError> {
3684 Err(RuntimeError::InvalidInput(format!("NoDepsB: {verb}")))
3685 }
3686 }
3687
3688 let mut builder = VerbRegistryBuilder::new();
3689 builder.register(NoDepsA);
3690 builder.register(NoDepsB);
3691 let reg = builder.build().expect("packs with REQUIRES=&[] build");
3692 assert_eq!(reg.pack_requires("no_deps_a").unwrap(), &[] as &[&str]);
3693 assert_eq!(reg.pack_requires("no_deps_b").unwrap(), &[] as &[&str]);
3694 }
3695}
3696
3697#[cfg(test)]
3700mod hook_tests {
3701 use super::*;
3702 use async_trait::async_trait;
3703 use khive_types::Pack;
3704 use std::sync::atomic::{AtomicUsize, Ordering};
3705 use std::sync::Mutex as StdMutex;
3706
3707 struct SimplePack;
3708
3709 impl Pack for SimplePack {
3710 const NAME: &'static str = "simple";
3711 const NOTE_KINDS: &'static [&'static str] = &[];
3712 const ENTITY_KINDS: &'static [&'static str] = &[];
3713 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
3714 name: "ping",
3715 description: "ping",
3716 visibility: Visibility::Verb,
3717 category: VerbCategory::Assertive,
3718 params: &[],
3719 }];
3720 }
3721
3722 #[async_trait]
3723 impl PackRuntime for SimplePack {
3724 fn name(&self) -> &str {
3725 SimplePack::NAME
3726 }
3727 fn note_kinds(&self) -> &'static [&'static str] {
3728 SimplePack::NOTE_KINDS
3729 }
3730 fn entity_kinds(&self) -> &'static [&'static str] {
3731 SimplePack::ENTITY_KINDS
3732 }
3733 fn handlers(&self) -> &'static [HandlerDef] {
3734 SimplePack::HANDLERS
3735 }
3736 async fn dispatch(
3737 &self,
3738 verb: &str,
3739 _params: Value,
3740 _registry: &VerbRegistry,
3741 _token: &NamespaceToken,
3742 ) -> Result<Value, RuntimeError> {
3743 Ok(serde_json::json!({ "verb": verb }))
3744 }
3745 }
3746
3747 #[derive(Default)]
3749 struct CountingHook {
3750 calls: AtomicUsize,
3751 last_verb: StdMutex<String>,
3752 }
3753
3754 #[async_trait]
3755 impl DispatchHook for CountingHook {
3756 async fn on_dispatch(&self, view: &EventView) {
3757 self.calls.fetch_add(1, Ordering::SeqCst);
3758 *self.last_verb.lock().unwrap() = view.event.verb.clone();
3759 }
3760 }
3761
3762 #[tokio::test]
3763 async fn dispatch_hook_fires_on_successful_dispatch() {
3764 let hook = Arc::new(CountingHook::default());
3765 let mut builder = VerbRegistryBuilder::new();
3766 builder.register(SimplePack);
3767 builder.with_dispatch_hook(hook.clone());
3768 let reg = builder.build().expect("registry builds");
3769
3770 reg.dispatch("ping", Value::Null).await.unwrap();
3771
3772 assert_eq!(
3773 hook.calls.load(Ordering::SeqCst),
3774 1,
3775 "hook must fire once per successful dispatch"
3776 );
3777 assert_eq!(
3778 hook.last_verb.lock().unwrap().as_str(),
3779 "ping",
3780 "hook event must carry the dispatched verb"
3781 );
3782 }
3783
3784 #[tokio::test]
3785 async fn dispatch_hook_fires_multiple_times() {
3786 let hook = Arc::new(CountingHook::default());
3787 let mut builder = VerbRegistryBuilder::new();
3788 builder.register(SimplePack);
3789 builder.with_dispatch_hook(hook.clone());
3790 let reg = builder.build().expect("registry builds");
3791
3792 reg.dispatch("ping", Value::Null).await.unwrap();
3793 reg.dispatch("ping", Value::Null).await.unwrap();
3794 reg.dispatch("ping", Value::Null).await.unwrap();
3795
3796 assert_eq!(
3797 hook.calls.load(Ordering::SeqCst),
3798 3,
3799 "hook must fire once per successful dispatch"
3800 );
3801 }
3802
3803 #[tokio::test]
3804 async fn dispatch_hook_does_not_fire_on_unknown_verb() {
3805 let hook = Arc::new(CountingHook::default());
3806 let mut builder = VerbRegistryBuilder::new();
3807 builder.register(SimplePack);
3808 builder.with_dispatch_hook(hook.clone());
3809 let reg = builder.build().expect("registry builds");
3810
3811 let _ = reg.dispatch("nonexistent", Value::Null).await;
3812
3813 assert_eq!(
3814 hook.calls.load(Ordering::SeqCst),
3815 0,
3816 "hook must NOT fire for unknown verb (dispatch returns error)"
3817 );
3818 }
3819
3820 #[tokio::test]
3821 async fn dispatch_hook_does_not_fire_on_gate_deny() {
3822 use khive_gate::{Gate, GateDecision, GateError};
3823
3824 #[derive(Debug)]
3825 struct AlwaysDenyGate;
3826 impl Gate for AlwaysDenyGate {
3827 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
3828 Ok(GateDecision::deny("test deny"))
3829 }
3830 }
3831
3832 let hook = Arc::new(CountingHook::default());
3833 let mut builder = VerbRegistryBuilder::new();
3834 builder.register(SimplePack);
3835 builder.with_gate(Arc::new(AlwaysDenyGate));
3836 builder.with_dispatch_hook(hook.clone());
3837 let reg = builder.build().expect("registry builds");
3838
3839 let err = reg.dispatch("ping", Value::Null).await.unwrap_err();
3840 assert!(matches!(err, RuntimeError::PermissionDenied { .. }));
3841
3842 assert_eq!(
3843 hook.calls.load(Ordering::SeqCst),
3844 0,
3845 "hook must NOT fire when gate denies dispatch"
3846 );
3847 }
3848
3849 #[tokio::test]
3850 async fn dispatch_hook_event_carries_namespace_from_params() {
3851 let hook = Arc::new(CountingHook::default());
3852
3853 #[derive(Default)]
3854 struct NsCapturingHook {
3855 ns: StdMutex<String>,
3856 }
3857
3858 #[async_trait]
3859 impl DispatchHook for NsCapturingHook {
3860 async fn on_dispatch(&self, view: &EventView) {
3861 *self.ns.lock().unwrap() = view.event.namespace.clone();
3862 }
3863 }
3864
3865 let ns_hook = Arc::new(NsCapturingHook::default());
3866 let mut builder = VerbRegistryBuilder::new();
3867 builder.register(SimplePack);
3868 builder.with_dispatch_hook(ns_hook.clone());
3869 let reg = builder.build().expect("registry builds");
3870
3871 reg.dispatch("ping", serde_json::json!({"namespace": "tenant-abc"}))
3872 .await
3873 .unwrap();
3874
3875 assert_eq!(
3876 ns_hook.ns.lock().unwrap().as_str(),
3877 "tenant-abc",
3878 "dispatch hook event must carry the resolved namespace"
3879 );
3880
3881 drop(hook);
3883 }
3884
3885 #[tokio::test]
3886 async fn no_dispatch_hook_configured_dispatch_succeeds() {
3887 let mut builder = VerbRegistryBuilder::new();
3889 builder.register(SimplePack);
3890 let reg = builder.build().expect("registry builds");
3892
3893 let res = reg.dispatch("ping", Value::Null).await.unwrap();
3894 assert_eq!(res["verb"], "ping");
3895 }
3896}
3897
3898#[cfg(test)]
3901mod help_tests {
3902 use super::*;
3903 use async_trait::async_trait;
3904 use khive_types::Pack;
3905 use std::sync::{
3906 atomic::{AtomicUsize, Ordering},
3907 Arc,
3908 };
3909
3910 static CREATE_PARAMS: [ParamDef; 2] = [
3915 ParamDef {
3916 name: "kind",
3917 param_type: "string",
3918 required: true,
3919 description: "Granular kind (concept | document | ...).",
3920 },
3921 ParamDef {
3922 name: "name",
3923 param_type: "string",
3924 required: false,
3925 description: "Human-readable name.",
3926 },
3927 ];
3928
3929 static RECALL_PARAMS: [ParamDef; 2] = [
3930 ParamDef {
3931 name: "query",
3932 param_type: "string",
3933 required: true,
3934 description: "Semantic recall query.",
3935 },
3936 ParamDef {
3937 name: "limit",
3938 param_type: "integer",
3939 required: false,
3940 description: "Maximum memories to return.",
3941 },
3942 ];
3943
3944 static EMBED_PARAMS: [ParamDef; 0] = [];
3947
3948 struct HelpPack {
3949 invocations: Arc<AtomicUsize>,
3950 }
3951
3952 impl Pack for HelpPack {
3953 const NAME: &'static str = "helptest";
3954 const NOTE_KINDS: &'static [&'static str] = &[];
3955 const ENTITY_KINDS: &'static [&'static str] = &[];
3956 const HANDLERS: &'static [HandlerDef] = &[
3957 HandlerDef {
3958 name: "create",
3959 description: "Create an entity or note",
3960 visibility: Visibility::Verb,
3961 category: VerbCategory::Commissive,
3962 params: &CREATE_PARAMS,
3963 },
3964 HandlerDef {
3965 name: "recall",
3966 description: "Recall memory notes with decay-aware hybrid ranking",
3967 visibility: Visibility::Verb,
3968 category: VerbCategory::Assertive,
3969 params: &RECALL_PARAMS,
3970 },
3971 HandlerDef {
3974 name: "recall.embed",
3975 description: "Return the embedding vector used by memory recall",
3976 visibility: Visibility::Subhandler,
3977 category: VerbCategory::Assertive,
3978 params: &EMBED_PARAMS,
3979 },
3980 ];
3981 }
3982
3983 #[async_trait]
3984 impl PackRuntime for HelpPack {
3985 fn name(&self) -> &str {
3986 HelpPack::NAME
3987 }
3988 fn note_kinds(&self) -> &'static [&'static str] {
3989 HelpPack::NOTE_KINDS
3990 }
3991 fn entity_kinds(&self) -> &'static [&'static str] {
3992 HelpPack::ENTITY_KINDS
3993 }
3994 fn handlers(&self) -> &'static [HandlerDef] {
3995 HelpPack::HANDLERS
3996 }
3997 async fn dispatch(
3998 &self,
3999 verb: &str,
4000 _params: Value,
4001 _registry: &VerbRegistry,
4002 _token: &NamespaceToken,
4003 ) -> Result<Value, RuntimeError> {
4004 self.invocations.fetch_add(1, Ordering::SeqCst);
4005 Ok(serde_json::json!({ "pack": "helptest", "verb": verb }))
4006 }
4007 }
4008
4009 fn build_help_registry(invocations: Arc<AtomicUsize>) -> VerbRegistry {
4010 let mut builder = VerbRegistryBuilder::new();
4011 builder.register(HelpPack { invocations });
4012 builder.build().expect("help registry builds")
4013 }
4014
4015 #[tokio::test]
4018 async fn test_help_true_returns_schema_for_kg_create() {
4019 let invocations = Arc::new(AtomicUsize::new(0));
4020 let reg = build_help_registry(invocations.clone());
4021
4022 let result = reg
4023 .dispatch("create", serde_json::json!({ "help": true }))
4024 .await
4025 .expect("help=true must succeed for a known verb");
4026
4027 assert_eq!(result["verb"], "create", "envelope must name the verb");
4029 assert_eq!(
4030 result["pack"], "helptest",
4031 "envelope must name the owning pack"
4032 );
4033 assert!(
4034 result["description"].as_str().is_some(),
4035 "description must be a string"
4036 );
4037
4038 let params = result["params"]
4040 .as_array()
4041 .expect("params must be a JSON array");
4042 assert!(!params.is_empty(), "params array must not be empty");
4043
4044 let kind_param = params.iter().find(|p| p["name"] == "kind");
4046 assert!(
4047 kind_param.is_some(),
4048 "params array must include the 'kind' parameter"
4049 );
4050 let kind_param = kind_param.unwrap();
4051 assert_eq!(
4052 kind_param["required"],
4053 serde_json::json!(true),
4054 "'kind' must be required"
4055 );
4056 assert_eq!(kind_param["type"], "string", "'kind' type must be 'string'");
4057 }
4058
4059 #[tokio::test]
4061 async fn test_help_true_returns_schema_for_recall() {
4062 let invocations = Arc::new(AtomicUsize::new(0));
4063 let reg = build_help_registry(invocations.clone());
4064
4065 let result = reg
4066 .dispatch("recall", serde_json::json!({ "help": true }))
4067 .await
4068 .expect("help=true must succeed for recall");
4069
4070 assert_eq!(result["verb"], "recall");
4071 assert_eq!(result["pack"], "helptest");
4072
4073 let params = result["params"]
4074 .as_array()
4075 .expect("params must be a JSON array");
4076
4077 let query_param = params.iter().find(|p| p["name"] == "query");
4079 assert!(query_param.is_some(), "params must include 'query'");
4080 let query_param = query_param.unwrap();
4081 assert_eq!(
4082 query_param["required"],
4083 serde_json::json!(true),
4084 "'query' must be required"
4085 );
4086
4087 let limit_param = params.iter().find(|p| p["name"] == "limit");
4089 assert!(limit_param.is_some(), "params must include 'limit'");
4090 let limit_param = limit_param.unwrap();
4091 assert_eq!(
4092 limit_param["required"],
4093 serde_json::json!(false),
4094 "'limit' must be optional"
4095 );
4096 }
4097
4098 #[tokio::test]
4101 async fn test_help_true_does_not_execute_the_verb() {
4102 let invocations = Arc::new(AtomicUsize::new(0));
4103 let reg = build_help_registry(invocations.clone());
4104
4105 reg.dispatch("create", serde_json::json!({ "help": true }))
4107 .await
4108 .expect("help=true must succeed");
4109 reg.dispatch("recall", serde_json::json!({ "help": true }))
4110 .await
4111 .expect("help=true must succeed");
4112
4113 assert_eq!(
4114 invocations.load(Ordering::SeqCst),
4115 0,
4116 "pack dispatch MUST NOT be invoked when help=true; \
4117 got {} invocation(s)",
4118 invocations.load(Ordering::SeqCst)
4119 );
4120
4121 reg.dispatch("create", serde_json::json!({}))
4123 .await
4124 .expect("normal dispatch must succeed");
4125 assert_eq!(
4126 invocations.load(Ordering::SeqCst),
4127 1,
4128 "pack dispatch must fire exactly once for a normal call"
4129 );
4130 }
4131
4132 #[tokio::test]
4141 async fn help_true_on_subhandler_returns_callable_via_mcp_false() {
4142 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
4143
4144 let result = reg
4145 .dispatch("recall.embed", serde_json::json!({ "help": true }))
4146 .await
4147 .expect("help=true on subhandler must succeed (no permission check on help path)");
4148
4149 assert_eq!(
4150 result["callable_via_mcp"],
4151 serde_json::json!(false),
4152 "subhandler help must carry callable_via_mcp: false"
4153 );
4154 assert_eq!(
4155 result["visibility"], "internal",
4156 "subhandler help must carry visibility: internal"
4157 );
4158 assert_eq!(result["verb"], "recall.embed");
4161 assert_eq!(result["pack"], "helptest");
4162 }
4163
4164 #[tokio::test]
4166 async fn help_true_on_public_verb_does_not_have_callable_via_mcp_false() {
4167 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
4168
4169 let result = reg
4170 .dispatch("create", serde_json::json!({ "help": true }))
4171 .await
4172 .expect("help=true on public verb must succeed");
4173
4174 assert_ne!(
4176 result.get("callable_via_mcp"),
4177 Some(&serde_json::json!(false)),
4178 "public verb help must NOT carry callable_via_mcp: false"
4179 );
4180 assert_ne!(
4182 result.get("visibility"),
4183 Some(&serde_json::json!("internal")),
4184 "public verb help must NOT carry visibility: internal"
4185 );
4186 }
4187
4188 #[tokio::test]
4190 async fn help_true_on_unknown_verb_returns_error() {
4191 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
4192
4193 let err = reg
4194 .dispatch("nonexistent_verb", serde_json::json!({ "help": true }))
4195 .await
4196 .unwrap_err();
4197
4198 assert!(
4199 matches!(err, RuntimeError::InvalidInput(_)),
4200 "help=true on unknown verb must return InvalidInput, got {err:?}"
4201 );
4202 let msg = err.to_string();
4203 assert!(
4204 msg.contains("nonexistent_verb"),
4205 "error must name the unknown verb: {msg}"
4206 );
4207 }
4208
4209 #[tokio::test]
4211 async fn help_true_on_subhandler_includes_params_field() {
4212 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
4213
4214 let result = reg
4215 .dispatch("recall.embed", serde_json::json!({ "help": true }))
4216 .await
4217 .expect("help=true on subhandler must succeed");
4218
4219 let params = result
4221 .get("params")
4222 .expect("subhandler help must include 'params' field");
4223 assert!(
4224 params.is_array(),
4225 "subhandler help params must be a JSON array"
4226 );
4227 }
4228
4229 #[tokio::test]
4235 async fn help_true_unknown_verb_available_list_excludes_subhandlers() {
4236 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
4237
4238 let err = reg
4239 .dispatch("not_a_verb", serde_json::json!({ "help": true }))
4240 .await
4241 .unwrap_err();
4242
4243 let msg = err.to_string();
4244 assert!(
4247 !msg.contains("recall.embed"),
4248 "unknown-verb help error must not advertise subhandler recall.embed: {msg}"
4249 );
4250 assert!(
4252 msg.contains("create"),
4253 "unknown-verb help error must still list public verb 'create': {msg}"
4254 );
4255 assert!(
4256 msg.contains("recall"),
4257 "unknown-verb help error must still list public verb 'recall': {msg}"
4258 );
4259 }
4260
4261 #[tokio::test]
4263 async fn dispatch_unknown_verb_available_list_excludes_subhandlers() {
4264 let reg = build_help_registry(Arc::new(AtomicUsize::new(0)));
4265
4266 let err = reg
4267 .dispatch("not_a_verb", serde_json::json!({}))
4268 .await
4269 .unwrap_err();
4270
4271 let msg = err.to_string();
4272 assert!(
4275 !msg.contains("recall.embed"),
4276 "dispatch unknown-verb error must not advertise subhandler recall.embed: {msg}"
4277 );
4278 assert!(
4280 msg.contains("create"),
4281 "dispatch unknown-verb error must still list public verb 'create': {msg}"
4282 );
4283 assert!(
4284 msg.contains("recall"),
4285 "dispatch unknown-verb error must still list public verb 'recall': {msg}"
4286 );
4287 }
4288
4289 struct SchemaPack {
4293 pack_name: &'static str,
4294 statements: &'static [&'static str],
4295 }
4296
4297 impl Pack for SchemaPack {
4298 const NAME: &'static str = "schema-pack";
4299 const NOTE_KINDS: &'static [&'static str] = &[];
4300 const ENTITY_KINDS: &'static [&'static str] = &[];
4301 const HANDLERS: &'static [HandlerDef] = &[];
4302 }
4303
4304 #[async_trait]
4305 impl PackRuntime for SchemaPack {
4306 fn name(&self) -> &str {
4307 self.pack_name
4308 }
4309 fn note_kinds(&self) -> &'static [&'static str] {
4310 &[]
4311 }
4312 fn entity_kinds(&self) -> &'static [&'static str] {
4313 &[]
4314 }
4315 fn handlers(&self) -> &'static [HandlerDef] {
4316 &[]
4317 }
4318 fn schema_plan(&self) -> SchemaPlan {
4319 SchemaPlan {
4320 pack: self.pack_name,
4321 statements: self.statements,
4322 }
4323 }
4324 async fn dispatch(
4325 &self,
4326 verb: &str,
4327 _params: Value,
4328 _registry: &VerbRegistry,
4329 _token: &NamespaceToken,
4330 ) -> Result<Value, RuntimeError> {
4331 Ok(serde_json::json!({ "pack": self.pack_name, "verb": verb }))
4332 }
4333 }
4334
4335 #[test]
4338 fn all_schema_plans_named_returns_correct_pairs() {
4339 let mut builder = VerbRegistryBuilder::new();
4340 builder.register_boxed(Box::new(SchemaPack {
4341 pack_name: "alpha",
4342 statements: &["CREATE TABLE IF NOT EXISTS t_alpha (id INTEGER PRIMARY KEY)"],
4343 }));
4344 builder.register_boxed(Box::new(SchemaPack {
4345 pack_name: "beta",
4346 statements: &[],
4347 }));
4348 let reg = builder.build().expect("registry builds");
4349
4350 let named = reg.all_schema_plans_named();
4351 assert_eq!(named.len(), 2);
4352
4353 let alpha_entry = named.iter().find(|(n, _)| *n == "alpha");
4354 let beta_entry = named.iter().find(|(n, _)| *n == "beta");
4355
4356 assert!(alpha_entry.is_some(), "alpha must appear in named plans");
4357 assert!(beta_entry.is_some(), "beta must appear in named plans");
4358
4359 let (_, alpha_plan) = alpha_entry.unwrap();
4360 assert_eq!(alpha_plan.statements.len(), 1);
4361 assert!(!alpha_plan.is_empty());
4362
4363 let (_, beta_plan) = beta_entry.unwrap();
4364 assert!(beta_plan.is_empty());
4365 }
4366
4367 #[tokio::test]
4384 async fn apply_schema_plans_with_map_routes_to_correct_backend() {
4385 use khive_storage::types::{SqlStatement, SqlValue};
4386
4387 let default_backend = khive_db::StorageBackend::memory().expect("default memory backend");
4388 let pack_backend =
4389 khive_db::StorageBackend::memory().expect("pack-specific memory backend");
4390
4391 let mut builder = VerbRegistryBuilder::new();
4392 builder.register_boxed(Box::new(SchemaPack {
4393 pack_name: "routed",
4394 statements: &["CREATE TABLE IF NOT EXISTS t_routed (id INTEGER PRIMARY KEY)"],
4395 }));
4396 let reg = builder.build().expect("registry builds");
4397
4398 let mut backend_map: HashMap<&str, &khive_db::StorageBackend> = HashMap::new();
4399 backend_map.insert("routed", &pack_backend);
4400
4401 reg.apply_schema_plans_with_map(&backend_map, &default_backend)
4402 .expect("schema application must not collide");
4403
4404 let mut writer = pack_backend.sql().writer().await.expect("writer");
4406 let result = writer
4407 .execute(SqlStatement {
4408 sql: "INSERT INTO t_routed (id) VALUES (?1)".into(),
4409 params: vec![SqlValue::Integer(1)],
4410 label: None,
4411 })
4412 .await;
4413 assert!(
4414 result.is_ok(),
4415 "t_routed must exist on pack_backend after routing: {result:?}"
4416 );
4417
4418 let mut default_writer = default_backend.sql().writer().await.expect("writer");
4420 let default_result = default_writer
4421 .execute(SqlStatement {
4422 sql: "INSERT INTO t_routed (id) VALUES (?1)".into(),
4423 params: vec![SqlValue::Integer(2)],
4424 label: None,
4425 })
4426 .await;
4427 assert!(
4428 default_result.is_err(),
4429 "t_routed must NOT exist on default_backend (table should not be there)"
4430 );
4431 }
4432
4433 #[tokio::test]
4436 async fn apply_schema_plans_with_map_falls_back_to_default_for_unmapped_packs() {
4437 use khive_storage::types::{SqlStatement, SqlValue};
4438
4439 let default_backend = khive_db::StorageBackend::memory().expect("default memory backend");
4440
4441 let mut builder = VerbRegistryBuilder::new();
4442 builder.register_boxed(Box::new(SchemaPack {
4443 pack_name: "unmapped",
4444 statements: &["CREATE TABLE IF NOT EXISTS t_unmapped (id INTEGER PRIMARY KEY)"],
4445 }));
4446 let reg = builder.build().expect("registry builds");
4447
4448 let backend_map: HashMap<&str, &khive_db::StorageBackend> = HashMap::new();
4449 reg.apply_schema_plans_with_map(&backend_map, &default_backend)
4450 .expect("schema application must not collide");
4451
4452 let mut writer = default_backend.sql().writer().await.expect("writer");
4454 let result = writer
4455 .execute(SqlStatement {
4456 sql: "INSERT INTO t_unmapped (id) VALUES (?1)".into(),
4457 params: vec![SqlValue::Integer(1)],
4458 label: None,
4459 })
4460 .await;
4461 assert!(
4462 result.is_ok(),
4463 "t_unmapped must exist on default_backend for unmapped pack: {result:?}"
4464 );
4465 }
4466
4467 #[test]
4472 fn apply_schema_plans_with_map_collision_is_an_error() {
4473 let backend = khive_db::StorageBackend::memory().expect("memory backend");
4474 let empty_map: HashMap<&str, &khive_db::StorageBackend> = HashMap::new();
4475
4476 let mut builder = VerbRegistryBuilder::new();
4477 builder.register_boxed(Box::new(SchemaPack {
4478 pack_name: "pack_alpha",
4479 statements: &["CREATE TABLE IF NOT EXISTS collision_table (id INTEGER PRIMARY KEY)"],
4480 }));
4481 builder.register_boxed(Box::new(SchemaPack {
4482 pack_name: "pack_beta",
4483 statements: &["CREATE TABLE IF NOT EXISTS collision_table (id INTEGER PRIMARY KEY)"],
4484 }));
4485 let registry = builder.build().expect("registry builds");
4486
4487 let result = registry.apply_schema_plans_with_map(&empty_map, &backend);
4488 assert!(
4489 result.is_err(),
4490 "two packs declaring the same table on the same backend must produce a collision error"
4491 );
4492 let err = result.unwrap_err();
4493 let msg = err.to_string();
4494 assert!(
4495 msg.contains("pack_alpha"),
4496 "collision error must name first pack; got: {msg}"
4497 );
4498 assert!(
4499 msg.contains("pack_beta"),
4500 "collision error must name second pack; got: {msg}"
4501 );
4502 assert!(
4503 msg.contains("collision_table"),
4504 "collision error must name the table; got: {msg}"
4505 );
4506 }
4507}