1use std::collections::{HashMap, HashSet, VecDeque};
15use std::sync::Arc;
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 VerbCategory, 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 async fn dispatch(
182 &self,
183 verb: &str,
184 params: Value,
185 registry: &VerbRegistry,
186 token: &NamespaceToken,
187 ) -> Result<Value, RuntimeError>;
188}
189
190#[async_trait]
204pub trait KindHook: Send + Sync + std::fmt::Debug {
205 async fn prepare_create(
211 &self,
212 runtime: &KhiveRuntime,
213 args: &mut Value,
214 ) -> Result<(), RuntimeError>;
215
216 async fn after_create(
225 &self,
226 runtime: &KhiveRuntime,
227 id: uuid::Uuid,
228 args: &Value,
229 ) -> Result<(), RuntimeError>;
230}
231
232pub struct VerbRegistryBuilder {
237 packs: Vec<Box<dyn PackRuntime>>,
238 gate: GateRef,
239 default_namespace: String,
240 event_store: Option<Arc<dyn EventStore>>,
247 dispatch_hook: Option<Arc<dyn DispatchHook>>,
253}
254
255impl VerbRegistryBuilder {
256 pub fn new() -> Self {
257 Self {
258 packs: Vec::new(),
259 gate: std::sync::Arc::new(AllowAllGate),
260 default_namespace: Namespace::local().as_str().to_string(),
261 event_store: None,
262 dispatch_hook: None,
263 }
264 }
265
266 pub fn register<P: khive_types::Pack + PackRuntime + 'static>(&mut self, pack: P) -> &mut Self {
269 self.packs.push(Box::new(pack));
270 self
271 }
272
273 pub(crate) fn register_boxed(&mut self, pack: Box<dyn PackRuntime>) -> &mut Self {
280 self.packs.push(pack);
281 self
282 }
283
284 pub fn with_gate(&mut self, gate: GateRef) -> &mut Self {
289 self.gate = gate;
290 self
291 }
292
293 pub fn with_default_namespace(&mut self, ns: impl Into<String>) -> &mut Self {
298 self.default_namespace = ns.into();
299 self
300 }
301
302 pub fn with_event_store(&mut self, store: Arc<dyn EventStore>) -> &mut Self {
311 self.event_store = Some(store);
312 self
313 }
314
315 pub fn with_dispatch_hook(&mut self, hook: Arc<dyn DispatchHook>) -> &mut Self {
326 self.dispatch_hook = Some(hook);
327 self
328 }
329
330 pub fn build(self) -> Result<VerbRegistry, RuntimeError> {
336 let packs = self.packs;
337 let mut name_to_idx: HashMap<&str, usize> = HashMap::with_capacity(packs.len());
338 for (idx, pack) in packs.iter().enumerate() {
339 if let Some(prev_idx) = name_to_idx.insert(pack.name(), idx) {
340 return Err(RuntimeError::PackRedeclared {
341 name: pack.name().to_string(),
342 first_idx: prev_idx,
343 second_idx: idx,
344 });
345 }
346 }
347
348 let mut missing: Vec<MissingPackDependency> = Vec::new();
349 let mut indegree = vec![0usize; packs.len()];
350 let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); packs.len()];
351
352 for (idx, pack) in packs.iter().enumerate() {
353 for &requires in pack.requires() {
354 match name_to_idx.get(requires).copied() {
355 Some(dep_idx) => {
356 dependents[dep_idx].push(idx);
357 indegree[idx] += 1;
358 }
359 None => missing.push(MissingPackDependency {
360 from: pack.name().to_string(),
361 requires: requires.to_string(),
362 }),
363 }
364 }
365 }
366
367 if !missing.is_empty() {
368 return if missing.len() == 1 {
369 Err(RuntimeError::MissingPackDependency(missing.remove(0)))
370 } else {
371 Err(RuntimeError::MissingPackDependencies(
372 MissingPackDependencies { missing },
373 ))
374 };
375 }
376
377 let mut ready: VecDeque<usize> = indegree
378 .iter()
379 .enumerate()
380 .filter_map(|(idx, degree)| (*degree == 0).then_some(idx))
381 .collect();
382 let mut ordered_indices = Vec::with_capacity(packs.len());
383
384 while let Some(idx) = ready.pop_front() {
385 ordered_indices.push(idx);
386 for &dep_idx in &dependents[idx] {
387 indegree[dep_idx] -= 1;
388 if indegree[dep_idx] == 0 {
389 ready.push_back(dep_idx);
390 }
391 }
392 }
393
394 if ordered_indices.len() != packs.len() {
395 let cycle_nodes: HashSet<usize> = indegree
396 .iter()
397 .enumerate()
398 .filter_map(|(idx, degree)| (*degree > 0).then_some(idx))
399 .collect();
400 let cycle = find_pack_dependency_cycle(&packs, &name_to_idx, &cycle_nodes);
401 return Err(RuntimeError::CircularPackDependency(
402 CircularPackDependency { cycle },
403 ));
404 }
405
406 let mut slots: Vec<Option<Box<dyn PackRuntime>>> = packs.into_iter().map(Some).collect();
407 let ordered_packs: Vec<Box<dyn PackRuntime>> = ordered_indices
408 .into_iter()
409 .map(|idx| slots[idx].take().expect("topological index must exist"))
410 .collect();
411
412 validate_unique_note_kinds(&ordered_packs)?;
413 validate_unique_verb_names(&ordered_packs)?;
414
415 Ok(VerbRegistry {
416 packs: Arc::new(ordered_packs),
417 gate: self.gate,
418 default_namespace: self.default_namespace,
419 event_store: self.event_store,
420 dispatch_hook: self.dispatch_hook,
421 })
422 }
423}
424
425fn validate_unique_note_kinds(packs: &[Box<dyn PackRuntime>]) -> Result<(), RuntimeError> {
431 let mut seen: HashMap<&str, &str> = HashMap::new();
432 for pack in packs {
433 for &kind in pack.note_kinds() {
434 if let Some(first_pack) = seen.insert(kind, pack.name()) {
435 return Err(RuntimeError::InvalidInput(format!(
436 "duplicate note kind {kind:?}: claimed by both {first_pack:?} and {:?}",
437 pack.name()
438 )));
439 }
440 }
441 }
442 Ok(())
443}
444
445fn validate_unique_verb_names(packs: &[Box<dyn PackRuntime>]) -> Result<(), RuntimeError> {
453 let mut seen: HashMap<&str, &str> = HashMap::new();
454 for pack in packs {
455 for handler in pack.handlers() {
456 if !matches!(handler.visibility, Visibility::Verb) {
457 continue;
458 }
459 if let Some(first_pack) = seen.insert(handler.name, pack.name()) {
460 return Err(RuntimeError::VerbCollision {
461 verb: handler.name.to_string(),
462 first_pack: first_pack.to_string(),
463 second_pack: pack.name().to_string(),
464 });
465 }
466 }
467 }
468 Ok(())
469}
470
471fn find_pack_dependency_cycle(
472 packs: &[Box<dyn PackRuntime>],
473 name_to_idx: &HashMap<&str, usize>,
474 cycle_nodes: &HashSet<usize>,
475) -> Vec<String> {
476 fn visit(
477 idx: usize,
478 packs: &[Box<dyn PackRuntime>],
479 name_to_idx: &HashMap<&str, usize>,
480 cycle_nodes: &HashSet<usize>,
481 visiting: &mut Vec<usize>,
482 visited: &mut HashSet<usize>,
483 ) -> Option<Vec<String>> {
484 if let Some(pos) = visiting.iter().position(|&seen| seen == idx) {
485 let mut cycle: Vec<String> = visiting[pos..]
486 .iter()
487 .map(|&i| packs[i].name().to_string())
488 .collect();
489 cycle.push(packs[idx].name().to_string());
490 return Some(cycle);
491 }
492 if !visited.insert(idx) {
493 return None;
494 }
495 visiting.push(idx);
496 for &req in packs[idx].requires() {
497 let Some(&dep_idx) = name_to_idx.get(req) else {
498 continue;
499 };
500 if cycle_nodes.contains(&dep_idx) {
501 if let Some(cycle) =
502 visit(dep_idx, packs, name_to_idx, cycle_nodes, visiting, visited)
503 {
504 return Some(cycle);
505 }
506 }
507 }
508 visiting.pop();
509 None
510 }
511
512 let mut visited = HashSet::new();
513 for &idx in cycle_nodes {
514 let mut visiting = Vec::new();
515 if let Some(cycle) = visit(
516 idx,
517 packs,
518 name_to_idx,
519 cycle_nodes,
520 &mut visiting,
521 &mut visited,
522 ) {
523 return cycle;
524 }
525 }
526 cycle_nodes
527 .iter()
528 .map(|&idx| packs[idx].name().to_string())
529 .collect()
530}
531
532impl Default for VerbRegistryBuilder {
533 fn default() -> Self {
534 Self::new()
535 }
536}
537
538#[derive(Clone)]
542pub struct VerbRegistry {
543 packs: std::sync::Arc<Vec<Box<dyn PackRuntime>>>,
544 gate: GateRef,
545 default_namespace: String,
546 event_store: Option<Arc<dyn EventStore>>,
548 dispatch_hook: Option<Arc<dyn DispatchHook>>,
550}
551
552impl VerbRegistry {
553 pub async fn dispatch(&self, verb: &str, params: Value) -> Result<Value, RuntimeError> {
584 let ns_str: String = params
587 .get("namespace")
588 .and_then(Value::as_str)
589 .map(str::to_string)
590 .unwrap_or_else(|| self.default_namespace.clone());
591 let ns = Namespace::parse(&ns_str)
592 .map_err(|e| RuntimeError::InvalidInput(format!("invalid namespace: {e}")))?;
593 let gate_req = GateRequest::new(ActorRef::anonymous(), ns, verb, params.clone());
594
595 let gate_blocked = match self.gate.check(&gate_req) {
601 Ok(decision) => {
602 let is_deny = matches!(decision, GateDecision::Deny { .. });
603
604 let audit = AuditEvent::from_check(&gate_req, &decision, self.gate.impl_name());
606 tracing::info!(
607 audit_event = %serde_json::to_string(&audit)
608 .unwrap_or_else(|_| "{\"error\":\"serialize\"}".into()),
609 "gate.check"
610 );
611
612 if let Some(store) = &self.event_store {
614 let outcome = if is_deny {
615 EventOutcome::Denied
616 } else {
617 EventOutcome::Success
618 };
619 let audit_data = serde_json::to_value(&audit).unwrap_or_else(|e| {
620 tracing::warn!(error = %e, "failed to serialize AuditEvent for EventStore");
621 serde_json::Value::Null
622 });
623 let storage_event = Event::new(
624 gate_req.namespace.as_str(),
625 verb,
626 EventKind::Audit,
627 SubstrateKind::Event,
628 format!("{}:{}", gate_req.actor.kind, gate_req.actor.id),
629 )
630 .with_outcome(outcome)
631 .with_payload(audit_data);
632 if let Err(store_err) = store.append_event(storage_event).await {
633 tracing::warn!(
634 verb,
635 error = %store_err,
636 "audit event store write failed (non-fatal)"
637 );
638 }
639 }
640
641 if is_deny {
642 let reason = match decision {
643 GateDecision::Deny { reason } => reason,
644 _ => String::new(),
645 };
646 Some(reason)
647 } else {
648 None
649 }
650 }
651 Err(err) => {
652 tracing::warn!(verb, error = %err, "gate check failed (fail-open)");
655 None
656 }
657 };
658
659 if let Some(reason) = gate_blocked {
661 return Err(RuntimeError::PermissionDenied {
662 verb: verb.to_string(),
663 reason,
664 });
665 }
666
667 let token = NamespaceToken::mint_authorized(
670 Namespace::parse(&ns_str)
671 .map_err(|e| RuntimeError::InvalidInput(format!("invalid namespace: {e}")))?,
672 ActorRef::anonymous(),
673 );
674
675 for pack in self.packs.iter() {
676 if pack.handlers().iter().any(|v| v.name == verb) {
677 let result = pack.dispatch(verb, params, self, &token).await;
678
679 if let (Ok(_), Some(hook)) = (&result, &self.dispatch_hook) {
681 let dispatch_event = Event::new(
682 ns_str.as_str(),
683 verb,
684 EventKind::Audit,
685 SubstrateKind::Event,
686 pack.name(),
687 )
688 .with_outcome(EventOutcome::Success);
689 let dispatch_view = EventView {
690 event: dispatch_event,
691 observations: Vec::new(),
692 };
693 let hook = Arc::clone(hook);
694 hook.on_dispatch(&dispatch_view).await;
695 }
696
697 return result;
698 }
699 }
700 let available: Vec<&str> = self
701 .packs
702 .iter()
703 .flat_map(|p| p.handlers().iter().map(|v| v.name))
704 .collect();
705 Err(RuntimeError::InvalidInput(format!(
706 "unknown verb {verb:?}; available: {}",
707 available.join(", ")
708 )))
709 }
710
711 pub fn find_kind_hook(&self, kind: &str) -> Option<Arc<dyn KindHook>> {
718 for pack in self.packs.iter() {
719 let owns = pack.note_kinds().contains(&kind) || pack.entity_kinds().contains(&kind);
720 if owns {
721 if let Some(hook) = pack.kind_hook(kind) {
722 return Some(hook);
723 }
724 }
725 }
726 None
727 }
728
729 pub fn all_verbs(&self) -> Vec<&'static HandlerDef> {
736 self.packs
737 .iter()
738 .flat_map(|p| p.handlers().iter())
739 .filter(|h| matches!(h.visibility, Visibility::Verb))
740 .collect()
741 }
742
743 pub fn all_verbs_with_names(&self) -> Vec<(&str, &'static HandlerDef)> {
750 self.packs
751 .iter()
752 .flat_map(|p| p.handlers().iter().map(move |v| (p.name(), v)))
753 .filter(|(_, h)| matches!(h.visibility, Visibility::Verb))
754 .collect()
755 }
756
757 pub fn all_handlers_with_names(&self) -> Vec<(&str, &'static HandlerDef)> {
763 self.packs
764 .iter()
765 .flat_map(|p| p.handlers().iter().map(move |v| (p.name(), v)))
766 .collect()
767 }
768
769 pub fn all_note_kinds(&self) -> Vec<&'static str> {
772 let mut seen = std::collections::HashSet::new();
773 self.packs
774 .iter()
775 .flat_map(|p| p.note_kinds().iter().copied())
776 .filter(|k| seen.insert(*k))
777 .collect()
778 }
779
780 pub fn all_entity_kinds(&self) -> Vec<&'static str> {
783 let mut seen = std::collections::HashSet::new();
784 self.packs
785 .iter()
786 .flat_map(|p| p.entity_kinds().iter().copied())
787 .filter(|k| seen.insert(*k))
788 .collect()
789 }
790
791 pub fn pack_names(&self) -> Vec<&str> {
793 self.packs.iter().map(|p| p.name()).collect()
794 }
795
796 pub fn pack_requires(&self, name: &str) -> Option<&'static [&'static str]> {
798 self.packs
799 .iter()
800 .find(|p| p.name() == name)
801 .map(|p| p.requires())
802 }
803
804 pub fn pack_note_kinds(&self, name: &str) -> Option<&'static [&'static str]> {
809 self.packs
810 .iter()
811 .find(|p| p.name() == name)
812 .map(|p| p.note_kinds())
813 }
814
815 pub fn pack_entity_kinds(&self, name: &str) -> Option<&'static [&'static str]> {
820 self.packs
821 .iter()
822 .find(|p| p.name() == name)
823 .map(|p| p.entity_kinds())
824 }
825
826 pub fn pack_verbs(&self, name: &str) -> Option<&'static [HandlerDef]> {
832 self.packs
833 .iter()
834 .find(|p| p.name() == name)
835 .map(|p| p.handlers())
836 }
837
838 pub fn all_edge_rules(&self) -> Vec<EdgeEndpointRule> {
844 self.packs
845 .iter()
846 .flat_map(|p| p.edge_rules().iter().copied())
847 .collect()
848 }
849
850 pub fn all_note_kind_specs(&self) -> Vec<&'static NoteKindSpec> {
854 self.packs
855 .iter()
856 .flat_map(|p| p.note_kind_specs().iter())
857 .collect()
858 }
859
860 pub fn all_validation_rules(&self) -> Vec<&'static ValidationRule> {
866 self.packs
867 .iter()
868 .flat_map(|p| p.validation_rules().iter())
869 .collect()
870 }
871
872 pub fn all_schema_plans(&self) -> Vec<SchemaPlan> {
879 self.packs.iter().map(|p| p.schema_plan()).collect()
880 }
881
882 pub fn apply_schema_plans(&self, backend: &khive_db::StorageBackend) {
895 for plan in self.all_schema_plans() {
896 if plan.is_empty() {
897 continue;
898 }
899 if let Err(e) = backend.apply_pack_ddl_statements(plan.statements) {
900 tracing::warn!(
901 pack = plan.pack,
902 error = %e,
903 "failed to apply pack schema plan at startup (non-fatal)"
904 );
905 }
906 }
907 }
908}
909
910pub trait PackFactory: Send + Sync + 'static {
920 fn name(&self) -> &'static str;
922
923 fn requires(&self) -> &'static [&'static str] {
930 &[]
931 }
932
933 fn create(&self, runtime: KhiveRuntime) -> Box<dyn PackRuntime>;
935}
936
937pub struct PackRegistration(pub &'static dyn PackFactory);
942
943inventory::collect!(PackRegistration);
944
945pub struct PackRegistry;
950
951impl PackRegistry {
952 pub fn discovered_names() -> Vec<&'static str> {
954 inventory::iter::<PackRegistration>
955 .into_iter()
956 .map(|r| r.0.name())
957 .collect()
958 }
959
960 pub fn register_packs(
974 names: &[String],
975 runtime: KhiveRuntime,
976 builder: &mut VerbRegistryBuilder,
977 ) -> Result<(), String> {
978 let all: Vec<&'static dyn PackFactory> = inventory::iter::<PackRegistration>
980 .into_iter()
981 .map(|r| r.0)
982 .collect();
983 let factory_for = |name: &str| -> Option<&'static dyn PackFactory> {
984 all.iter().copied().find(|f| f.name() == name)
985 };
986
987 let requested: std::collections::HashSet<&str> = names.iter().map(String::as_str).collect();
989 for name in names {
990 factory_for(name.as_str()).ok_or_else(|| name.clone())?;
991 }
992
993 for name in names {
996 let factory = factory_for(name.as_str()).unwrap(); for &dep in factory.requires() {
998 if !requested.contains(dep) {
999 return Err(dep.to_string());
1000 }
1001 }
1002 }
1003
1004 for name in names {
1007 let factory = factory_for(name.as_str()).unwrap(); builder.register_boxed(factory.create(runtime.clone()));
1009 }
1010
1011 Ok(())
1012 }
1013}
1014
1015#[cfg(test)]
1016mod tests {
1017 use super::*;
1018 use khive_types::Pack;
1019
1020 struct AlphaPack;
1021
1022 impl Pack for AlphaPack {
1023 const NAME: &'static str = "alpha";
1024 const NOTE_KINDS: &'static [&'static str] = &["memo", "log"];
1025 const ENTITY_KINDS: &'static [&'static str] = &["widget"];
1026 const HANDLERS: &'static [HandlerDef] = &[
1027 HandlerDef {
1028 name: "create",
1029 description: "create a widget",
1030 visibility: Visibility::Verb,
1031 category: VerbCategory::Commissive,
1032 },
1033 HandlerDef {
1034 name: "list",
1035 description: "list widgets",
1036 visibility: Visibility::Verb,
1037 category: VerbCategory::Assertive,
1038 },
1039 ];
1040 }
1041
1042 #[async_trait]
1043 impl PackRuntime for AlphaPack {
1044 fn name(&self) -> &str {
1045 AlphaPack::NAME
1046 }
1047 fn note_kinds(&self) -> &'static [&'static str] {
1048 AlphaPack::NOTE_KINDS
1049 }
1050 fn entity_kinds(&self) -> &'static [&'static str] {
1051 AlphaPack::ENTITY_KINDS
1052 }
1053 fn handlers(&self) -> &'static [HandlerDef] {
1054 AlphaPack::HANDLERS
1055 }
1056 async fn dispatch(
1057 &self,
1058 verb: &str,
1059 _params: Value,
1060 _registry: &VerbRegistry,
1061 _token: &NamespaceToken,
1062 ) -> Result<Value, RuntimeError> {
1063 Ok(serde_json::json!({ "pack": "alpha", "verb": verb }))
1064 }
1065 }
1066
1067 struct BetaPack;
1068
1069 impl Pack for BetaPack {
1070 const NAME: &'static str = "beta";
1071 const NOTE_KINDS: &'static [&'static str] = &["alert"];
1072 const ENTITY_KINDS: &'static [&'static str] = &["widget", "gadget"];
1073 const HANDLERS: &'static [HandlerDef] = &[
1074 HandlerDef {
1075 name: "notify",
1076 description: "send alert",
1077 visibility: Visibility::Verb,
1078 category: VerbCategory::Commissive,
1079 },
1080 HandlerDef {
1084 name: "create",
1085 description: "beta internal create (subhandler)",
1086 visibility: Visibility::Subhandler,
1087 category: VerbCategory::Commissive,
1088 },
1089 ];
1090 }
1091
1092 fn build_registry() -> VerbRegistry {
1098 let mut builder = VerbRegistryBuilder::new();
1099 builder.register(AlphaPack);
1100 builder.register(BetaPack);
1101 builder.build().expect("registry builds without collision")
1102 }
1103
1104 struct CollidingPack;
1107
1108 impl Pack for CollidingPack {
1109 const NAME: &'static str = "colliding";
1110 const NOTE_KINDS: &'static [&'static str] = &[];
1111 const ENTITY_KINDS: &'static [&'static str] = &[];
1112 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
1113 name: "create",
1114 description: "duplicate Verb-visibility create",
1115 visibility: Visibility::Verb,
1116 category: VerbCategory::Commissive,
1117 }];
1118 }
1119
1120 #[async_trait]
1121 impl PackRuntime for CollidingPack {
1122 fn name(&self) -> &str {
1123 Self::NAME
1124 }
1125 fn note_kinds(&self) -> &'static [&'static str] {
1126 Self::NOTE_KINDS
1127 }
1128 fn entity_kinds(&self) -> &'static [&'static str] {
1129 Self::ENTITY_KINDS
1130 }
1131 fn handlers(&self) -> &'static [HandlerDef] {
1132 Self::HANDLERS
1133 }
1134 async fn dispatch(
1135 &self,
1136 verb: &str,
1137 _params: Value,
1138 _registry: &VerbRegistry,
1139 _token: &NamespaceToken,
1140 ) -> Result<Value, RuntimeError> {
1141 Ok(serde_json::json!({ "pack": "colliding", "verb": verb }))
1142 }
1143 }
1144
1145 #[async_trait]
1146 impl PackRuntime for BetaPack {
1147 fn name(&self) -> &str {
1148 BetaPack::NAME
1149 }
1150 fn note_kinds(&self) -> &'static [&'static str] {
1151 BetaPack::NOTE_KINDS
1152 }
1153 fn entity_kinds(&self) -> &'static [&'static str] {
1154 BetaPack::ENTITY_KINDS
1155 }
1156 fn handlers(&self) -> &'static [HandlerDef] {
1157 BetaPack::HANDLERS
1158 }
1159 async fn dispatch(
1160 &self,
1161 verb: &str,
1162 _params: Value,
1163 _registry: &VerbRegistry,
1164 _token: &NamespaceToken,
1165 ) -> Result<Value, RuntimeError> {
1166 Ok(serde_json::json!({ "pack": "beta", "verb": verb }))
1167 }
1168 }
1169
1170 #[tokio::test]
1171 async fn dispatch_routes_to_correct_pack() {
1172 let reg = build_registry();
1173
1174 let res = reg.dispatch("list", Value::Null).await.unwrap();
1175 assert_eq!(res["pack"], "alpha");
1176
1177 let res = reg.dispatch("notify", Value::Null).await.unwrap();
1178 assert_eq!(res["pack"], "beta");
1179 }
1180
1181 #[test]
1185 fn verb_collision_is_boot_time_error() {
1186 let mut builder = VerbRegistryBuilder::new();
1187 builder.register(AlphaPack);
1188 builder.register(CollidingPack);
1189 let err = builder
1190 .build()
1191 .err()
1192 .expect("duplicate Verb-visibility handler must be rejected at build time");
1193 assert!(
1194 matches!(err, RuntimeError::VerbCollision { ref verb, .. } if verb == "create"),
1195 "expected VerbCollision for 'create', got {err:?}"
1196 );
1197 let msg = err.to_string();
1198 assert!(
1199 msg.contains("create"),
1200 "error must name the colliding verb: {msg}"
1201 );
1202 assert!(
1203 msg.contains("alpha") || msg.contains("colliding"),
1204 "error must name one of the conflicting packs: {msg}"
1205 );
1206 }
1207
1208 #[test]
1212 fn subhandler_same_name_across_packs_is_not_a_collision() {
1213 struct SubhandlerPack;
1214 impl Pack for SubhandlerPack {
1215 const NAME: &'static str = "subhandler_pack";
1216 const NOTE_KINDS: &'static [&'static str] = &[];
1217 const ENTITY_KINDS: &'static [&'static str] = &[];
1218 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
1219 name: "create",
1220 description: "internal create",
1221 visibility: Visibility::Subhandler,
1222 category: VerbCategory::Commissive,
1223 }];
1224 }
1225 #[async_trait]
1226 impl PackRuntime for SubhandlerPack {
1227 fn name(&self) -> &str {
1228 Self::NAME
1229 }
1230 fn note_kinds(&self) -> &'static [&'static str] {
1231 Self::NOTE_KINDS
1232 }
1233 fn entity_kinds(&self) -> &'static [&'static str] {
1234 Self::ENTITY_KINDS
1235 }
1236 fn handlers(&self) -> &'static [HandlerDef] {
1237 Self::HANDLERS
1238 }
1239 async fn dispatch(
1240 &self,
1241 verb: &str,
1242 _: Value,
1243 _: &VerbRegistry,
1244 _: &NamespaceToken,
1245 ) -> Result<Value, RuntimeError> {
1246 Ok(serde_json::json!({"pack": "subhandler_pack", "verb": verb}))
1247 }
1248 }
1249 let mut builder = VerbRegistryBuilder::new();
1250 builder.register(AlphaPack); builder.register(SubhandlerPack); builder
1253 .build()
1254 .expect("subhandler same name must NOT be a collision");
1255 }
1256
1257 #[tokio::test]
1258 async fn dispatch_unknown_verb_returns_error() {
1259 let reg = build_registry();
1260
1261 let err = reg.dispatch("explode", Value::Null).await.unwrap_err();
1262 let msg = err.to_string();
1263 assert!(msg.contains("explode"));
1264 assert!(msg.contains("create"));
1265 }
1266
1267 #[test]
1272 fn all_verbs_aggregates_across_packs_excludes_subhandlers() {
1273 let reg = build_registry();
1274 let verbs: Vec<&str> = reg.all_verbs().iter().map(|v| v.name).collect();
1275 assert_eq!(verbs, vec!["create", "list", "notify"]);
1277 }
1278
1279 #[test]
1280 fn all_verbs_with_names_pairs_pack_name_excludes_subhandlers() {
1281 let reg = build_registry();
1282 let pairs: Vec<(&str, &str)> = reg
1283 .all_verbs_with_names()
1284 .iter()
1285 .map(|(pack, v)| (*pack, v.name))
1286 .collect();
1287 assert_eq!(
1289 pairs,
1290 vec![("alpha", "create"), ("alpha", "list"), ("beta", "notify"),]
1291 );
1292 }
1293
1294 #[test]
1295 fn all_handlers_with_names_includes_subhandlers() {
1296 let reg = build_registry();
1297 let pairs: Vec<(&str, &str)> = reg
1298 .all_handlers_with_names()
1299 .iter()
1300 .map(|(pack, v)| (*pack, v.name))
1301 .collect();
1302 assert_eq!(
1304 pairs,
1305 vec![
1306 ("alpha", "create"),
1307 ("alpha", "list"),
1308 ("beta", "notify"),
1309 ("beta", "create"),
1310 ]
1311 );
1312 }
1313
1314 #[test]
1315 fn note_kinds_are_ordered() {
1316 let reg = build_registry();
1317 let kinds = reg.all_note_kinds();
1318 assert_eq!(kinds, vec!["memo", "log", "alert"]);
1319 }
1320
1321 #[test]
1322 fn note_kind_duplicate_rejected_at_build_time() {
1323 struct DupPack;
1324
1325 impl khive_types::Pack for DupPack {
1326 const NAME: &'static str = "dup";
1327 const NOTE_KINDS: &'static [&'static str] = &["memo"];
1329 const ENTITY_KINDS: &'static [&'static str] = &[];
1330 const HANDLERS: &'static [HandlerDef] = &[];
1331 }
1332
1333 #[async_trait]
1334 impl PackRuntime for DupPack {
1335 fn name(&self) -> &str {
1336 Self::NAME
1337 }
1338 fn note_kinds(&self) -> &'static [&'static str] {
1339 Self::NOTE_KINDS
1340 }
1341 fn entity_kinds(&self) -> &'static [&'static str] {
1342 Self::ENTITY_KINDS
1343 }
1344 fn handlers(&self) -> &'static [HandlerDef] {
1345 Self::HANDLERS
1346 }
1347 async fn dispatch(
1348 &self,
1349 _verb: &str,
1350 _params: Value,
1351 _registry: &VerbRegistry,
1352 _token: &NamespaceToken,
1353 ) -> Result<Value, RuntimeError> {
1354 Ok(Value::Null)
1355 }
1356 }
1357
1358 let mut builder = VerbRegistryBuilder::new();
1359 builder.register(AlphaPack);
1360 builder.register(DupPack);
1361 let err = builder
1362 .build()
1363 .err()
1364 .expect("duplicate note kind must be rejected");
1365 let msg = err.to_string();
1366 assert!(
1367 msg.contains("memo"),
1368 "error must name the duplicate kind: {msg}"
1369 );
1370 assert!(
1371 msg.contains("alpha") || msg.contains("dup"),
1372 "error must name one of the conflicting packs: {msg}"
1373 );
1374 }
1375
1376 #[test]
1377 fn entity_kinds_are_deduplicated() {
1378 let reg = build_registry();
1379 let kinds = reg.all_entity_kinds();
1380 assert_eq!(kinds, vec!["widget", "gadget"]);
1381 }
1382
1383 use khive_gate::{Gate, GateError};
1386 use std::sync::atomic::{AtomicUsize, Ordering};
1387 use std::sync::Arc;
1388
1389 #[derive(Default, Debug)]
1390 struct CountingGate {
1391 calls: AtomicUsize,
1392 deny_verb: Option<&'static str>,
1393 }
1394
1395 impl Gate for CountingGate {
1396 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
1397 self.calls.fetch_add(1, Ordering::SeqCst);
1398 if Some(req.verb.as_str()) == self.deny_verb {
1399 Ok(GateDecision::deny(format!("test deny for {}", req.verb)))
1400 } else {
1401 Ok(GateDecision::allow())
1402 }
1403 }
1404 }
1405
1406 #[tokio::test]
1407 async fn dispatch_consults_the_gate() {
1408 let gate = Arc::new(CountingGate::default());
1409 let mut builder = VerbRegistryBuilder::new();
1410 builder.register(AlphaPack);
1411 builder.with_gate(gate.clone());
1412 let reg = builder.build().expect("registry builds");
1413
1414 reg.dispatch("list", Value::Null).await.unwrap();
1415 reg.dispatch("create", Value::Null).await.unwrap();
1416 assert_eq!(
1417 gate.calls.load(Ordering::SeqCst),
1418 2,
1419 "gate should be consulted once per dispatch"
1420 );
1421 }
1422
1423 #[tokio::test]
1424 async fn dispatch_returns_permission_denied_on_deny_v03() {
1425 let gate = Arc::new(CountingGate {
1426 calls: AtomicUsize::new(0),
1427 deny_verb: Some("create"),
1428 });
1429 let mut builder = VerbRegistryBuilder::new();
1430 builder.register(AlphaPack);
1431 builder.with_gate(gate.clone());
1432 let reg = builder.build().expect("registry builds");
1433
1434 let err = reg.dispatch("create", Value::Null).await.unwrap_err();
1436 assert!(
1437 matches!(err, RuntimeError::PermissionDenied { ref verb, .. } if verb == "create"),
1438 "expected PermissionDenied, got {err:?}"
1439 );
1440 let msg = err.to_string();
1441 assert!(
1442 msg.contains("create"),
1443 "error message must name the verb: {msg}"
1444 );
1445 assert!(
1446 msg.contains("test deny for create"),
1447 "error message must carry the deny reason: {msg}"
1448 );
1449 assert_eq!(gate.calls.load(Ordering::SeqCst), 1);
1450 }
1451
1452 #[tokio::test]
1453 async fn dispatch_allow_verb_succeeds_even_with_deny_gate_for_other_verb() {
1454 let gate = Arc::new(CountingGate {
1456 calls: AtomicUsize::new(0),
1457 deny_verb: Some("create"),
1458 });
1459 let mut builder = VerbRegistryBuilder::new();
1460 builder.register(AlphaPack);
1461 builder.with_gate(gate.clone());
1462 let reg = builder.build().expect("registry builds");
1463
1464 let res = reg.dispatch("list", Value::Null).await.unwrap();
1465 assert_eq!(res["pack"], "alpha");
1466 }
1467
1468 #[tokio::test]
1469 async fn dispatch_uses_allow_all_gate_by_default() {
1470 let reg = build_registry();
1472 let res = reg.dispatch("list", Value::Null).await.unwrap();
1473 assert_eq!(res["pack"], "alpha");
1474 }
1475
1476 #[derive(Default, Debug)]
1479 struct NamespaceCapturingGate {
1480 seen: std::sync::Mutex<Vec<String>>,
1481 }
1482
1483 impl Gate for NamespaceCapturingGate {
1484 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
1485 self.seen
1486 .lock()
1487 .unwrap()
1488 .push(req.namespace.as_str().to_string());
1489 Ok(GateDecision::allow())
1490 }
1491 }
1492
1493 #[tokio::test]
1494 async fn dispatch_propagates_params_namespace_to_gate() {
1495 let gate = Arc::new(NamespaceCapturingGate::default());
1496 let mut builder = VerbRegistryBuilder::new();
1497 builder.register(AlphaPack);
1498 builder.with_gate(gate.clone());
1499 builder.with_default_namespace("tenant-x");
1500 let reg = builder.build().expect("registry builds");
1501
1502 reg.dispatch("list", serde_json::json!({"namespace": "tenant-y"}))
1504 .await
1505 .unwrap();
1506 reg.dispatch("list", Value::Null).await.unwrap();
1508 let err = reg
1510 .dispatch("list", serde_json::json!({"namespace": ""}))
1511 .await
1512 .unwrap_err();
1513 assert!(
1514 matches!(err, RuntimeError::InvalidInput(_)),
1515 "empty namespace must return InvalidInput, got {err:?}"
1516 );
1517
1518 let seen = gate.seen.lock().unwrap().clone();
1519 assert_eq!(seen, vec!["tenant-y", "tenant-x"]);
1520 }
1521
1522 #[tokio::test]
1523 async fn dispatch_falls_back_to_local_when_no_default_set() {
1524 let gate = Arc::new(NamespaceCapturingGate::default());
1526 let mut builder = VerbRegistryBuilder::new();
1527 builder.register(AlphaPack);
1528 builder.with_gate(gate.clone());
1529 let reg = builder.build().expect("registry builds");
1530
1531 reg.dispatch("list", Value::Null).await.unwrap();
1532 let seen = gate.seen.lock().unwrap().clone();
1533 assert_eq!(seen, vec!["local"]);
1534 }
1535
1536 use khive_gate::{AuditDecision, AuditEvent, Obligation};
1539
1540 #[derive(Default, Debug)]
1542 struct AuditCapturingGate {
1543 events: std::sync::Mutex<Vec<AuditEvent>>,
1544 deny_verb: Option<&'static str>,
1545 }
1546
1547 impl Gate for AuditCapturingGate {
1548 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
1549 let decision = if Some(req.verb.as_str()) == self.deny_verb {
1550 GateDecision::deny("test deny")
1551 } else {
1552 GateDecision::allow_with(vec![Obligation::Audit {
1553 tag: format!("{}.check", req.verb),
1554 }])
1555 };
1556 let ev = AuditEvent::from_check(req, &decision, self.impl_name());
1558 self.events.lock().unwrap().push(ev);
1559 Ok(decision)
1560 }
1561
1562 fn impl_name(&self) -> &'static str {
1563 "AuditCapturingGate"
1564 }
1565 }
1566
1567 #[tokio::test]
1568 async fn dispatch_emits_one_audit_event_per_call() {
1569 let gate = Arc::new(AuditCapturingGate::default());
1570 let mut builder = VerbRegistryBuilder::new();
1571 builder.register(AlphaPack);
1572 builder.with_gate(gate.clone());
1573 let reg = builder.build().expect("registry builds");
1574
1575 reg.dispatch("list", Value::Null).await.unwrap();
1576 reg.dispatch("create", Value::Null).await.unwrap();
1577
1578 let evs = gate.events.lock().unwrap();
1579 assert_eq!(evs.len(), 2, "exactly one audit event per dispatch call");
1580 }
1581
1582 #[tokio::test]
1583 async fn dispatch_audit_event_allow_carries_obligations() {
1584 let gate = Arc::new(AuditCapturingGate::default());
1585 let mut builder = VerbRegistryBuilder::new();
1586 builder.register(AlphaPack);
1587 builder.with_gate(gate.clone());
1588 let reg = builder.build().expect("registry builds");
1589
1590 reg.dispatch("list", Value::Null).await.unwrap();
1591
1592 let evs = gate.events.lock().unwrap();
1593 let ev = &evs[0];
1594 assert_eq!(ev.verb, "list");
1595 assert_eq!(ev.decision, AuditDecision::Allow);
1596 assert!(ev.deny_reason.is_none());
1597 assert_eq!(ev.obligations.len(), 1);
1598 assert_eq!(ev.gate_impl, "AuditCapturingGate");
1599 }
1600
1601 #[tokio::test]
1602 async fn dispatch_audit_event_deny_carries_reason() {
1603 let gate = Arc::new(AuditCapturingGate {
1604 events: Default::default(),
1605 deny_verb: Some("create"),
1606 });
1607 let mut builder = VerbRegistryBuilder::new();
1608 builder.register(AlphaPack);
1609 builder.with_gate(gate.clone());
1610 let reg = builder.build().expect("registry builds");
1611
1612 let err = reg.dispatch("create", Value::Null).await.unwrap_err();
1615 assert!(matches!(err, RuntimeError::PermissionDenied { .. }));
1616
1617 let evs = gate.events.lock().unwrap();
1618 let ev = &evs[0];
1619 assert_eq!(ev.verb, "create");
1620 assert_eq!(ev.decision, AuditDecision::Deny);
1621 assert_eq!(ev.deny_reason.as_deref(), Some("test deny"));
1622 assert!(ev.obligations.is_empty());
1623 }
1624
1625 #[tokio::test]
1626 async fn dispatch_audit_event_fields_match_gate_request() {
1627 let gate = Arc::new(AuditCapturingGate::default());
1628 let mut builder = VerbRegistryBuilder::new();
1629 builder.register(AlphaPack);
1630 builder.with_gate(gate.clone());
1631 builder.with_default_namespace("tenant-z");
1632 let reg = builder.build().expect("registry builds");
1633
1634 reg.dispatch("list", serde_json::json!({"namespace": "tenant-q"}))
1635 .await
1636 .unwrap();
1637
1638 let evs = gate.events.lock().unwrap();
1639 let ev = &evs[0];
1640 assert_eq!(ev.namespace, "tenant-q");
1642 assert_eq!(ev.verb, "list");
1643 assert_eq!(ev.actor.kind, "anonymous");
1644 }
1645
1646 use std::sync::{Mutex as StdMutex, Once, OnceLock};
1659
1660 use serial_test::serial;
1661 use tracing::field::{Field, Visit};
1662
1663 #[derive(Clone, Debug, Default)]
1664 struct CapturedEvent {
1665 message: Option<String>,
1666 audit_event: Option<String>,
1667 }
1668
1669 #[derive(Default)]
1670 struct CapturedEventVisitor(CapturedEvent);
1671
1672 impl Visit for CapturedEventVisitor {
1673 fn record_str(&mut self, field: &Field, value: &str) {
1674 match field.name() {
1675 "message" => self.0.message = Some(value.to_string()),
1676 "audit_event" => self.0.audit_event = Some(value.to_string()),
1677 _ => {}
1678 }
1679 }
1680
1681 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
1682 let formatted = format!("{value:?}");
1688 let cleaned = formatted
1689 .trim_start_matches('"')
1690 .trim_end_matches('"')
1691 .to_string();
1692 match field.name() {
1693 "message" => self.0.message = Some(cleaned),
1694 "audit_event" => self.0.audit_event = Some(cleaned),
1695 _ => {}
1696 }
1697 }
1698 }
1699
1700 struct CaptureSubscriber {
1713 events: Arc<StdMutex<Vec<CapturedEvent>>>,
1714 }
1715
1716 impl CaptureSubscriber {
1717 fn new(events: Arc<StdMutex<Vec<CapturedEvent>>>) -> Self {
1718 Self { events }
1719 }
1720 }
1721
1722 impl tracing::Subscriber for CaptureSubscriber {
1723 fn enabled(&self, _: &tracing::Metadata<'_>) -> bool {
1724 true
1725 }
1726 fn new_span(&self, _: &tracing::span::Attributes<'_>) -> tracing::span::Id {
1727 tracing::span::Id::from_u64(1)
1728 }
1729 fn record(&self, _: &tracing::span::Id, _: &tracing::span::Record<'_>) {}
1730 fn record_follows_from(&self, _: &tracing::span::Id, _: &tracing::span::Id) {}
1731 fn event(&self, event: &tracing::Event<'_>) {
1732 let mut visitor = CapturedEventVisitor::default();
1733 event.record(&mut visitor);
1734 self.events.lock().unwrap().push(visitor.0);
1735 }
1736 fn enter(&self, _: &tracing::span::Id) {}
1737 fn exit(&self, _: &tracing::span::Id) {}
1738 }
1739
1740 static GLOBAL_CAPTURE: OnceLock<Arc<StdMutex<Vec<CapturedEvent>>>> = OnceLock::new();
1750 static GLOBAL_INIT: Once = Once::new();
1751
1752 fn global_capture() -> Arc<StdMutex<Vec<CapturedEvent>>> {
1753 GLOBAL_INIT.call_once(|| {
1754 let buffer = Arc::new(StdMutex::new(Vec::new()));
1755 let subscriber = CaptureSubscriber::new(Arc::clone(&buffer));
1756 let _ = tracing::subscriber::set_global_default(subscriber);
1761 let _ = GLOBAL_CAPTURE.set(buffer);
1762 });
1763 Arc::clone(GLOBAL_CAPTURE.get().expect("global capture initialized"))
1764 }
1765
1766 fn capture_dispatch_events<Fut>(future: Fut) -> Vec<CapturedEvent>
1771 where
1772 Fut: std::future::Future<Output = ()>,
1773 {
1774 let buffer = global_capture();
1775 buffer.lock().unwrap().clear();
1776
1777 let rt = tokio::runtime::Builder::new_current_thread()
1778 .enable_all()
1779 .build()
1780 .expect("build current-thread tokio runtime");
1781 rt.block_on(future);
1782
1783 let result = buffer.lock().unwrap().clone();
1784 result
1785 }
1786
1787 fn gate_check_events_for(events: &[CapturedEvent], gate_impl: &str) -> Vec<CapturedEvent> {
1794 events
1795 .iter()
1796 .filter(|e| e.message.as_deref() == Some("gate.check"))
1797 .filter(|e| {
1798 e.audit_event
1799 .as_deref()
1800 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
1801 .and_then(|v| {
1802 v.get("gate_impl")
1803 .and_then(|g| g.as_str().map(|s| s.to_string()))
1804 })
1805 .as_deref()
1806 == Some(gate_impl)
1807 })
1808 .cloned()
1809 .collect()
1810 }
1811
1812 #[test]
1813 #[serial]
1814 fn dispatch_tracing_emits_one_gate_check_event_on_allow() {
1815 #[derive(Debug)]
1816 struct TracingAllowGate;
1817 impl Gate for TracingAllowGate {
1818 fn check(&self, _: &GateRequest) -> Result<GateDecision, GateError> {
1819 Ok(GateDecision::allow())
1820 }
1821 fn impl_name(&self) -> &'static str {
1822 "TracingAllowGate"
1823 }
1824 }
1825
1826 let events = capture_dispatch_events(async {
1827 let mut builder = VerbRegistryBuilder::new();
1828 builder.register(AlphaPack);
1829 builder.with_gate(Arc::new(TracingAllowGate));
1830 builder.with_default_namespace("tenant-default");
1831 let reg = builder.build().expect("registry builds");
1832 reg.dispatch("list", serde_json::json!({"namespace": "tenant-q"}))
1833 .await
1834 .unwrap();
1835 });
1836
1837 let gate_events = gate_check_events_for(&events, "TracingAllowGate");
1838 assert_eq!(
1839 gate_events.len(),
1840 1,
1841 "exactly one gate.check tracing event per dispatch (allow); got {gate_events:?}"
1842 );
1843 let payload = gate_events[0]
1844 .audit_event
1845 .as_ref()
1846 .expect("gate.check event must carry an audit_event field");
1847 let audit: khive_gate::AuditEvent =
1848 serde_json::from_str(payload).expect("audit_event payload must decode to AuditEvent");
1849 assert_eq!(audit.decision, AuditDecision::Allow);
1850 assert_eq!(audit.verb, "list");
1851 assert_eq!(audit.namespace, "tenant-q");
1852 assert_eq!(audit.gate_impl, "TracingAllowGate");
1853 assert!(
1854 audit.deny_reason.is_none(),
1855 "deny_reason must be None on Allow"
1856 );
1857 }
1858
1859 use crate::runtime::NamespaceToken;
1862 use async_trait::async_trait;
1863 use khive_storage::{
1864 BatchWriteSummary, Event, EventFilter, EventStore, Page, PageRequest, SubstrateKind,
1865 };
1866 use khive_types::EventOutcome;
1867
1868 #[derive(Default, Debug)]
1870 struct MemoryEventStore {
1871 events: std::sync::Mutex<Vec<Event>>,
1872 }
1873
1874 #[async_trait]
1875 impl EventStore for MemoryEventStore {
1876 async fn append_event(&self, event: Event) -> khive_storage::StorageResult<()> {
1877 self.events.lock().unwrap().push(event);
1878 Ok(())
1879 }
1880 async fn append_events(
1881 &self,
1882 events: Vec<Event>,
1883 ) -> khive_storage::StorageResult<BatchWriteSummary> {
1884 let attempted = events.len() as u64;
1885 let affected = attempted;
1886 self.events.lock().unwrap().extend(events);
1887 Ok(BatchWriteSummary {
1888 attempted,
1889 affected,
1890 failed: 0,
1891 first_error: String::new(),
1892 })
1893 }
1894 async fn get_event(&self, id: uuid::Uuid) -> khive_storage::StorageResult<Option<Event>> {
1895 Ok(self
1896 .events
1897 .lock()
1898 .unwrap()
1899 .iter()
1900 .find(|e| e.id == id)
1901 .cloned())
1902 }
1903 async fn query_events(
1904 &self,
1905 _filter: EventFilter,
1906 _page: PageRequest,
1907 ) -> khive_storage::StorageResult<Page<Event>> {
1908 let items = self.events.lock().unwrap().clone();
1909 let total = items.len() as u64;
1910 Ok(Page {
1911 items,
1912 total: Some(total),
1913 })
1914 }
1915 async fn count_events(&self, _filter: EventFilter) -> khive_storage::StorageResult<u64> {
1916 Ok(self.events.lock().unwrap().len() as u64)
1917 }
1918 }
1919
1920 #[tokio::test]
1921 async fn allow_all_gate_default_remains_backward_compatible() {
1922 let mut builder = VerbRegistryBuilder::new();
1924 builder.register(AlphaPack);
1925 let reg = builder.build().expect("registry builds");
1926
1927 let res = reg.dispatch("list", Value::Null).await.unwrap();
1928 assert_eq!(
1929 res["pack"], "alpha",
1930 "AllowAllGate must allow every verb — backward compat guarantee"
1931 );
1932 let res = reg.dispatch("create", Value::Null).await.unwrap();
1933 assert_eq!(res["pack"], "alpha");
1934 }
1935
1936 #[tokio::test]
1937 async fn deny_gate_returns_permission_denied_pack_never_invoked() {
1938 #[derive(Debug)]
1939 struct AlwaysDenyGate;
1940 impl Gate for AlwaysDenyGate {
1941 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
1942 Ok(GateDecision::deny("test: always deny"))
1943 }
1944 }
1945
1946 #[derive(Debug)]
1948 struct TrackedPack {
1949 invoked: Arc<AtomicUsize>,
1950 }
1951
1952 impl khive_types::Pack for TrackedPack {
1953 const NAME: &'static str = "tracked";
1954 const NOTE_KINDS: &'static [&'static str] = &[];
1955 const ENTITY_KINDS: &'static [&'static str] = &[];
1956 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
1957 name: "guarded",
1958 description: "a guarded verb",
1959 visibility: Visibility::Verb,
1960 category: VerbCategory::Assertive,
1961 }];
1962 }
1963
1964 #[async_trait]
1965 impl PackRuntime for TrackedPack {
1966 fn name(&self) -> &str {
1967 Self::NAME
1968 }
1969 fn note_kinds(&self) -> &'static [&'static str] {
1970 Self::NOTE_KINDS
1971 }
1972 fn entity_kinds(&self) -> &'static [&'static str] {
1973 Self::ENTITY_KINDS
1974 }
1975 fn handlers(&self) -> &'static [HandlerDef] {
1976 Self::HANDLERS
1977 }
1978 async fn dispatch(
1979 &self,
1980 _verb: &str,
1981 _params: Value,
1982 _registry: &VerbRegistry,
1983 _token: &NamespaceToken,
1984 ) -> Result<Value, RuntimeError> {
1985 self.invoked.fetch_add(1, Ordering::SeqCst);
1986 Ok(serde_json::json!({"invoked": true}))
1987 }
1988 }
1989
1990 let invoked = Arc::new(AtomicUsize::new(0));
1991 let mut builder = VerbRegistryBuilder::new();
1992 builder.register(TrackedPack {
1993 invoked: invoked.clone(),
1994 });
1995 builder.with_gate(Arc::new(AlwaysDenyGate));
1996 let reg = builder.build().expect("registry builds");
1997
1998 let err = reg.dispatch("guarded", Value::Null).await.unwrap_err();
1999 assert!(
2000 matches!(err, RuntimeError::PermissionDenied { ref verb, ref reason } if verb == "guarded" && reason.contains("always deny")),
2001 "expected PermissionDenied with verb=guarded and reason, got: {err:?}"
2002 );
2003 assert_eq!(
2004 invoked.load(Ordering::SeqCst),
2005 0,
2006 "pack dispatch MUST NOT be invoked when gate denies"
2007 );
2008 }
2009
2010 #[tokio::test]
2011 async fn audit_event_persists_to_event_store_on_allow() {
2012 let store = Arc::new(MemoryEventStore::default());
2013 let mut builder = VerbRegistryBuilder::new();
2014 builder.register(AlphaPack);
2015 builder.with_event_store(store.clone());
2016 let reg = builder.build().expect("registry builds");
2017
2018 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2019 .await
2020 .unwrap();
2021
2022 let count = store.count_events(EventFilter::default()).await.unwrap();
2023 assert_eq!(count, 1, "one audit event persisted to EventStore on allow");
2024
2025 let page = store
2026 .query_events(
2027 EventFilter::default(),
2028 PageRequest {
2029 limit: 10,
2030 offset: 0,
2031 },
2032 )
2033 .await
2034 .unwrap();
2035 let ev = &page.items[0];
2036 assert_eq!(ev.verb, "list");
2037 assert_eq!(ev.namespace, "test-ns");
2038 assert_eq!(ev.substrate, SubstrateKind::Event);
2039 assert_eq!(ev.outcome, EventOutcome::Success);
2040 }
2041
2042 #[tokio::test]
2043 async fn audit_event_persists_to_event_store_on_deny() {
2044 #[derive(Debug)]
2045 struct AlwaysDenyGate;
2046 impl Gate for AlwaysDenyGate {
2047 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2048 Ok(GateDecision::deny("denied by test"))
2049 }
2050 }
2051
2052 let store = Arc::new(MemoryEventStore::default());
2053 let mut builder = VerbRegistryBuilder::new();
2054 builder.register(AlphaPack);
2055 builder.with_gate(Arc::new(AlwaysDenyGate));
2056 builder.with_event_store(store.clone());
2057 let reg = builder.build().expect("registry builds");
2058
2059 let err = reg
2061 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2062 .await
2063 .unwrap_err();
2064 assert!(matches!(err, RuntimeError::PermissionDenied { .. }));
2065
2066 let count = store.count_events(EventFilter::default()).await.unwrap();
2067 assert_eq!(count, 1, "one audit event persisted to EventStore on deny");
2068
2069 let page = store
2070 .query_events(
2071 EventFilter::default(),
2072 PageRequest {
2073 limit: 10,
2074 offset: 0,
2075 },
2076 )
2077 .await
2078 .unwrap();
2079 let ev = &page.items[0];
2080 assert_eq!(ev.verb, "list");
2081 assert_eq!(ev.outcome, EventOutcome::Denied);
2082 }
2083
2084 #[tokio::test]
2085 async fn gate_error_does_not_persist_to_event_store() {
2086 #[derive(Debug)]
2087 struct FailingGate;
2088 impl Gate for FailingGate {
2089 fn check(&self, _req: &GateRequest) -> Result<GateDecision, khive_gate::GateError> {
2090 Err(khive_gate::GateError::Internal("gate broken".into()))
2091 }
2092 }
2093
2094 let store = Arc::new(MemoryEventStore::default());
2095 let mut builder = VerbRegistryBuilder::new();
2096 builder.register(AlphaPack);
2097 builder.with_gate(Arc::new(FailingGate));
2098 builder.with_event_store(store.clone());
2099 let reg = builder.build().expect("registry builds");
2100
2101 let res = reg.dispatch("list", Value::Null).await.unwrap();
2103 assert_eq!(
2104 res["pack"], "alpha",
2105 "gate error must fail-open, not block dispatch"
2106 );
2107
2108 let count = store.count_events(EventFilter::default()).await.unwrap();
2109 assert_eq!(
2110 count, 0,
2111 "gate infrastructure error must NOT produce an audit event in EventStore"
2112 );
2113 }
2114
2115 #[tokio::test]
2116 async fn no_event_store_configured_tracing_only() {
2117 let mut builder = VerbRegistryBuilder::new();
2121 builder.register(AlphaPack);
2122 let reg = builder.build().expect("registry builds");
2123
2124 let res = reg.dispatch("list", Value::Null).await.unwrap();
2125 assert_eq!(res["pack"], "alpha");
2126 }
2127
2128 #[test]
2129 #[serial]
2130 fn dispatch_tracing_emits_gate_check_event_with_deny_payload() {
2131 #[derive(Debug)]
2132 struct TracingDenyGate;
2133 impl Gate for TracingDenyGate {
2134 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2135 Ok(GateDecision::deny("denied by test gate"))
2136 }
2137 fn impl_name(&self) -> &'static str {
2138 "TracingDenyGate"
2139 }
2140 }
2141
2142 let events = capture_dispatch_events(async {
2143 let mut builder = VerbRegistryBuilder::new();
2144 builder.register(AlphaPack);
2145 builder.with_gate(Arc::new(TracingDenyGate));
2146 let reg = builder.build().expect("registry builds");
2147 let _ = reg.dispatch("create", serde_json::Value::Null).await;
2150 });
2151
2152 let gate_events = gate_check_events_for(&events, "TracingDenyGate");
2153 assert_eq!(
2154 gate_events.len(),
2155 1,
2156 "exactly one gate.check tracing event per dispatch (deny); got {gate_events:?}"
2157 );
2158 let payload = gate_events[0]
2159 .audit_event
2160 .as_ref()
2161 .expect("gate.check event must carry an audit_event field on Deny");
2162 let audit: khive_gate::AuditEvent =
2163 serde_json::from_str(payload).expect("audit_event payload must decode to AuditEvent");
2164 assert_eq!(audit.decision, AuditDecision::Deny);
2165 assert_eq!(audit.deny_reason.as_deref(), Some("denied by test gate"));
2166 assert_eq!(audit.gate_impl, "TracingDenyGate");
2167 let payload_json: serde_json::Value =
2171 serde_json::from_str(payload).expect("payload must be valid JSON");
2172 assert_eq!(
2173 payload_json["obligations"],
2174 serde_json::Value::Array(Vec::new()),
2175 "obligations must be `[]` on Deny on the tracing payload, not omitted"
2176 );
2177 }
2178
2179 #[tokio::test]
2187 async fn audit_envelope_round_trips_deny_reason_and_gate_impl_through_event_store() {
2188 #[derive(Debug)]
2189 struct DenyGateWithName;
2190 impl Gate for DenyGateWithName {
2191 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2192 Ok(GateDecision::deny("policy: write forbidden for anon"))
2193 }
2194 fn impl_name(&self) -> &'static str {
2195 "DenyGateWithName"
2196 }
2197 }
2198
2199 let store = Arc::new(MemoryEventStore::default());
2200 let mut builder = VerbRegistryBuilder::new();
2201 builder.register(AlphaPack);
2202 builder.with_gate(Arc::new(DenyGateWithName));
2203 builder.with_event_store(store.clone());
2204 let reg = builder.build().expect("registry builds");
2205
2206 let err = reg
2208 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2209 .await
2210 .unwrap_err();
2211 assert!(
2212 matches!(err, RuntimeError::PermissionDenied { .. }),
2213 "expected PermissionDenied, got {err:?}"
2214 );
2215
2216 let page = store
2218 .query_events(
2219 EventFilter::default(),
2220 PageRequest {
2221 limit: 10,
2222 offset: 0,
2223 },
2224 )
2225 .await
2226 .unwrap();
2227 assert_eq!(
2228 page.items.len(),
2229 1,
2230 "one audit event must be persisted on deny"
2231 );
2232
2233 let ev = &page.items[0];
2234 assert_eq!(ev.outcome, EventOutcome::Denied);
2235
2236 let data = &ev.payload;
2238
2239 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
2240 .expect("Event.payload must deserialize to AuditEvent");
2241
2242 assert_eq!(
2243 audit.deny_reason.as_deref(),
2244 Some("policy: write forbidden for anon"),
2245 "deny_reason must be preserved through EventStore"
2246 );
2247 assert_eq!(
2248 audit.gate_impl, "DenyGateWithName",
2249 "gate_impl must be preserved through EventStore"
2250 );
2251 assert_eq!(
2252 audit.decision,
2253 khive_gate::AuditDecision::Deny,
2254 "decision field must be preserved through EventStore"
2255 );
2256 }
2257
2258 #[tokio::test]
2259 async fn audit_envelope_round_trips_obligations_through_event_store() {
2260 use khive_gate::Obligation;
2261
2262 #[derive(Debug)]
2263 struct ObligationGate;
2264 impl Gate for ObligationGate {
2265 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2266 Ok(GateDecision::allow_with(vec![Obligation::Audit {
2267 tag: "billing.meter".into(),
2268 }]))
2269 }
2270 fn impl_name(&self) -> &'static str {
2271 "ObligationGate"
2272 }
2273 }
2274
2275 let store = Arc::new(MemoryEventStore::default());
2276 let mut builder = VerbRegistryBuilder::new();
2277 builder.register(AlphaPack);
2278 builder.with_gate(Arc::new(ObligationGate));
2279 builder.with_event_store(store.clone());
2280 let reg = builder.build().expect("registry builds");
2281
2282 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2283 .await
2284 .unwrap();
2285
2286 let page = store
2287 .query_events(
2288 EventFilter::default(),
2289 PageRequest {
2290 limit: 10,
2291 offset: 0,
2292 },
2293 )
2294 .await
2295 .unwrap();
2296 assert_eq!(page.items.len(), 1);
2297
2298 let ev = &page.items[0];
2299 assert_eq!(ev.outcome, EventOutcome::Success);
2300
2301 let data = &ev.payload;
2302
2303 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
2304 .expect("Event.payload must deserialize to AuditEvent");
2305
2306 assert_eq!(audit.gate_impl, "ObligationGate");
2307 assert_eq!(
2308 audit.obligations.len(),
2309 1,
2310 "obligations must be preserved through EventStore"
2311 );
2312 match &audit.obligations[0] {
2313 Obligation::Audit { tag } => assert_eq!(tag, "billing.meter"),
2314 other => panic!("expected Audit obligation, got {other:?}"),
2315 }
2316 }
2317
2318 #[tokio::test]
2326 async fn sql_backed_audit_envelope_round_trips_deny_reason_gate_impl_and_obligations() {
2327 #[derive(Debug)]
2328 struct SqlTestDenyGate;
2329 impl Gate for SqlTestDenyGate {
2330 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2331 Ok(GateDecision::deny("sql-path: write denied"))
2332 }
2333 fn impl_name(&self) -> &'static str {
2334 "SqlTestDenyGate"
2335 }
2336 }
2337
2338 let rt = KhiveRuntime::memory().expect("in-memory runtime");
2342 let test_tok = NamespaceToken::for_namespace(Namespace::parse("test-ns").unwrap());
2343 let sql_store = rt
2344 .events(&test_tok)
2345 .expect("events_for_namespace must succeed");
2346
2347 let mut builder = VerbRegistryBuilder::new();
2348 builder.register(AlphaPack);
2349 builder.with_gate(Arc::new(SqlTestDenyGate));
2350 builder.with_event_store(sql_store.clone());
2351 let reg = builder.build().expect("registry builds");
2352
2353 let err = reg
2355 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2356 .await
2357 .unwrap_err();
2358 assert!(
2359 matches!(err, RuntimeError::PermissionDenied { .. }),
2360 "expected PermissionDenied, got {err:?}"
2361 );
2362
2363 let page = sql_store
2365 .query_events(
2366 EventFilter::default(),
2367 PageRequest {
2368 limit: 10,
2369 offset: 0,
2370 },
2371 )
2372 .await
2373 .unwrap();
2374 assert_eq!(
2375 page.items.len(),
2376 1,
2377 "one audit event must be persisted on deny through SqlEventStore"
2378 );
2379
2380 let ev = &page.items[0];
2381 assert_eq!(ev.outcome, EventOutcome::Denied);
2382
2383 let data = &ev.payload;
2387
2388 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
2389 .expect("Event.payload must deserialize to AuditEvent after SQL round-trip");
2390
2391 assert_eq!(
2392 audit.deny_reason.as_deref(),
2393 Some("sql-path: write denied"),
2394 "deny_reason must survive the SQL text round-trip"
2395 );
2396 assert_eq!(
2397 audit.gate_impl, "SqlTestDenyGate",
2398 "gate_impl must survive the SQL text round-trip"
2399 );
2400 assert_eq!(
2401 audit.decision,
2402 khive_gate::AuditDecision::Deny,
2403 "decision field must survive the SQL text round-trip"
2404 );
2405 assert!(
2408 audit.obligations.is_empty(),
2409 "obligations must be preserved as empty [] through SQL round-trip"
2410 );
2411 }
2412
2413 #[tokio::test]
2425 async fn sql_backed_audit_envelope_round_trips_non_empty_obligations() {
2426 use khive_gate::Obligation;
2427
2428 #[derive(Debug)]
2429 struct SqlTestAllowWithObligationGate;
2430 impl Gate for SqlTestAllowWithObligationGate {
2431 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2432 Ok(GateDecision::allow_with(vec![Obligation::Audit {
2433 tag: "sql-path-billing.meter".into(),
2434 }]))
2435 }
2436 fn impl_name(&self) -> &'static str {
2437 "SqlTestAllowWithObligationGate"
2438 }
2439 }
2440
2441 let rt = KhiveRuntime::memory().expect("in-memory runtime");
2442 let test_tok = NamespaceToken::for_namespace(Namespace::parse("test-ns").unwrap());
2443 let sql_store = rt
2444 .events(&test_tok)
2445 .expect("events_for_namespace must succeed");
2446
2447 let mut builder = VerbRegistryBuilder::new();
2448 builder.register(AlphaPack);
2449 builder.with_gate(Arc::new(SqlTestAllowWithObligationGate));
2450 builder.with_event_store(sql_store.clone());
2451 let reg = builder.build().expect("registry builds");
2452
2453 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2455 .await
2456 .expect("dispatch must succeed when gate allows");
2457
2458 let page = sql_store
2460 .query_events(
2461 EventFilter::default(),
2462 PageRequest {
2463 limit: 10,
2464 offset: 0,
2465 },
2466 )
2467 .await
2468 .unwrap();
2469 assert_eq!(
2470 page.items.len(),
2471 1,
2472 "one audit event must be persisted on allow through SqlEventStore"
2473 );
2474
2475 let ev = &page.items[0];
2476 assert_eq!(ev.outcome, EventOutcome::Success);
2477
2478 let data = &ev.payload;
2479
2480 let obligations_raw = data
2485 .get("obligations")
2486 .expect("Event.data JSON must contain 'obligations' key");
2487 let obligations_arr = obligations_raw
2488 .as_array()
2489 .expect("'obligations' must be a JSON array");
2490 assert!(
2491 !obligations_arr.is_empty(),
2492 "raw Event.data['obligations'] must be non-empty after SQL round-trip"
2493 );
2494
2495 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
2498 .expect("Event.data must deserialize to AuditEvent after SQL round-trip");
2499
2500 assert_eq!(
2501 audit.gate_impl, "SqlTestAllowWithObligationGate",
2502 "gate_impl must survive the SQL text round-trip"
2503 );
2504 assert_eq!(
2505 audit.decision,
2506 khive_gate::AuditDecision::Allow,
2507 "decision field must survive the SQL text round-trip"
2508 );
2509 assert_eq!(
2510 audit.obligations.len(),
2511 1,
2512 "obligations must be non-empty after SQL round-trip (not silently defaulted to [])"
2513 );
2514 match &audit.obligations[0] {
2515 Obligation::Audit { tag } => assert_eq!(
2516 tag, "sql-path-billing.meter",
2517 "Audit obligation tag must survive the SQL text round-trip"
2518 ),
2519 other => panic!("expected Audit obligation, got {other:?}"),
2520 }
2521 }
2522
2523 #[tokio::test]
2531 async fn audit_event_payload_shape_for_create_verb_matches_adr035_envelope() {
2532 let store = Arc::new(MemoryEventStore::default());
2533 let mut builder = VerbRegistryBuilder::new();
2534 builder.register(AlphaPack);
2535 builder.with_event_store(store.clone());
2536 builder.with_default_namespace("test-ns");
2537 let reg = builder.build().expect("registry builds");
2538
2539 reg.dispatch("create", serde_json::json!({"namespace": "test-ns"}))
2542 .await
2543 .unwrap();
2544
2545 let count = store.count_events(EventFilter::default()).await.unwrap();
2546 assert_eq!(count, 1, "exactly one audit event for one dispatch");
2547
2548 let page = store
2549 .query_events(
2550 EventFilter::default(),
2551 PageRequest {
2552 limit: 10,
2553 offset: 0,
2554 },
2555 )
2556 .await
2557 .unwrap();
2558 let ev = &page.items[0];
2559
2560 assert_eq!(ev.verb, "create", "ev.verb must be the dispatched verb");
2562 assert_eq!(
2563 ev.outcome,
2564 EventOutcome::Success,
2565 "ev.outcome must be Success on allow"
2566 );
2567 assert_eq!(
2568 ev.namespace, "test-ns",
2569 "ev.namespace must match the dispatch namespace"
2570 );
2571
2572 let data = &ev.payload;
2574
2575 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
2576 .expect("ev.payload must deserialize to AuditEvent");
2577
2578 assert_eq!(
2579 audit.decision,
2580 khive_gate::AuditDecision::Allow,
2581 "AuditEvent.decision must be Allow"
2582 );
2583 assert_eq!(audit.verb, "create", "AuditEvent.verb must be 'create'");
2584 assert_eq!(
2585 audit.namespace, "test-ns",
2586 "AuditEvent.namespace must be preserved"
2587 );
2588 assert_eq!(
2589 audit.gate_impl, "AllowAllGate",
2590 "AuditEvent.gate_impl must name the gate implementation"
2591 );
2592 assert!(
2593 audit.deny_reason.is_none(),
2594 "AuditEvent.deny_reason must be None on Allow"
2595 );
2596 let payload_json: serde_json::Value =
2598 serde_json::from_value(data.clone()).expect("data must be valid JSON");
2599 assert_eq!(
2600 payload_json["obligations"],
2601 serde_json::Value::Array(Vec::new()),
2602 "obligations must be [] on AllowAllGate (wire-shape rule ADR-033)"
2603 );
2604 }
2605}
2606
2607#[cfg(test)]
2610mod dep_tests {
2611 use super::*;
2612 use async_trait::async_trait;
2613 use khive_types::Pack;
2614 use serde_json::Value;
2615
2616 struct KgDepPack;
2617 struct MemoryDepPack;
2618 struct ADepPack;
2619 struct BDepPack;
2620
2621 impl Pack for KgDepPack {
2622 const NAME: &'static str = "kg_dep";
2623 const NOTE_KINDS: &'static [&'static str] = &["observation"];
2624 const ENTITY_KINDS: &'static [&'static str] = &["concept"];
2625 const HANDLERS: &'static [HandlerDef] = &[];
2626 }
2627
2628 impl Pack for MemoryDepPack {
2629 const NAME: &'static str = "memory_dep";
2630 const NOTE_KINDS: &'static [&'static str] = &["memory"];
2631 const ENTITY_KINDS: &'static [&'static str] = &[];
2632 const HANDLERS: &'static [HandlerDef] = &[];
2633 const REQUIRES: &'static [&'static str] = &["kg_dep"];
2634 }
2635
2636 impl Pack for ADepPack {
2637 const NAME: &'static str = "pack_a";
2638 const NOTE_KINDS: &'static [&'static str] = &[];
2639 const ENTITY_KINDS: &'static [&'static str] = &[];
2640 const HANDLERS: &'static [HandlerDef] = &[];
2641 const REQUIRES: &'static [&'static str] = &["pack_b"];
2642 }
2643
2644 impl Pack for BDepPack {
2645 const NAME: &'static str = "pack_b";
2646 const NOTE_KINDS: &'static [&'static str] = &[];
2647 const ENTITY_KINDS: &'static [&'static str] = &[];
2648 const HANDLERS: &'static [HandlerDef] = &[];
2649 const REQUIRES: &'static [&'static str] = &["pack_a"];
2650 }
2651
2652 #[async_trait]
2653 impl PackRuntime for KgDepPack {
2654 fn name(&self) -> &str {
2655 Self::NAME
2656 }
2657 fn note_kinds(&self) -> &'static [&'static str] {
2658 Self::NOTE_KINDS
2659 }
2660 fn entity_kinds(&self) -> &'static [&'static str] {
2661 Self::ENTITY_KINDS
2662 }
2663 fn handlers(&self) -> &'static [HandlerDef] {
2664 Self::HANDLERS
2665 }
2666 async fn dispatch(
2667 &self,
2668 verb: &str,
2669 _: Value,
2670 _: &VerbRegistry,
2671 _: &NamespaceToken,
2672 ) -> Result<Value, RuntimeError> {
2673 Err(RuntimeError::InvalidInput(format!(
2674 "KgDepPack has no verbs: {verb}"
2675 )))
2676 }
2677 }
2678
2679 #[async_trait]
2680 impl PackRuntime for MemoryDepPack {
2681 fn name(&self) -> &str {
2682 Self::NAME
2683 }
2684 fn note_kinds(&self) -> &'static [&'static str] {
2685 Self::NOTE_KINDS
2686 }
2687 fn entity_kinds(&self) -> &'static [&'static str] {
2688 Self::ENTITY_KINDS
2689 }
2690 fn handlers(&self) -> &'static [HandlerDef] {
2691 Self::HANDLERS
2692 }
2693 fn requires(&self) -> &'static [&'static str] {
2694 Self::REQUIRES
2695 }
2696 async fn dispatch(
2697 &self,
2698 verb: &str,
2699 _: Value,
2700 _: &VerbRegistry,
2701 _: &NamespaceToken,
2702 ) -> Result<Value, RuntimeError> {
2703 Err(RuntimeError::InvalidInput(format!(
2704 "MemoryDepPack has no verbs: {verb}"
2705 )))
2706 }
2707 }
2708
2709 #[async_trait]
2710 impl PackRuntime for ADepPack {
2711 fn name(&self) -> &str {
2712 Self::NAME
2713 }
2714 fn note_kinds(&self) -> &'static [&'static str] {
2715 Self::NOTE_KINDS
2716 }
2717 fn entity_kinds(&self) -> &'static [&'static str] {
2718 Self::ENTITY_KINDS
2719 }
2720 fn handlers(&self) -> &'static [HandlerDef] {
2721 Self::HANDLERS
2722 }
2723 fn requires(&self) -> &'static [&'static str] {
2724 Self::REQUIRES
2725 }
2726 async fn dispatch(
2727 &self,
2728 verb: &str,
2729 _: Value,
2730 _: &VerbRegistry,
2731 _: &NamespaceToken,
2732 ) -> Result<Value, RuntimeError> {
2733 Err(RuntimeError::InvalidInput(format!(
2734 "ADepPack has no verbs: {verb}"
2735 )))
2736 }
2737 }
2738
2739 #[async_trait]
2740 impl PackRuntime for BDepPack {
2741 fn name(&self) -> &str {
2742 Self::NAME
2743 }
2744 fn note_kinds(&self) -> &'static [&'static str] {
2745 Self::NOTE_KINDS
2746 }
2747 fn entity_kinds(&self) -> &'static [&'static str] {
2748 Self::ENTITY_KINDS
2749 }
2750 fn handlers(&self) -> &'static [HandlerDef] {
2751 Self::HANDLERS
2752 }
2753 fn requires(&self) -> &'static [&'static str] {
2754 Self::REQUIRES
2755 }
2756 async fn dispatch(
2757 &self,
2758 verb: &str,
2759 _: Value,
2760 _: &VerbRegistry,
2761 _: &NamespaceToken,
2762 ) -> Result<Value, RuntimeError> {
2763 Err(RuntimeError::InvalidInput(format!(
2764 "BDepPack has no verbs: {verb}"
2765 )))
2766 }
2767 }
2768
2769 #[test]
2770 fn test_pack_deps_happy_path() {
2771 let mut builder = VerbRegistryBuilder::new();
2772 builder.register(MemoryDepPack);
2773 builder.register(KgDepPack);
2774 let reg = builder
2775 .build()
2776 .expect("kg_dep satisfies memory_dep dependency");
2777 assert_eq!(reg.pack_requires("memory_dep").unwrap(), &["kg_dep"]);
2778 let names = reg.pack_names();
2779 let kg_pos = names.iter().position(|&n| n == "kg_dep").unwrap();
2780 let mem_pos = names.iter().position(|&n| n == "memory_dep").unwrap();
2781 assert!(
2782 kg_pos < mem_pos,
2783 "kg_dep must be loaded before memory_dep; order: {names:?}"
2784 );
2785 }
2786
2787 #[test]
2788 fn test_pack_deps_missing() {
2789 let mut builder = VerbRegistryBuilder::new();
2790 builder.register(MemoryDepPack);
2791 let err = match builder.build() {
2792 Ok(_) => panic!("expected Err, got Ok"),
2793 Err(e) => e,
2794 };
2795 assert!(
2796 matches!(err, RuntimeError::MissingPackDependency(_)),
2797 "expected MissingPackDependency, got {err:?}"
2798 );
2799 let msg = err.to_string();
2800 assert!(
2801 msg.contains("memory_dep"),
2802 "error must name the dependent pack: {msg}"
2803 );
2804 assert!(
2805 msg.contains("kg_dep"),
2806 "error must name the missing dep: {msg}"
2807 );
2808 }
2809
2810 #[test]
2811 fn test_pack_deps_circular() {
2812 let mut builder = VerbRegistryBuilder::new();
2813 builder.register(ADepPack);
2814 builder.register(BDepPack);
2815 let err = match builder.build() {
2816 Ok(_) => panic!("expected Err, got Ok"),
2817 Err(e) => e,
2818 };
2819 assert!(
2820 matches!(err, RuntimeError::CircularPackDependency(_)),
2821 "expected CircularPackDependency, got {err:?}"
2822 );
2823 let msg = err.to_string();
2824 assert!(msg.contains("pack_a"), "error must name pack_a: {msg}");
2825 assert!(msg.contains("pack_b"), "error must name pack_b: {msg}");
2826 }
2827
2828 #[test]
2829 fn test_pack_deps_no_deps() {
2830 struct NoDepsA;
2831 struct NoDepsB;
2832
2833 impl Pack for NoDepsA {
2834 const NAME: &'static str = "no_deps_a";
2835 const NOTE_KINDS: &'static [&'static str] = &[];
2836 const ENTITY_KINDS: &'static [&'static str] = &[];
2837 const HANDLERS: &'static [HandlerDef] = &[];
2838 }
2839
2840 impl Pack for NoDepsB {
2841 const NAME: &'static str = "no_deps_b";
2842 const NOTE_KINDS: &'static [&'static str] = &[];
2843 const ENTITY_KINDS: &'static [&'static str] = &[];
2844 const HANDLERS: &'static [HandlerDef] = &[];
2845 }
2846
2847 #[async_trait]
2848 impl PackRuntime for NoDepsA {
2849 fn name(&self) -> &str {
2850 Self::NAME
2851 }
2852 fn note_kinds(&self) -> &'static [&'static str] {
2853 Self::NOTE_KINDS
2854 }
2855 fn entity_kinds(&self) -> &'static [&'static str] {
2856 Self::ENTITY_KINDS
2857 }
2858 fn handlers(&self) -> &'static [HandlerDef] {
2859 Self::HANDLERS
2860 }
2861 async fn dispatch(
2862 &self,
2863 verb: &str,
2864 _: Value,
2865 _: &VerbRegistry,
2866 _: &NamespaceToken,
2867 ) -> Result<Value, RuntimeError> {
2868 Err(RuntimeError::InvalidInput(format!("NoDepsA: {verb}")))
2869 }
2870 }
2871
2872 #[async_trait]
2873 impl PackRuntime for NoDepsB {
2874 fn name(&self) -> &str {
2875 Self::NAME
2876 }
2877 fn note_kinds(&self) -> &'static [&'static str] {
2878 Self::NOTE_KINDS
2879 }
2880 fn entity_kinds(&self) -> &'static [&'static str] {
2881 Self::ENTITY_KINDS
2882 }
2883 fn handlers(&self) -> &'static [HandlerDef] {
2884 Self::HANDLERS
2885 }
2886 async fn dispatch(
2887 &self,
2888 verb: &str,
2889 _: Value,
2890 _: &VerbRegistry,
2891 _: &NamespaceToken,
2892 ) -> Result<Value, RuntimeError> {
2893 Err(RuntimeError::InvalidInput(format!("NoDepsB: {verb}")))
2894 }
2895 }
2896
2897 let mut builder = VerbRegistryBuilder::new();
2898 builder.register(NoDepsA);
2899 builder.register(NoDepsB);
2900 let reg = builder.build().expect("packs with REQUIRES=&[] build");
2901 assert_eq!(reg.pack_requires("no_deps_a").unwrap(), &[] as &[&str]);
2902 assert_eq!(reg.pack_requires("no_deps_b").unwrap(), &[] as &[&str]);
2903 }
2904}
2905
2906#[cfg(test)]
2909mod hook_tests {
2910 use super::*;
2911 use async_trait::async_trait;
2912 use khive_types::Pack;
2913 use std::sync::atomic::{AtomicUsize, Ordering};
2914 use std::sync::Mutex as StdMutex;
2915
2916 struct SimplePack;
2917
2918 impl Pack for SimplePack {
2919 const NAME: &'static str = "simple";
2920 const NOTE_KINDS: &'static [&'static str] = &[];
2921 const ENTITY_KINDS: &'static [&'static str] = &[];
2922 const HANDLERS: &'static [HandlerDef] = &[HandlerDef {
2923 name: "ping",
2924 description: "ping",
2925 visibility: Visibility::Verb,
2926 category: VerbCategory::Assertive,
2927 }];
2928 }
2929
2930 #[async_trait]
2931 impl PackRuntime for SimplePack {
2932 fn name(&self) -> &str {
2933 SimplePack::NAME
2934 }
2935 fn note_kinds(&self) -> &'static [&'static str] {
2936 SimplePack::NOTE_KINDS
2937 }
2938 fn entity_kinds(&self) -> &'static [&'static str] {
2939 SimplePack::ENTITY_KINDS
2940 }
2941 fn handlers(&self) -> &'static [HandlerDef] {
2942 SimplePack::HANDLERS
2943 }
2944 async fn dispatch(
2945 &self,
2946 verb: &str,
2947 _params: Value,
2948 _registry: &VerbRegistry,
2949 _token: &NamespaceToken,
2950 ) -> Result<Value, RuntimeError> {
2951 Ok(serde_json::json!({ "verb": verb }))
2952 }
2953 }
2954
2955 #[derive(Default)]
2957 struct CountingHook {
2958 calls: AtomicUsize,
2959 last_verb: StdMutex<String>,
2960 }
2961
2962 #[async_trait]
2963 impl DispatchHook for CountingHook {
2964 async fn on_dispatch(&self, view: &EventView) {
2965 self.calls.fetch_add(1, Ordering::SeqCst);
2966 *self.last_verb.lock().unwrap() = view.event.verb.clone();
2967 }
2968 }
2969
2970 #[tokio::test]
2971 async fn dispatch_hook_fires_on_successful_dispatch() {
2972 let hook = Arc::new(CountingHook::default());
2973 let mut builder = VerbRegistryBuilder::new();
2974 builder.register(SimplePack);
2975 builder.with_dispatch_hook(hook.clone());
2976 let reg = builder.build().expect("registry builds");
2977
2978 reg.dispatch("ping", Value::Null).await.unwrap();
2979
2980 assert_eq!(
2981 hook.calls.load(Ordering::SeqCst),
2982 1,
2983 "hook must fire once per successful dispatch"
2984 );
2985 assert_eq!(
2986 hook.last_verb.lock().unwrap().as_str(),
2987 "ping",
2988 "hook event must carry the dispatched verb"
2989 );
2990 }
2991
2992 #[tokio::test]
2993 async fn dispatch_hook_fires_multiple_times() {
2994 let hook = Arc::new(CountingHook::default());
2995 let mut builder = VerbRegistryBuilder::new();
2996 builder.register(SimplePack);
2997 builder.with_dispatch_hook(hook.clone());
2998 let reg = builder.build().expect("registry builds");
2999
3000 reg.dispatch("ping", Value::Null).await.unwrap();
3001 reg.dispatch("ping", Value::Null).await.unwrap();
3002 reg.dispatch("ping", Value::Null).await.unwrap();
3003
3004 assert_eq!(
3005 hook.calls.load(Ordering::SeqCst),
3006 3,
3007 "hook must fire once per successful dispatch"
3008 );
3009 }
3010
3011 #[tokio::test]
3012 async fn dispatch_hook_does_not_fire_on_unknown_verb() {
3013 let hook = Arc::new(CountingHook::default());
3014 let mut builder = VerbRegistryBuilder::new();
3015 builder.register(SimplePack);
3016 builder.with_dispatch_hook(hook.clone());
3017 let reg = builder.build().expect("registry builds");
3018
3019 let _ = reg.dispatch("nonexistent", Value::Null).await;
3020
3021 assert_eq!(
3022 hook.calls.load(Ordering::SeqCst),
3023 0,
3024 "hook must NOT fire for unknown verb (dispatch returns error)"
3025 );
3026 }
3027
3028 #[tokio::test]
3029 async fn dispatch_hook_does_not_fire_on_gate_deny() {
3030 use khive_gate::{Gate, GateDecision, GateError};
3031
3032 #[derive(Debug)]
3033 struct AlwaysDenyGate;
3034 impl Gate for AlwaysDenyGate {
3035 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
3036 Ok(GateDecision::deny("test deny"))
3037 }
3038 }
3039
3040 let hook = Arc::new(CountingHook::default());
3041 let mut builder = VerbRegistryBuilder::new();
3042 builder.register(SimplePack);
3043 builder.with_gate(Arc::new(AlwaysDenyGate));
3044 builder.with_dispatch_hook(hook.clone());
3045 let reg = builder.build().expect("registry builds");
3046
3047 let err = reg.dispatch("ping", Value::Null).await.unwrap_err();
3048 assert!(matches!(err, RuntimeError::PermissionDenied { .. }));
3049
3050 assert_eq!(
3051 hook.calls.load(Ordering::SeqCst),
3052 0,
3053 "hook must NOT fire when gate denies dispatch"
3054 );
3055 }
3056
3057 #[tokio::test]
3058 async fn dispatch_hook_event_carries_namespace_from_params() {
3059 let hook = Arc::new(CountingHook::default());
3060
3061 #[derive(Default)]
3062 struct NsCapturingHook {
3063 ns: StdMutex<String>,
3064 }
3065
3066 #[async_trait]
3067 impl DispatchHook for NsCapturingHook {
3068 async fn on_dispatch(&self, view: &EventView) {
3069 *self.ns.lock().unwrap() = view.event.namespace.clone();
3070 }
3071 }
3072
3073 let ns_hook = Arc::new(NsCapturingHook::default());
3074 let mut builder = VerbRegistryBuilder::new();
3075 builder.register(SimplePack);
3076 builder.with_dispatch_hook(ns_hook.clone());
3077 let reg = builder.build().expect("registry builds");
3078
3079 reg.dispatch("ping", serde_json::json!({"namespace": "tenant-abc"}))
3080 .await
3081 .unwrap();
3082
3083 assert_eq!(
3084 ns_hook.ns.lock().unwrap().as_str(),
3085 "tenant-abc",
3086 "dispatch hook event must carry the resolved namespace"
3087 );
3088
3089 drop(hook);
3091 }
3092
3093 #[tokio::test]
3094 async fn no_dispatch_hook_configured_dispatch_succeeds() {
3095 let mut builder = VerbRegistryBuilder::new();
3097 builder.register(SimplePack);
3098 let reg = builder.build().expect("registry builds");
3100
3101 let res = reg.dispatch("ping", Value::Null).await.unwrap();
3102 assert_eq!(res["verb"], "ping");
3103 }
3104}