1use std::collections::{HashMap, HashSet, VecDeque};
15use std::sync::Arc;
16
17use async_trait::async_trait;
18use khive_gate::{ActorRef, AllowAllGate, AuditEvent, GateDecision, GateRef, GateRequest};
19use khive_storage::{Event, EventStore, SubstrateKind};
20use khive_types::{EventOutcome, Namespace};
21use serde_json::Value;
22
23pub use khive_types::{EdgeEndpointRule, EndpointKind, VerbDef};
24
25#[async_trait]
36pub trait DispatchHook: Send + Sync {
37 async fn on_dispatch(&self, event: &Event);
42}
43
44use crate::error::{
45 CircularPackDependency, MissingPackDependencies, MissingPackDependency, RuntimeError,
46};
47use crate::KhiveRuntime;
48
49#[async_trait]
58pub trait PackRuntime: Send + Sync {
59 fn name(&self) -> &str;
61
62 fn note_kinds(&self) -> &'static [&'static str];
64
65 fn entity_kinds(&self) -> &'static [&'static str];
67
68 fn verbs(&self) -> &'static [VerbDef];
70
71 fn edge_rules(&self) -> &'static [EdgeEndpointRule] {
75 &[]
76 }
77
78 fn requires(&self) -> &'static [&'static str] {
81 &[]
82 }
83
84 fn kind_hook(&self, _kind: &str) -> Option<Arc<dyn KindHook>> {
92 None
93 }
94
95 async fn dispatch(
100 &self,
101 verb: &str,
102 params: Value,
103 registry: &VerbRegistry,
104 ) -> Result<Value, RuntimeError>;
105}
106
107#[async_trait]
121pub trait KindHook: Send + Sync + std::fmt::Debug {
122 async fn prepare_create(
128 &self,
129 runtime: &KhiveRuntime,
130 args: &mut Value,
131 ) -> Result<(), RuntimeError>;
132
133 async fn after_create(
142 &self,
143 runtime: &KhiveRuntime,
144 id: uuid::Uuid,
145 args: &Value,
146 ) -> Result<(), RuntimeError>;
147}
148
149pub struct VerbRegistryBuilder {
154 packs: Vec<Box<dyn PackRuntime>>,
155 gate: GateRef,
156 default_namespace: String,
157 event_store: Option<Arc<dyn EventStore>>,
164 dispatch_hook: Option<Arc<dyn DispatchHook>>,
170}
171
172impl VerbRegistryBuilder {
173 pub fn new() -> Self {
174 Self {
175 packs: Vec::new(),
176 gate: std::sync::Arc::new(AllowAllGate),
177 default_namespace: Namespace::default_ns().as_str().to_string(),
178 event_store: None,
179 dispatch_hook: None,
180 }
181 }
182
183 pub fn register<P: khive_types::Pack + PackRuntime + 'static>(&mut self, pack: P) -> &mut Self {
186 self.packs.push(Box::new(pack));
187 self
188 }
189
190 pub(crate) fn register_boxed(&mut self, pack: Box<dyn PackRuntime>) -> &mut Self {
197 self.packs.push(pack);
198 self
199 }
200
201 pub fn with_gate(&mut self, gate: GateRef) -> &mut Self {
206 self.gate = gate;
207 self
208 }
209
210 pub fn with_default_namespace(&mut self, ns: impl Into<String>) -> &mut Self {
215 self.default_namespace = ns.into();
216 self
217 }
218
219 pub fn with_event_store(&mut self, store: Arc<dyn EventStore>) -> &mut Self {
228 self.event_store = Some(store);
229 self
230 }
231
232 pub fn with_dispatch_hook(&mut self, hook: Arc<dyn DispatchHook>) -> &mut Self {
243 self.dispatch_hook = Some(hook);
244 self
245 }
246
247 pub fn build(self) -> Result<VerbRegistry, RuntimeError> {
253 let packs = self.packs;
254 let mut name_to_idx: HashMap<&str, usize> = HashMap::with_capacity(packs.len());
255 for (idx, pack) in packs.iter().enumerate() {
256 if let Some(prev_idx) = name_to_idx.insert(pack.name(), idx) {
257 return Err(RuntimeError::PackRedeclared {
258 name: pack.name().to_string(),
259 first_idx: prev_idx,
260 second_idx: idx,
261 });
262 }
263 }
264
265 let mut missing: Vec<MissingPackDependency> = Vec::new();
266 let mut indegree = vec![0usize; packs.len()];
267 let mut dependents: Vec<Vec<usize>> = vec![Vec::new(); packs.len()];
268
269 for (idx, pack) in packs.iter().enumerate() {
270 for &requires in pack.requires() {
271 match name_to_idx.get(requires).copied() {
272 Some(dep_idx) => {
273 dependents[dep_idx].push(idx);
274 indegree[idx] += 1;
275 }
276 None => missing.push(MissingPackDependency {
277 from: pack.name().to_string(),
278 requires: requires.to_string(),
279 }),
280 }
281 }
282 }
283
284 if !missing.is_empty() {
285 return if missing.len() == 1 {
286 Err(RuntimeError::MissingPackDependency(missing.remove(0)))
287 } else {
288 Err(RuntimeError::MissingPackDependencies(
289 MissingPackDependencies { missing },
290 ))
291 };
292 }
293
294 let mut ready: VecDeque<usize> = indegree
295 .iter()
296 .enumerate()
297 .filter_map(|(idx, degree)| (*degree == 0).then_some(idx))
298 .collect();
299 let mut ordered_indices = Vec::with_capacity(packs.len());
300
301 while let Some(idx) = ready.pop_front() {
302 ordered_indices.push(idx);
303 for &dep_idx in &dependents[idx] {
304 indegree[dep_idx] -= 1;
305 if indegree[dep_idx] == 0 {
306 ready.push_back(dep_idx);
307 }
308 }
309 }
310
311 if ordered_indices.len() != packs.len() {
312 let cycle_nodes: HashSet<usize> = indegree
313 .iter()
314 .enumerate()
315 .filter_map(|(idx, degree)| (*degree > 0).then_some(idx))
316 .collect();
317 let cycle = find_pack_dependency_cycle(&packs, &name_to_idx, &cycle_nodes);
318 return Err(RuntimeError::CircularPackDependency(
319 CircularPackDependency { cycle },
320 ));
321 }
322
323 let mut slots: Vec<Option<Box<dyn PackRuntime>>> = packs.into_iter().map(Some).collect();
324 let ordered_packs: Vec<Box<dyn PackRuntime>> = ordered_indices
325 .into_iter()
326 .map(|idx| slots[idx].take().expect("topological index must exist"))
327 .collect();
328
329 Ok(VerbRegistry {
330 packs: Arc::new(ordered_packs),
331 gate: self.gate,
332 default_namespace: self.default_namespace,
333 event_store: self.event_store,
334 dispatch_hook: self.dispatch_hook,
335 })
336 }
337}
338
339fn find_pack_dependency_cycle(
340 packs: &[Box<dyn PackRuntime>],
341 name_to_idx: &HashMap<&str, usize>,
342 cycle_nodes: &HashSet<usize>,
343) -> Vec<String> {
344 fn visit(
345 idx: usize,
346 packs: &[Box<dyn PackRuntime>],
347 name_to_idx: &HashMap<&str, usize>,
348 cycle_nodes: &HashSet<usize>,
349 visiting: &mut Vec<usize>,
350 visited: &mut HashSet<usize>,
351 ) -> Option<Vec<String>> {
352 if let Some(pos) = visiting.iter().position(|&seen| seen == idx) {
353 let mut cycle: Vec<String> = visiting[pos..]
354 .iter()
355 .map(|&i| packs[i].name().to_string())
356 .collect();
357 cycle.push(packs[idx].name().to_string());
358 return Some(cycle);
359 }
360 if !visited.insert(idx) {
361 return None;
362 }
363 visiting.push(idx);
364 for &req in packs[idx].requires() {
365 let Some(&dep_idx) = name_to_idx.get(req) else {
366 continue;
367 };
368 if cycle_nodes.contains(&dep_idx) {
369 if let Some(cycle) =
370 visit(dep_idx, packs, name_to_idx, cycle_nodes, visiting, visited)
371 {
372 return Some(cycle);
373 }
374 }
375 }
376 visiting.pop();
377 None
378 }
379
380 let mut visited = HashSet::new();
381 for &idx in cycle_nodes {
382 let mut visiting = Vec::new();
383 if let Some(cycle) = visit(
384 idx,
385 packs,
386 name_to_idx,
387 cycle_nodes,
388 &mut visiting,
389 &mut visited,
390 ) {
391 return cycle;
392 }
393 }
394 cycle_nodes
395 .iter()
396 .map(|&idx| packs[idx].name().to_string())
397 .collect()
398}
399
400impl Default for VerbRegistryBuilder {
401 fn default() -> Self {
402 Self::new()
403 }
404}
405
406#[derive(Clone)]
410pub struct VerbRegistry {
411 packs: std::sync::Arc<Vec<Box<dyn PackRuntime>>>,
412 gate: GateRef,
413 default_namespace: String,
414 event_store: Option<Arc<dyn EventStore>>,
416 dispatch_hook: Option<Arc<dyn DispatchHook>>,
418}
419
420impl VerbRegistry {
421 pub async fn dispatch(&self, verb: &str, params: Value) -> Result<Value, RuntimeError> {
452 let ns_str: String = params
455 .get("namespace")
456 .and_then(Value::as_str)
457 .map(str::to_string)
458 .unwrap_or_else(|| self.default_namespace.clone());
459 let gate_req = GateRequest::new(
460 ActorRef::anonymous(),
461 Namespace::new(&ns_str),
462 verb,
463 params.clone(),
464 );
465
466 let gate_blocked = match self.gate.check(&gate_req) {
472 Ok(decision) => {
473 let is_deny = matches!(decision, GateDecision::Deny { .. });
474
475 let audit = AuditEvent::from_check(&gate_req, &decision, self.gate.impl_name());
477 tracing::info!(
478 audit_event = %serde_json::to_string(&audit)
479 .unwrap_or_else(|_| "{\"error\":\"serialize\"}".into()),
480 "gate.check"
481 );
482
483 if let Some(store) = &self.event_store {
485 let outcome = if is_deny {
486 EventOutcome::Denied
487 } else {
488 EventOutcome::Success
489 };
490 let audit_data = serde_json::to_value(&audit).unwrap_or_else(|e| {
491 tracing::warn!(error = %e, "failed to serialize AuditEvent for EventStore");
492 serde_json::Value::Null
493 });
494 let storage_event = Event::new(
495 gate_req.namespace.as_str(),
496 verb,
497 SubstrateKind::Event,
498 format!("{}:{}", gate_req.actor.kind, gate_req.actor.id),
499 )
500 .with_outcome(outcome)
501 .with_data(audit_data);
502 if let Err(store_err) = store.append_event(storage_event).await {
503 tracing::warn!(
504 verb,
505 error = %store_err,
506 "audit event store write failed (non-fatal)"
507 );
508 }
509 }
510
511 if is_deny {
512 let reason = match decision {
513 GateDecision::Deny { reason } => reason,
514 _ => String::new(),
515 };
516 Some(reason)
517 } else {
518 None
519 }
520 }
521 Err(err) => {
522 tracing::warn!(verb, error = %err, "gate check failed (fail-open)");
525 None
526 }
527 };
528
529 if let Some(reason) = gate_blocked {
531 return Err(RuntimeError::PermissionDenied {
532 verb: verb.to_string(),
533 reason,
534 });
535 }
536
537 for pack in self.packs.iter() {
538 if pack.verbs().iter().any(|v| v.name == verb) {
539 let result = pack.dispatch(verb, params, self).await;
540
541 if let (Ok(_), Some(hook)) = (&result, &self.dispatch_hook) {
543 let dispatch_event =
544 Event::new(ns_str.as_str(), verb, SubstrateKind::Event, pack.name())
545 .with_outcome(EventOutcome::Success);
546 let hook = Arc::clone(hook);
547 hook.on_dispatch(&dispatch_event).await;
548 }
549
550 return result;
551 }
552 }
553 let available: Vec<&str> = self
554 .packs
555 .iter()
556 .flat_map(|p| p.verbs().iter().map(|v| v.name))
557 .collect();
558 Err(RuntimeError::InvalidInput(format!(
559 "unknown verb {verb:?}; available: {}",
560 available.join(", ")
561 )))
562 }
563
564 pub fn find_kind_hook(&self, kind: &str) -> Option<Arc<dyn KindHook>> {
571 for pack in self.packs.iter() {
572 let owns = pack.note_kinds().contains(&kind) || pack.entity_kinds().contains(&kind);
573 if owns {
574 if let Some(hook) = pack.kind_hook(kind) {
575 return Some(hook);
576 }
577 }
578 }
579 None
580 }
581
582 pub fn all_verbs(&self) -> Vec<&'static VerbDef> {
588 self.packs.iter().flat_map(|p| p.verbs().iter()).collect()
589 }
590
591 pub fn all_verbs_with_names(&self) -> Vec<(&str, &'static VerbDef)> {
597 self.packs
598 .iter()
599 .flat_map(|p| p.verbs().iter().map(move |v| (p.name(), v)))
600 .collect()
601 }
602
603 pub fn all_note_kinds(&self) -> Vec<&'static str> {
606 let mut seen = std::collections::HashSet::new();
607 self.packs
608 .iter()
609 .flat_map(|p| p.note_kinds().iter().copied())
610 .filter(|k| seen.insert(*k))
611 .collect()
612 }
613
614 pub fn all_entity_kinds(&self) -> Vec<&'static str> {
617 let mut seen = std::collections::HashSet::new();
618 self.packs
619 .iter()
620 .flat_map(|p| p.entity_kinds().iter().copied())
621 .filter(|k| seen.insert(*k))
622 .collect()
623 }
624
625 pub fn pack_names(&self) -> Vec<&str> {
627 self.packs.iter().map(|p| p.name()).collect()
628 }
629
630 pub fn pack_requires(&self, name: &str) -> Option<&'static [&'static str]> {
632 self.packs
633 .iter()
634 .find(|p| p.name() == name)
635 .map(|p| p.requires())
636 }
637
638 pub fn pack_note_kinds(&self, name: &str) -> Option<&'static [&'static str]> {
643 self.packs
644 .iter()
645 .find(|p| p.name() == name)
646 .map(|p| p.note_kinds())
647 }
648
649 pub fn pack_entity_kinds(&self, name: &str) -> Option<&'static [&'static str]> {
654 self.packs
655 .iter()
656 .find(|p| p.name() == name)
657 .map(|p| p.entity_kinds())
658 }
659
660 pub fn pack_verbs(&self, name: &str) -> Option<&'static [VerbDef]> {
666 self.packs
667 .iter()
668 .find(|p| p.name() == name)
669 .map(|p| p.verbs())
670 }
671
672 pub fn all_edge_rules(&self) -> Vec<EdgeEndpointRule> {
678 self.packs
679 .iter()
680 .flat_map(|p| p.edge_rules().iter().copied())
681 .collect()
682 }
683}
684
685pub trait PackFactory: Send + Sync + 'static {
695 fn name(&self) -> &'static str;
697
698 fn requires(&self) -> &'static [&'static str] {
704 &[]
705 }
706
707 fn create(&self, runtime: KhiveRuntime) -> Box<dyn PackRuntime>;
709}
710
711pub struct PackRegistration(pub &'static dyn PackFactory);
716
717inventory::collect!(PackRegistration);
718
719pub struct PackRegistry;
724
725impl PackRegistry {
726 pub fn discovered_names() -> Vec<&'static str> {
728 inventory::iter::<PackRegistration>
729 .into_iter()
730 .map(|r| r.0.name())
731 .collect()
732 }
733
734 pub fn register_packs(
746 names: &[String],
747 runtime: KhiveRuntime,
748 builder: &mut VerbRegistryBuilder,
749 ) -> Result<(), String> {
750 let all: Vec<&'static dyn PackFactory> = inventory::iter::<PackRegistration>
752 .into_iter()
753 .map(|r| r.0)
754 .collect();
755 let factory_for = |name: &str| -> Option<&'static dyn PackFactory> {
756 all.iter().copied().find(|f| f.name() == name)
757 };
758
759 let mut full_set: std::collections::HashSet<&str> = std::collections::HashSet::new();
762 let mut queue: std::collections::VecDeque<&str> = std::collections::VecDeque::new();
763
764 for name in names {
765 queue.push_back(name.as_str());
766 }
767
768 while let Some(name) = queue.pop_front() {
769 if !full_set.insert(name) {
770 continue; }
772 let factory = factory_for(name).ok_or_else(|| name.to_string())?;
773 for &dep in factory.requires() {
774 if !full_set.contains(dep) {
775 queue.push_back(dep);
776 }
777 }
778 }
779
780 for name in &full_set {
783 let factory = factory_for(name).unwrap();
786 builder.register_boxed(factory.create(runtime.clone()));
787 }
788
789 Ok(())
790 }
791}
792
793#[cfg(test)]
794mod tests {
795 use super::*;
796 use khive_types::Pack;
797
798 struct AlphaPack;
799
800 impl Pack for AlphaPack {
801 const NAME: &'static str = "alpha";
802 const NOTE_KINDS: &'static [&'static str] = &["memo", "log"];
803 const ENTITY_KINDS: &'static [&'static str] = &["widget"];
804 const VERBS: &'static [VerbDef] = &[
805 VerbDef {
806 name: "create",
807 description: "create a widget",
808 },
809 VerbDef {
810 name: "list",
811 description: "list widgets",
812 },
813 ];
814 }
815
816 #[async_trait]
817 impl PackRuntime for AlphaPack {
818 fn name(&self) -> &str {
819 AlphaPack::NAME
820 }
821 fn note_kinds(&self) -> &'static [&'static str] {
822 AlphaPack::NOTE_KINDS
823 }
824 fn entity_kinds(&self) -> &'static [&'static str] {
825 AlphaPack::ENTITY_KINDS
826 }
827 fn verbs(&self) -> &'static [VerbDef] {
828 AlphaPack::VERBS
829 }
830 async fn dispatch(
831 &self,
832 verb: &str,
833 _params: Value,
834 _registry: &VerbRegistry,
835 ) -> Result<Value, RuntimeError> {
836 Ok(serde_json::json!({ "pack": "alpha", "verb": verb }))
837 }
838 }
839
840 struct BetaPack;
841
842 impl Pack for BetaPack {
843 const NAME: &'static str = "beta";
844 const NOTE_KINDS: &'static [&'static str] = &["log", "alert"];
845 const ENTITY_KINDS: &'static [&'static str] = &["widget", "gadget"];
846 const VERBS: &'static [VerbDef] = &[
847 VerbDef {
848 name: "notify",
849 description: "send alert",
850 },
851 VerbDef {
852 name: "create",
853 description: "create a gadget",
854 },
855 ];
856 }
857
858 #[async_trait]
859 impl PackRuntime for BetaPack {
860 fn name(&self) -> &str {
861 BetaPack::NAME
862 }
863 fn note_kinds(&self) -> &'static [&'static str] {
864 BetaPack::NOTE_KINDS
865 }
866 fn entity_kinds(&self) -> &'static [&'static str] {
867 BetaPack::ENTITY_KINDS
868 }
869 fn verbs(&self) -> &'static [VerbDef] {
870 BetaPack::VERBS
871 }
872 async fn dispatch(
873 &self,
874 verb: &str,
875 _params: Value,
876 _registry: &VerbRegistry,
877 ) -> Result<Value, RuntimeError> {
878 Ok(serde_json::json!({ "pack": "beta", "verb": verb }))
879 }
880 }
881
882 fn build_registry() -> VerbRegistry {
883 let mut builder = VerbRegistryBuilder::new();
884 builder.register(AlphaPack);
885 builder.register(BetaPack);
886 builder.build().expect("registry builds")
887 }
888
889 #[tokio::test]
890 async fn dispatch_routes_to_correct_pack() {
891 let reg = build_registry();
892
893 let res = reg.dispatch("list", Value::Null).await.unwrap();
894 assert_eq!(res["pack"], "alpha");
895
896 let res = reg.dispatch("notify", Value::Null).await.unwrap();
897 assert_eq!(res["pack"], "beta");
898 }
899
900 #[tokio::test]
901 async fn dispatch_first_registered_wins_on_collision() {
902 let reg = build_registry();
903
904 let res = reg.dispatch("create", Value::Null).await.unwrap();
905 assert_eq!(res["pack"], "alpha", "first registered pack wins");
906 }
907
908 #[tokio::test]
909 async fn dispatch_unknown_verb_returns_error() {
910 let reg = build_registry();
911
912 let err = reg.dispatch("explode", Value::Null).await.unwrap_err();
913 let msg = err.to_string();
914 assert!(msg.contains("explode"));
915 assert!(msg.contains("create"));
916 }
917
918 #[test]
919 fn all_verbs_aggregates_across_packs() {
920 let reg = build_registry();
921 let verbs: Vec<&str> = reg.all_verbs().iter().map(|v| v.name).collect();
922 assert_eq!(verbs, vec!["create", "list", "notify", "create"]);
923 }
924
925 #[test]
926 fn all_verbs_with_names_pairs_pack_name() {
927 let reg = build_registry();
928 let pairs: Vec<(&str, &str)> = reg
929 .all_verbs_with_names()
930 .iter()
931 .map(|(pack, v)| (*pack, v.name))
932 .collect();
933 assert_eq!(
934 pairs,
935 vec![
936 ("alpha", "create"),
937 ("alpha", "list"),
938 ("beta", "notify"),
939 ("beta", "create"),
940 ]
941 );
942 }
943
944 #[test]
945 fn note_kinds_are_deduplicated() {
946 let reg = build_registry();
947 let kinds = reg.all_note_kinds();
948 assert_eq!(kinds, vec!["memo", "log", "alert"]);
949 }
950
951 #[test]
952 fn entity_kinds_are_deduplicated() {
953 let reg = build_registry();
954 let kinds = reg.all_entity_kinds();
955 assert_eq!(kinds, vec!["widget", "gadget"]);
956 }
957
958 use khive_gate::{Gate, GateError};
961 use std::sync::atomic::{AtomicUsize, Ordering};
962 use std::sync::Arc;
963
964 #[derive(Default, Debug)]
965 struct CountingGate {
966 calls: AtomicUsize,
967 deny_verb: Option<&'static str>,
968 }
969
970 impl Gate for CountingGate {
971 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
972 self.calls.fetch_add(1, Ordering::SeqCst);
973 if Some(req.verb.as_str()) == self.deny_verb {
974 Ok(GateDecision::deny(format!("test deny for {}", req.verb)))
975 } else {
976 Ok(GateDecision::allow())
977 }
978 }
979 }
980
981 #[tokio::test]
982 async fn dispatch_consults_the_gate() {
983 let gate = Arc::new(CountingGate::default());
984 let mut builder = VerbRegistryBuilder::new();
985 builder.register(AlphaPack);
986 builder.with_gate(gate.clone());
987 let reg = builder.build().expect("registry builds");
988
989 reg.dispatch("list", Value::Null).await.unwrap();
990 reg.dispatch("create", Value::Null).await.unwrap();
991 assert_eq!(
992 gate.calls.load(Ordering::SeqCst),
993 2,
994 "gate should be consulted once per dispatch"
995 );
996 }
997
998 #[tokio::test]
999 async fn dispatch_returns_permission_denied_on_deny_v03() {
1000 let gate = Arc::new(CountingGate {
1001 calls: AtomicUsize::new(0),
1002 deny_verb: Some("create"),
1003 });
1004 let mut builder = VerbRegistryBuilder::new();
1005 builder.register(AlphaPack);
1006 builder.with_gate(gate.clone());
1007 let reg = builder.build().expect("registry builds");
1008
1009 let err = reg.dispatch("create", Value::Null).await.unwrap_err();
1011 assert!(
1012 matches!(err, RuntimeError::PermissionDenied { ref verb, .. } if verb == "create"),
1013 "expected PermissionDenied, got {err:?}"
1014 );
1015 let msg = err.to_string();
1016 assert!(
1017 msg.contains("create"),
1018 "error message must name the verb: {msg}"
1019 );
1020 assert!(
1021 msg.contains("test deny for create"),
1022 "error message must carry the deny reason: {msg}"
1023 );
1024 assert_eq!(gate.calls.load(Ordering::SeqCst), 1);
1025 }
1026
1027 #[tokio::test]
1028 async fn dispatch_allow_verb_succeeds_even_with_deny_gate_for_other_verb() {
1029 let gate = Arc::new(CountingGate {
1031 calls: AtomicUsize::new(0),
1032 deny_verb: Some("create"),
1033 });
1034 let mut builder = VerbRegistryBuilder::new();
1035 builder.register(AlphaPack);
1036 builder.with_gate(gate.clone());
1037 let reg = builder.build().expect("registry builds");
1038
1039 let res = reg.dispatch("list", Value::Null).await.unwrap();
1040 assert_eq!(res["pack"], "alpha");
1041 }
1042
1043 #[tokio::test]
1044 async fn dispatch_uses_allow_all_gate_by_default() {
1045 let reg = build_registry();
1047 let res = reg.dispatch("list", Value::Null).await.unwrap();
1048 assert_eq!(res["pack"], "alpha");
1049 }
1050
1051 #[derive(Default, Debug)]
1054 struct NamespaceCapturingGate {
1055 seen: std::sync::Mutex<Vec<String>>,
1056 }
1057
1058 impl Gate for NamespaceCapturingGate {
1059 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
1060 self.seen
1061 .lock()
1062 .unwrap()
1063 .push(req.namespace.as_str().to_string());
1064 Ok(GateDecision::allow())
1065 }
1066 }
1067
1068 #[tokio::test]
1069 async fn dispatch_propagates_params_namespace_to_gate() {
1070 let gate = Arc::new(NamespaceCapturingGate::default());
1071 let mut builder = VerbRegistryBuilder::new();
1072 builder.register(AlphaPack);
1073 builder.with_gate(gate.clone());
1074 builder.with_default_namespace("tenant-x");
1075 let reg = builder.build().expect("registry builds");
1076
1077 reg.dispatch("list", serde_json::json!({"namespace": "tenant-y"}))
1079 .await
1080 .unwrap();
1081 reg.dispatch("list", Value::Null).await.unwrap();
1083 reg.dispatch("list", serde_json::json!({"namespace": ""}))
1088 .await
1089 .unwrap();
1090
1091 let seen = gate.seen.lock().unwrap().clone();
1092 assert_eq!(seen, vec!["tenant-y", "tenant-x", ""]);
1093 }
1094
1095 #[tokio::test]
1096 async fn dispatch_falls_back_to_local_when_no_default_set() {
1097 let gate = Arc::new(NamespaceCapturingGate::default());
1099 let mut builder = VerbRegistryBuilder::new();
1100 builder.register(AlphaPack);
1101 builder.with_gate(gate.clone());
1102 let reg = builder.build().expect("registry builds");
1103
1104 reg.dispatch("list", Value::Null).await.unwrap();
1105 let seen = gate.seen.lock().unwrap().clone();
1106 assert_eq!(seen, vec!["local"]);
1107 }
1108
1109 use khive_gate::{AuditDecision, AuditEvent, Obligation};
1112
1113 #[derive(Default, Debug)]
1115 struct AuditCapturingGate {
1116 events: std::sync::Mutex<Vec<AuditEvent>>,
1117 deny_verb: Option<&'static str>,
1118 }
1119
1120 impl Gate for AuditCapturingGate {
1121 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
1122 let decision = if Some(req.verb.as_str()) == self.deny_verb {
1123 GateDecision::deny("test deny")
1124 } else {
1125 GateDecision::allow_with(vec![Obligation::Audit {
1126 tag: format!("{}.check", req.verb),
1127 }])
1128 };
1129 let ev = AuditEvent::from_check(req, &decision, self.impl_name());
1131 self.events.lock().unwrap().push(ev);
1132 Ok(decision)
1133 }
1134
1135 fn impl_name(&self) -> &'static str {
1136 "AuditCapturingGate"
1137 }
1138 }
1139
1140 #[tokio::test]
1141 async fn dispatch_emits_one_audit_event_per_call() {
1142 let gate = Arc::new(AuditCapturingGate::default());
1143 let mut builder = VerbRegistryBuilder::new();
1144 builder.register(AlphaPack);
1145 builder.with_gate(gate.clone());
1146 let reg = builder.build().expect("registry builds");
1147
1148 reg.dispatch("list", Value::Null).await.unwrap();
1149 reg.dispatch("create", Value::Null).await.unwrap();
1150
1151 let evs = gate.events.lock().unwrap();
1152 assert_eq!(evs.len(), 2, "exactly one audit event per dispatch call");
1153 }
1154
1155 #[tokio::test]
1156 async fn dispatch_audit_event_allow_carries_obligations() {
1157 let gate = Arc::new(AuditCapturingGate::default());
1158 let mut builder = VerbRegistryBuilder::new();
1159 builder.register(AlphaPack);
1160 builder.with_gate(gate.clone());
1161 let reg = builder.build().expect("registry builds");
1162
1163 reg.dispatch("list", Value::Null).await.unwrap();
1164
1165 let evs = gate.events.lock().unwrap();
1166 let ev = &evs[0];
1167 assert_eq!(ev.verb, "list");
1168 assert_eq!(ev.decision, AuditDecision::Allow);
1169 assert!(ev.deny_reason.is_none());
1170 assert_eq!(ev.obligations.len(), 1);
1171 assert_eq!(ev.gate_impl, "AuditCapturingGate");
1172 }
1173
1174 #[tokio::test]
1175 async fn dispatch_audit_event_deny_carries_reason() {
1176 let gate = Arc::new(AuditCapturingGate {
1177 events: Default::default(),
1178 deny_verb: Some("create"),
1179 });
1180 let mut builder = VerbRegistryBuilder::new();
1181 builder.register(AlphaPack);
1182 builder.with_gate(gate.clone());
1183 let reg = builder.build().expect("registry builds");
1184
1185 let err = reg.dispatch("create", Value::Null).await.unwrap_err();
1188 assert!(matches!(err, RuntimeError::PermissionDenied { .. }));
1189
1190 let evs = gate.events.lock().unwrap();
1191 let ev = &evs[0];
1192 assert_eq!(ev.verb, "create");
1193 assert_eq!(ev.decision, AuditDecision::Deny);
1194 assert_eq!(ev.deny_reason.as_deref(), Some("test deny"));
1195 assert!(ev.obligations.is_empty());
1196 }
1197
1198 #[tokio::test]
1199 async fn dispatch_audit_event_fields_match_gate_request() {
1200 let gate = Arc::new(AuditCapturingGate::default());
1201 let mut builder = VerbRegistryBuilder::new();
1202 builder.register(AlphaPack);
1203 builder.with_gate(gate.clone());
1204 builder.with_default_namespace("tenant-z");
1205 let reg = builder.build().expect("registry builds");
1206
1207 reg.dispatch("list", serde_json::json!({"namespace": "tenant-q"}))
1208 .await
1209 .unwrap();
1210
1211 let evs = gate.events.lock().unwrap();
1212 let ev = &evs[0];
1213 assert_eq!(ev.namespace, "tenant-q");
1215 assert_eq!(ev.verb, "list");
1216 assert_eq!(ev.actor.kind, "anonymous");
1217 }
1218
1219 use std::sync::{Mutex as StdMutex, Once, OnceLock};
1232
1233 use serial_test::serial;
1234 use tracing::field::{Field, Visit};
1235
1236 #[derive(Clone, Debug, Default)]
1237 struct CapturedEvent {
1238 message: Option<String>,
1239 audit_event: Option<String>,
1240 }
1241
1242 #[derive(Default)]
1243 struct CapturedEventVisitor(CapturedEvent);
1244
1245 impl Visit for CapturedEventVisitor {
1246 fn record_str(&mut self, field: &Field, value: &str) {
1247 match field.name() {
1248 "message" => self.0.message = Some(value.to_string()),
1249 "audit_event" => self.0.audit_event = Some(value.to_string()),
1250 _ => {}
1251 }
1252 }
1253
1254 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
1255 let formatted = format!("{value:?}");
1261 let cleaned = formatted
1262 .trim_start_matches('"')
1263 .trim_end_matches('"')
1264 .to_string();
1265 match field.name() {
1266 "message" => self.0.message = Some(cleaned),
1267 "audit_event" => self.0.audit_event = Some(cleaned),
1268 _ => {}
1269 }
1270 }
1271 }
1272
1273 struct CaptureSubscriber {
1286 events: Arc<StdMutex<Vec<CapturedEvent>>>,
1287 }
1288
1289 impl CaptureSubscriber {
1290 fn new(events: Arc<StdMutex<Vec<CapturedEvent>>>) -> Self {
1291 Self { events }
1292 }
1293 }
1294
1295 impl tracing::Subscriber for CaptureSubscriber {
1296 fn enabled(&self, _: &tracing::Metadata<'_>) -> bool {
1297 true
1298 }
1299 fn new_span(&self, _: &tracing::span::Attributes<'_>) -> tracing::span::Id {
1300 tracing::span::Id::from_u64(1)
1301 }
1302 fn record(&self, _: &tracing::span::Id, _: &tracing::span::Record<'_>) {}
1303 fn record_follows_from(&self, _: &tracing::span::Id, _: &tracing::span::Id) {}
1304 fn event(&self, event: &tracing::Event<'_>) {
1305 let mut visitor = CapturedEventVisitor::default();
1306 event.record(&mut visitor);
1307 self.events.lock().unwrap().push(visitor.0);
1308 }
1309 fn enter(&self, _: &tracing::span::Id) {}
1310 fn exit(&self, _: &tracing::span::Id) {}
1311 }
1312
1313 static GLOBAL_CAPTURE: OnceLock<Arc<StdMutex<Vec<CapturedEvent>>>> = OnceLock::new();
1323 static GLOBAL_INIT: Once = Once::new();
1324
1325 fn global_capture() -> Arc<StdMutex<Vec<CapturedEvent>>> {
1326 GLOBAL_INIT.call_once(|| {
1327 let buffer = Arc::new(StdMutex::new(Vec::new()));
1328 let subscriber = CaptureSubscriber::new(Arc::clone(&buffer));
1329 let _ = tracing::subscriber::set_global_default(subscriber);
1334 let _ = GLOBAL_CAPTURE.set(buffer);
1335 });
1336 Arc::clone(GLOBAL_CAPTURE.get().expect("global capture initialized"))
1337 }
1338
1339 fn capture_dispatch_events<Fut>(future: Fut) -> Vec<CapturedEvent>
1344 where
1345 Fut: std::future::Future<Output = ()>,
1346 {
1347 let buffer = global_capture();
1348 buffer.lock().unwrap().clear();
1349
1350 let rt = tokio::runtime::Builder::new_current_thread()
1351 .enable_all()
1352 .build()
1353 .expect("build current-thread tokio runtime");
1354 rt.block_on(future);
1355
1356 let result = buffer.lock().unwrap().clone();
1357 result
1358 }
1359
1360 fn gate_check_events_for(events: &[CapturedEvent], gate_impl: &str) -> Vec<CapturedEvent> {
1367 events
1368 .iter()
1369 .filter(|e| e.message.as_deref() == Some("gate.check"))
1370 .filter(|e| {
1371 e.audit_event
1372 .as_deref()
1373 .and_then(|s| serde_json::from_str::<serde_json::Value>(s).ok())
1374 .and_then(|v| {
1375 v.get("gate_impl")
1376 .and_then(|g| g.as_str().map(|s| s.to_string()))
1377 })
1378 .as_deref()
1379 == Some(gate_impl)
1380 })
1381 .cloned()
1382 .collect()
1383 }
1384
1385 #[test]
1386 #[serial]
1387 fn dispatch_tracing_emits_one_gate_check_event_on_allow() {
1388 #[derive(Debug)]
1389 struct TracingAllowGate;
1390 impl Gate for TracingAllowGate {
1391 fn check(&self, _: &GateRequest) -> Result<GateDecision, GateError> {
1392 Ok(GateDecision::allow())
1393 }
1394 fn impl_name(&self) -> &'static str {
1395 "TracingAllowGate"
1396 }
1397 }
1398
1399 let events = capture_dispatch_events(async {
1400 let mut builder = VerbRegistryBuilder::new();
1401 builder.register(AlphaPack);
1402 builder.with_gate(Arc::new(TracingAllowGate));
1403 builder.with_default_namespace("tenant-default");
1404 let reg = builder.build().expect("registry builds");
1405 reg.dispatch("list", serde_json::json!({"namespace": "tenant-q"}))
1406 .await
1407 .unwrap();
1408 });
1409
1410 let gate_events = gate_check_events_for(&events, "TracingAllowGate");
1411 assert_eq!(
1412 gate_events.len(),
1413 1,
1414 "exactly one gate.check tracing event per dispatch (allow); got {gate_events:?}"
1415 );
1416 let payload = gate_events[0]
1417 .audit_event
1418 .as_ref()
1419 .expect("gate.check event must carry an audit_event field");
1420 let audit: khive_gate::AuditEvent =
1421 serde_json::from_str(payload).expect("audit_event payload must decode to AuditEvent");
1422 assert_eq!(audit.decision, AuditDecision::Allow);
1423 assert_eq!(audit.verb, "list");
1424 assert_eq!(audit.namespace, "tenant-q");
1425 assert_eq!(audit.gate_impl, "TracingAllowGate");
1426 assert!(
1427 audit.deny_reason.is_none(),
1428 "deny_reason must be None on Allow"
1429 );
1430 }
1431
1432 use async_trait::async_trait;
1435 use khive_storage::{
1436 BatchWriteSummary, Event, EventFilter, EventStore, Page, PageRequest, SubstrateKind,
1437 };
1438 use khive_types::EventOutcome;
1439
1440 #[derive(Default, Debug)]
1442 struct MemoryEventStore {
1443 events: std::sync::Mutex<Vec<Event>>,
1444 }
1445
1446 #[async_trait]
1447 impl EventStore for MemoryEventStore {
1448 async fn append_event(&self, event: Event) -> khive_storage::StorageResult<()> {
1449 self.events.lock().unwrap().push(event);
1450 Ok(())
1451 }
1452 async fn append_events(
1453 &self,
1454 events: Vec<Event>,
1455 ) -> khive_storage::StorageResult<BatchWriteSummary> {
1456 let attempted = events.len() as u64;
1457 let affected = attempted;
1458 self.events.lock().unwrap().extend(events);
1459 Ok(BatchWriteSummary {
1460 attempted,
1461 affected,
1462 failed: 0,
1463 first_error: String::new(),
1464 })
1465 }
1466 async fn get_event(&self, id: uuid::Uuid) -> khive_storage::StorageResult<Option<Event>> {
1467 Ok(self
1468 .events
1469 .lock()
1470 .unwrap()
1471 .iter()
1472 .find(|e| e.id == id)
1473 .cloned())
1474 }
1475 async fn query_events(
1476 &self,
1477 _filter: EventFilter,
1478 _page: PageRequest,
1479 ) -> khive_storage::StorageResult<Page<Event>> {
1480 let items = self.events.lock().unwrap().clone();
1481 let total = items.len() as u64;
1482 Ok(Page {
1483 items,
1484 total: Some(total),
1485 })
1486 }
1487 async fn count_events(&self, _filter: EventFilter) -> khive_storage::StorageResult<u64> {
1488 Ok(self.events.lock().unwrap().len() as u64)
1489 }
1490 }
1491
1492 #[tokio::test]
1493 async fn allow_all_gate_default_remains_backward_compatible() {
1494 let mut builder = VerbRegistryBuilder::new();
1496 builder.register(AlphaPack);
1497 let reg = builder.build().expect("registry builds");
1498
1499 let res = reg.dispatch("list", Value::Null).await.unwrap();
1500 assert_eq!(
1501 res["pack"], "alpha",
1502 "AllowAllGate must allow every verb — backward compat guarantee"
1503 );
1504 let res = reg.dispatch("create", Value::Null).await.unwrap();
1505 assert_eq!(res["pack"], "alpha");
1506 }
1507
1508 #[tokio::test]
1509 async fn deny_gate_returns_permission_denied_pack_never_invoked() {
1510 #[derive(Debug)]
1511 struct AlwaysDenyGate;
1512 impl Gate for AlwaysDenyGate {
1513 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
1514 Ok(GateDecision::deny("test: always deny"))
1515 }
1516 }
1517
1518 #[derive(Debug)]
1520 struct TrackedPack {
1521 invoked: Arc<AtomicUsize>,
1522 }
1523
1524 impl khive_types::Pack for TrackedPack {
1525 const NAME: &'static str = "tracked";
1526 const NOTE_KINDS: &'static [&'static str] = &[];
1527 const ENTITY_KINDS: &'static [&'static str] = &[];
1528 const VERBS: &'static [VerbDef] = &[VerbDef {
1529 name: "guarded",
1530 description: "a guarded verb",
1531 }];
1532 }
1533
1534 #[async_trait]
1535 impl PackRuntime for TrackedPack {
1536 fn name(&self) -> &str {
1537 Self::NAME
1538 }
1539 fn note_kinds(&self) -> &'static [&'static str] {
1540 Self::NOTE_KINDS
1541 }
1542 fn entity_kinds(&self) -> &'static [&'static str] {
1543 Self::ENTITY_KINDS
1544 }
1545 fn verbs(&self) -> &'static [VerbDef] {
1546 Self::VERBS
1547 }
1548 async fn dispatch(
1549 &self,
1550 _verb: &str,
1551 _params: Value,
1552 _registry: &VerbRegistry,
1553 ) -> Result<Value, RuntimeError> {
1554 self.invoked.fetch_add(1, Ordering::SeqCst);
1555 Ok(serde_json::json!({"invoked": true}))
1556 }
1557 }
1558
1559 let invoked = Arc::new(AtomicUsize::new(0));
1560 let mut builder = VerbRegistryBuilder::new();
1561 builder.register(TrackedPack {
1562 invoked: invoked.clone(),
1563 });
1564 builder.with_gate(Arc::new(AlwaysDenyGate));
1565 let reg = builder.build().expect("registry builds");
1566
1567 let err = reg.dispatch("guarded", Value::Null).await.unwrap_err();
1568 assert!(
1569 matches!(err, RuntimeError::PermissionDenied { ref verb, ref reason } if verb == "guarded" && reason.contains("always deny")),
1570 "expected PermissionDenied with verb=guarded and reason, got: {err:?}"
1571 );
1572 assert_eq!(
1573 invoked.load(Ordering::SeqCst),
1574 0,
1575 "pack dispatch MUST NOT be invoked when gate denies"
1576 );
1577 }
1578
1579 #[tokio::test]
1580 async fn audit_event_persists_to_event_store_on_allow() {
1581 let store = Arc::new(MemoryEventStore::default());
1582 let mut builder = VerbRegistryBuilder::new();
1583 builder.register(AlphaPack);
1584 builder.with_event_store(store.clone());
1585 let reg = builder.build().expect("registry builds");
1586
1587 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
1588 .await
1589 .unwrap();
1590
1591 let count = store.count_events(EventFilter::default()).await.unwrap();
1592 assert_eq!(count, 1, "one audit event persisted to EventStore on allow");
1593
1594 let page = store
1595 .query_events(
1596 EventFilter::default(),
1597 PageRequest {
1598 limit: 10,
1599 offset: 0,
1600 },
1601 )
1602 .await
1603 .unwrap();
1604 let ev = &page.items[0];
1605 assert_eq!(ev.verb, "list");
1606 assert_eq!(ev.namespace, "test-ns");
1607 assert_eq!(ev.substrate, SubstrateKind::Event);
1608 assert_eq!(ev.outcome, EventOutcome::Success);
1609 }
1610
1611 #[tokio::test]
1612 async fn audit_event_persists_to_event_store_on_deny() {
1613 #[derive(Debug)]
1614 struct AlwaysDenyGate;
1615 impl Gate for AlwaysDenyGate {
1616 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
1617 Ok(GateDecision::deny("denied by test"))
1618 }
1619 }
1620
1621 let store = Arc::new(MemoryEventStore::default());
1622 let mut builder = VerbRegistryBuilder::new();
1623 builder.register(AlphaPack);
1624 builder.with_gate(Arc::new(AlwaysDenyGate));
1625 builder.with_event_store(store.clone());
1626 let reg = builder.build().expect("registry builds");
1627
1628 let err = reg
1630 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
1631 .await
1632 .unwrap_err();
1633 assert!(matches!(err, RuntimeError::PermissionDenied { .. }));
1634
1635 let count = store.count_events(EventFilter::default()).await.unwrap();
1636 assert_eq!(count, 1, "one audit event persisted to EventStore on deny");
1637
1638 let page = store
1639 .query_events(
1640 EventFilter::default(),
1641 PageRequest {
1642 limit: 10,
1643 offset: 0,
1644 },
1645 )
1646 .await
1647 .unwrap();
1648 let ev = &page.items[0];
1649 assert_eq!(ev.verb, "list");
1650 assert_eq!(ev.outcome, EventOutcome::Denied);
1651 }
1652
1653 #[tokio::test]
1654 async fn gate_error_does_not_persist_to_event_store() {
1655 #[derive(Debug)]
1656 struct FailingGate;
1657 impl Gate for FailingGate {
1658 fn check(&self, _req: &GateRequest) -> Result<GateDecision, khive_gate::GateError> {
1659 Err(khive_gate::GateError::Internal("gate broken".into()))
1660 }
1661 }
1662
1663 let store = Arc::new(MemoryEventStore::default());
1664 let mut builder = VerbRegistryBuilder::new();
1665 builder.register(AlphaPack);
1666 builder.with_gate(Arc::new(FailingGate));
1667 builder.with_event_store(store.clone());
1668 let reg = builder.build().expect("registry builds");
1669
1670 let res = reg.dispatch("list", Value::Null).await.unwrap();
1672 assert_eq!(
1673 res["pack"], "alpha",
1674 "gate error must fail-open, not block dispatch"
1675 );
1676
1677 let count = store.count_events(EventFilter::default()).await.unwrap();
1678 assert_eq!(
1679 count, 0,
1680 "gate infrastructure error must NOT produce an audit event in EventStore"
1681 );
1682 }
1683
1684 #[tokio::test]
1685 async fn no_event_store_configured_tracing_only() {
1686 let mut builder = VerbRegistryBuilder::new();
1690 builder.register(AlphaPack);
1691 let reg = builder.build().expect("registry builds");
1692
1693 let res = reg.dispatch("list", Value::Null).await.unwrap();
1694 assert_eq!(res["pack"], "alpha");
1695 }
1696
1697 #[test]
1698 #[serial]
1699 fn dispatch_tracing_emits_gate_check_event_with_deny_payload() {
1700 #[derive(Debug)]
1701 struct TracingDenyGate;
1702 impl Gate for TracingDenyGate {
1703 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
1704 Ok(GateDecision::deny("denied by test gate"))
1705 }
1706 fn impl_name(&self) -> &'static str {
1707 "TracingDenyGate"
1708 }
1709 }
1710
1711 let events = capture_dispatch_events(async {
1712 let mut builder = VerbRegistryBuilder::new();
1713 builder.register(AlphaPack);
1714 builder.with_gate(Arc::new(TracingDenyGate));
1715 let reg = builder.build().expect("registry builds");
1716 let _ = reg.dispatch("create", serde_json::Value::Null).await;
1719 });
1720
1721 let gate_events = gate_check_events_for(&events, "TracingDenyGate");
1722 assert_eq!(
1723 gate_events.len(),
1724 1,
1725 "exactly one gate.check tracing event per dispatch (deny); got {gate_events:?}"
1726 );
1727 let payload = gate_events[0]
1728 .audit_event
1729 .as_ref()
1730 .expect("gate.check event must carry an audit_event field on Deny");
1731 let audit: khive_gate::AuditEvent =
1732 serde_json::from_str(payload).expect("audit_event payload must decode to AuditEvent");
1733 assert_eq!(audit.decision, AuditDecision::Deny);
1734 assert_eq!(audit.deny_reason.as_deref(), Some("denied by test gate"));
1735 assert_eq!(audit.gate_impl, "TracingDenyGate");
1736 let payload_json: serde_json::Value =
1740 serde_json::from_str(payload).expect("payload must be valid JSON");
1741 assert_eq!(
1742 payload_json["obligations"],
1743 serde_json::Value::Array(Vec::new()),
1744 "obligations must be `[]` on Deny on the tracing payload, not omitted"
1745 );
1746 }
1747
1748 #[tokio::test]
1756 async fn audit_envelope_round_trips_deny_reason_and_gate_impl_through_event_store() {
1757 #[derive(Debug)]
1758 struct DenyGateWithName;
1759 impl Gate for DenyGateWithName {
1760 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
1761 Ok(GateDecision::deny("policy: write forbidden for anon"))
1762 }
1763 fn impl_name(&self) -> &'static str {
1764 "DenyGateWithName"
1765 }
1766 }
1767
1768 let store = Arc::new(MemoryEventStore::default());
1769 let mut builder = VerbRegistryBuilder::new();
1770 builder.register(AlphaPack);
1771 builder.with_gate(Arc::new(DenyGateWithName));
1772 builder.with_event_store(store.clone());
1773 let reg = builder.build().expect("registry builds");
1774
1775 let err = reg
1777 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
1778 .await
1779 .unwrap_err();
1780 assert!(
1781 matches!(err, RuntimeError::PermissionDenied { .. }),
1782 "expected PermissionDenied, got {err:?}"
1783 );
1784
1785 let page = store
1787 .query_events(
1788 EventFilter::default(),
1789 PageRequest {
1790 limit: 10,
1791 offset: 0,
1792 },
1793 )
1794 .await
1795 .unwrap();
1796 assert_eq!(
1797 page.items.len(),
1798 1,
1799 "one audit event must be persisted on deny"
1800 );
1801
1802 let ev = &page.items[0];
1803 assert_eq!(ev.outcome, EventOutcome::Denied);
1804
1805 let data = ev
1807 .data
1808 .as_ref()
1809 .expect("Event.data must be Some — full AuditEvent envelope must be persisted");
1810
1811 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
1812 .expect("Event.data must deserialize to AuditEvent");
1813
1814 assert_eq!(
1815 audit.deny_reason.as_deref(),
1816 Some("policy: write forbidden for anon"),
1817 "deny_reason must be preserved through EventStore"
1818 );
1819 assert_eq!(
1820 audit.gate_impl, "DenyGateWithName",
1821 "gate_impl must be preserved through EventStore"
1822 );
1823 assert_eq!(
1824 audit.decision,
1825 khive_gate::AuditDecision::Deny,
1826 "decision field must be preserved through EventStore"
1827 );
1828 }
1829
1830 #[tokio::test]
1831 async fn audit_envelope_round_trips_obligations_through_event_store() {
1832 use khive_gate::Obligation;
1833
1834 #[derive(Debug)]
1835 struct ObligationGate;
1836 impl Gate for ObligationGate {
1837 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
1838 Ok(GateDecision::allow_with(vec![Obligation::Audit {
1839 tag: "billing.meter".into(),
1840 }]))
1841 }
1842 fn impl_name(&self) -> &'static str {
1843 "ObligationGate"
1844 }
1845 }
1846
1847 let store = Arc::new(MemoryEventStore::default());
1848 let mut builder = VerbRegistryBuilder::new();
1849 builder.register(AlphaPack);
1850 builder.with_gate(Arc::new(ObligationGate));
1851 builder.with_event_store(store.clone());
1852 let reg = builder.build().expect("registry builds");
1853
1854 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
1855 .await
1856 .unwrap();
1857
1858 let page = store
1859 .query_events(
1860 EventFilter::default(),
1861 PageRequest {
1862 limit: 10,
1863 offset: 0,
1864 },
1865 )
1866 .await
1867 .unwrap();
1868 assert_eq!(page.items.len(), 1);
1869
1870 let ev = &page.items[0];
1871 assert_eq!(ev.outcome, EventOutcome::Success);
1872
1873 let data = ev
1874 .data
1875 .as_ref()
1876 .expect("Event.data must be Some — AuditEvent envelope must be persisted on allow");
1877
1878 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
1879 .expect("Event.data must deserialize to AuditEvent");
1880
1881 assert_eq!(audit.gate_impl, "ObligationGate");
1882 assert_eq!(
1883 audit.obligations.len(),
1884 1,
1885 "obligations must be preserved through EventStore"
1886 );
1887 match &audit.obligations[0] {
1888 Obligation::Audit { tag } => assert_eq!(tag, "billing.meter"),
1889 other => panic!("expected Audit obligation, got {other:?}"),
1890 }
1891 }
1892
1893 #[tokio::test]
1901 async fn sql_backed_audit_envelope_round_trips_deny_reason_gate_impl_and_obligations() {
1902 #[derive(Debug)]
1903 struct SqlTestDenyGate;
1904 impl Gate for SqlTestDenyGate {
1905 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
1906 Ok(GateDecision::deny("sql-path: write denied"))
1907 }
1908 fn impl_name(&self) -> &'static str {
1909 "SqlTestDenyGate"
1910 }
1911 }
1912
1913 let rt = KhiveRuntime::memory().expect("in-memory runtime");
1917 let sql_store = rt
1918 .events(Some("test-ns"))
1919 .expect("events_for_namespace must succeed");
1920
1921 let mut builder = VerbRegistryBuilder::new();
1922 builder.register(AlphaPack);
1923 builder.with_gate(Arc::new(SqlTestDenyGate));
1924 builder.with_event_store(sql_store.clone());
1925 let reg = builder.build().expect("registry builds");
1926
1927 let err = reg
1929 .dispatch("list", serde_json::json!({"namespace": "test-ns"}))
1930 .await
1931 .unwrap_err();
1932 assert!(
1933 matches!(err, RuntimeError::PermissionDenied { .. }),
1934 "expected PermissionDenied, got {err:?}"
1935 );
1936
1937 let page = sql_store
1939 .query_events(
1940 EventFilter::default(),
1941 PageRequest {
1942 limit: 10,
1943 offset: 0,
1944 },
1945 )
1946 .await
1947 .unwrap();
1948 assert_eq!(
1949 page.items.len(),
1950 1,
1951 "one audit event must be persisted on deny through SqlEventStore"
1952 );
1953
1954 let ev = &page.items[0];
1955 assert_eq!(ev.outcome, EventOutcome::Denied);
1956
1957 let data = ev
1961 .data
1962 .as_ref()
1963 .expect("Event.data must be Some — SqlEventStore must persist AuditEvent envelope");
1964
1965 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
1966 .expect("Event.data must deserialize to AuditEvent after SQL round-trip");
1967
1968 assert_eq!(
1969 audit.deny_reason.as_deref(),
1970 Some("sql-path: write denied"),
1971 "deny_reason must survive the SQL text round-trip"
1972 );
1973 assert_eq!(
1974 audit.gate_impl, "SqlTestDenyGate",
1975 "gate_impl must survive the SQL text round-trip"
1976 );
1977 assert_eq!(
1978 audit.decision,
1979 khive_gate::AuditDecision::Deny,
1980 "decision field must survive the SQL text round-trip"
1981 );
1982 assert!(
1985 audit.obligations.is_empty(),
1986 "obligations must be preserved as empty [] through SQL round-trip"
1987 );
1988 }
1989
1990 #[tokio::test]
2002 async fn sql_backed_audit_envelope_round_trips_non_empty_obligations() {
2003 use khive_gate::Obligation;
2004
2005 #[derive(Debug)]
2006 struct SqlTestAllowWithObligationGate;
2007 impl Gate for SqlTestAllowWithObligationGate {
2008 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2009 Ok(GateDecision::allow_with(vec![Obligation::Audit {
2010 tag: "sql-path-billing.meter".into(),
2011 }]))
2012 }
2013 fn impl_name(&self) -> &'static str {
2014 "SqlTestAllowWithObligationGate"
2015 }
2016 }
2017
2018 let rt = KhiveRuntime::memory().expect("in-memory runtime");
2019 let sql_store = rt
2020 .events(Some("test-ns"))
2021 .expect("events_for_namespace must succeed");
2022
2023 let mut builder = VerbRegistryBuilder::new();
2024 builder.register(AlphaPack);
2025 builder.with_gate(Arc::new(SqlTestAllowWithObligationGate));
2026 builder.with_event_store(sql_store.clone());
2027 let reg = builder.build().expect("registry builds");
2028
2029 reg.dispatch("list", serde_json::json!({"namespace": "test-ns"}))
2031 .await
2032 .expect("dispatch must succeed when gate allows");
2033
2034 let page = sql_store
2036 .query_events(
2037 EventFilter::default(),
2038 PageRequest {
2039 limit: 10,
2040 offset: 0,
2041 },
2042 )
2043 .await
2044 .unwrap();
2045 assert_eq!(
2046 page.items.len(),
2047 1,
2048 "one audit event must be persisted on allow through SqlEventStore"
2049 );
2050
2051 let ev = &page.items[0];
2052 assert_eq!(ev.outcome, EventOutcome::Success);
2053
2054 let data = ev
2055 .data
2056 .as_ref()
2057 .expect("Event.data must be Some — SqlEventStore must persist AuditEvent envelope");
2058
2059 let obligations_raw = data
2064 .get("obligations")
2065 .expect("Event.data JSON must contain 'obligations' key");
2066 let obligations_arr = obligations_raw
2067 .as_array()
2068 .expect("'obligations' must be a JSON array");
2069 assert!(
2070 !obligations_arr.is_empty(),
2071 "raw Event.data['obligations'] must be non-empty after SQL round-trip"
2072 );
2073
2074 let audit: khive_gate::AuditEvent = serde_json::from_value(data.clone())
2077 .expect("Event.data must deserialize to AuditEvent after SQL round-trip");
2078
2079 assert_eq!(
2080 audit.gate_impl, "SqlTestAllowWithObligationGate",
2081 "gate_impl must survive the SQL text round-trip"
2082 );
2083 assert_eq!(
2084 audit.decision,
2085 khive_gate::AuditDecision::Allow,
2086 "decision field must survive the SQL text round-trip"
2087 );
2088 assert_eq!(
2089 audit.obligations.len(),
2090 1,
2091 "obligations must be non-empty after SQL round-trip (not silently defaulted to [])"
2092 );
2093 match &audit.obligations[0] {
2094 Obligation::Audit { tag } => assert_eq!(
2095 tag, "sql-path-billing.meter",
2096 "Audit obligation tag must survive the SQL text round-trip"
2097 ),
2098 other => panic!("expected Audit obligation, got {other:?}"),
2099 }
2100 }
2101
2102 #[tokio::test]
2110 async fn audit_event_payload_shape_for_create_verb_matches_adr035_envelope() {
2111 let store = Arc::new(MemoryEventStore::default());
2112 let mut builder = VerbRegistryBuilder::new();
2113 builder.register(AlphaPack);
2114 builder.with_event_store(store.clone());
2115 builder.with_default_namespace("test-ns");
2116 let reg = builder.build().expect("registry builds");
2117
2118 reg.dispatch("create", serde_json::json!({"namespace": "test-ns"}))
2121 .await
2122 .unwrap();
2123
2124 let count = store.count_events(EventFilter::default()).await.unwrap();
2125 assert_eq!(count, 1, "exactly one audit event for one dispatch");
2126
2127 let page = store
2128 .query_events(
2129 EventFilter::default(),
2130 PageRequest {
2131 limit: 10,
2132 offset: 0,
2133 },
2134 )
2135 .await
2136 .unwrap();
2137 let ev = &page.items[0];
2138
2139 assert_eq!(ev.verb, "create", "ev.verb must be the dispatched verb");
2141 assert_eq!(
2142 ev.outcome,
2143 EventOutcome::Success,
2144 "ev.outcome must be Success on allow"
2145 );
2146 assert_eq!(
2147 ev.namespace, "test-ns",
2148 "ev.namespace must match the dispatch namespace"
2149 );
2150
2151 let data = ev
2153 .data
2154 .as_ref()
2155 .expect("ev.data must be Some — full AuditEvent envelope required by ADR-035");
2156
2157 let audit: khive_gate::AuditEvent =
2158 serde_json::from_value(data.clone()).expect("ev.data must deserialize to AuditEvent");
2159
2160 assert_eq!(
2161 audit.decision,
2162 khive_gate::AuditDecision::Allow,
2163 "AuditEvent.decision must be Allow"
2164 );
2165 assert_eq!(audit.verb, "create", "AuditEvent.verb must be 'create'");
2166 assert_eq!(
2167 audit.namespace, "test-ns",
2168 "AuditEvent.namespace must be preserved"
2169 );
2170 assert_eq!(
2171 audit.gate_impl, "AllowAllGate",
2172 "AuditEvent.gate_impl must name the gate implementation"
2173 );
2174 assert!(
2175 audit.deny_reason.is_none(),
2176 "AuditEvent.deny_reason must be None on Allow"
2177 );
2178 let payload_json: serde_json::Value =
2180 serde_json::from_value(data.clone()).expect("data must be valid JSON");
2181 assert_eq!(
2182 payload_json["obligations"],
2183 serde_json::Value::Array(Vec::new()),
2184 "obligations must be [] on AllowAllGate (wire-shape rule ADR-033)"
2185 );
2186 }
2187}
2188
2189#[cfg(test)]
2192mod dep_tests {
2193 use super::*;
2194 use async_trait::async_trait;
2195 use khive_types::Pack;
2196 use serde_json::Value;
2197
2198 struct KgDepPack;
2199 struct MemoryDepPack;
2200 struct ADepPack;
2201 struct BDepPack;
2202
2203 impl Pack for KgDepPack {
2204 const NAME: &'static str = "kg_dep";
2205 const NOTE_KINDS: &'static [&'static str] = &["observation"];
2206 const ENTITY_KINDS: &'static [&'static str] = &["concept"];
2207 const VERBS: &'static [VerbDef] = &[];
2208 }
2209
2210 impl Pack for MemoryDepPack {
2211 const NAME: &'static str = "memory_dep";
2212 const NOTE_KINDS: &'static [&'static str] = &["memory"];
2213 const ENTITY_KINDS: &'static [&'static str] = &[];
2214 const VERBS: &'static [VerbDef] = &[];
2215 const REQUIRES: &'static [&'static str] = &["kg_dep"];
2216 }
2217
2218 impl Pack for ADepPack {
2219 const NAME: &'static str = "pack_a";
2220 const NOTE_KINDS: &'static [&'static str] = &[];
2221 const ENTITY_KINDS: &'static [&'static str] = &[];
2222 const VERBS: &'static [VerbDef] = &[];
2223 const REQUIRES: &'static [&'static str] = &["pack_b"];
2224 }
2225
2226 impl Pack for BDepPack {
2227 const NAME: &'static str = "pack_b";
2228 const NOTE_KINDS: &'static [&'static str] = &[];
2229 const ENTITY_KINDS: &'static [&'static str] = &[];
2230 const VERBS: &'static [VerbDef] = &[];
2231 const REQUIRES: &'static [&'static str] = &["pack_a"];
2232 }
2233
2234 #[async_trait]
2235 impl PackRuntime for KgDepPack {
2236 fn name(&self) -> &str {
2237 Self::NAME
2238 }
2239 fn note_kinds(&self) -> &'static [&'static str] {
2240 Self::NOTE_KINDS
2241 }
2242 fn entity_kinds(&self) -> &'static [&'static str] {
2243 Self::ENTITY_KINDS
2244 }
2245 fn verbs(&self) -> &'static [VerbDef] {
2246 Self::VERBS
2247 }
2248 async fn dispatch(
2249 &self,
2250 verb: &str,
2251 _: Value,
2252 _: &VerbRegistry,
2253 ) -> Result<Value, RuntimeError> {
2254 Err(RuntimeError::InvalidInput(format!(
2255 "KgDepPack has no verbs: {verb}"
2256 )))
2257 }
2258 }
2259
2260 #[async_trait]
2261 impl PackRuntime for MemoryDepPack {
2262 fn name(&self) -> &str {
2263 Self::NAME
2264 }
2265 fn note_kinds(&self) -> &'static [&'static str] {
2266 Self::NOTE_KINDS
2267 }
2268 fn entity_kinds(&self) -> &'static [&'static str] {
2269 Self::ENTITY_KINDS
2270 }
2271 fn verbs(&self) -> &'static [VerbDef] {
2272 Self::VERBS
2273 }
2274 fn requires(&self) -> &'static [&'static str] {
2275 Self::REQUIRES
2276 }
2277 async fn dispatch(
2278 &self,
2279 verb: &str,
2280 _: Value,
2281 _: &VerbRegistry,
2282 ) -> Result<Value, RuntimeError> {
2283 Err(RuntimeError::InvalidInput(format!(
2284 "MemoryDepPack has no verbs: {verb}"
2285 )))
2286 }
2287 }
2288
2289 #[async_trait]
2290 impl PackRuntime for ADepPack {
2291 fn name(&self) -> &str {
2292 Self::NAME
2293 }
2294 fn note_kinds(&self) -> &'static [&'static str] {
2295 Self::NOTE_KINDS
2296 }
2297 fn entity_kinds(&self) -> &'static [&'static str] {
2298 Self::ENTITY_KINDS
2299 }
2300 fn verbs(&self) -> &'static [VerbDef] {
2301 Self::VERBS
2302 }
2303 fn requires(&self) -> &'static [&'static str] {
2304 Self::REQUIRES
2305 }
2306 async fn dispatch(
2307 &self,
2308 verb: &str,
2309 _: Value,
2310 _: &VerbRegistry,
2311 ) -> Result<Value, RuntimeError> {
2312 Err(RuntimeError::InvalidInput(format!(
2313 "ADepPack has no verbs: {verb}"
2314 )))
2315 }
2316 }
2317
2318 #[async_trait]
2319 impl PackRuntime for BDepPack {
2320 fn name(&self) -> &str {
2321 Self::NAME
2322 }
2323 fn note_kinds(&self) -> &'static [&'static str] {
2324 Self::NOTE_KINDS
2325 }
2326 fn entity_kinds(&self) -> &'static [&'static str] {
2327 Self::ENTITY_KINDS
2328 }
2329 fn verbs(&self) -> &'static [VerbDef] {
2330 Self::VERBS
2331 }
2332 fn requires(&self) -> &'static [&'static str] {
2333 Self::REQUIRES
2334 }
2335 async fn dispatch(
2336 &self,
2337 verb: &str,
2338 _: Value,
2339 _: &VerbRegistry,
2340 ) -> Result<Value, RuntimeError> {
2341 Err(RuntimeError::InvalidInput(format!(
2342 "BDepPack has no verbs: {verb}"
2343 )))
2344 }
2345 }
2346
2347 #[test]
2348 fn test_pack_deps_happy_path() {
2349 let mut builder = VerbRegistryBuilder::new();
2350 builder.register(MemoryDepPack);
2351 builder.register(KgDepPack);
2352 let reg = builder
2353 .build()
2354 .expect("kg_dep satisfies memory_dep dependency");
2355 assert_eq!(reg.pack_requires("memory_dep").unwrap(), &["kg_dep"]);
2356 let names = reg.pack_names();
2357 let kg_pos = names.iter().position(|&n| n == "kg_dep").unwrap();
2358 let mem_pos = names.iter().position(|&n| n == "memory_dep").unwrap();
2359 assert!(
2360 kg_pos < mem_pos,
2361 "kg_dep must be loaded before memory_dep; order: {names:?}"
2362 );
2363 }
2364
2365 #[test]
2366 fn test_pack_deps_missing() {
2367 let mut builder = VerbRegistryBuilder::new();
2368 builder.register(MemoryDepPack);
2369 let err = match builder.build() {
2370 Ok(_) => panic!("expected Err, got Ok"),
2371 Err(e) => e,
2372 };
2373 assert!(
2374 matches!(err, RuntimeError::MissingPackDependency(_)),
2375 "expected MissingPackDependency, got {err:?}"
2376 );
2377 let msg = err.to_string();
2378 assert!(
2379 msg.contains("memory_dep"),
2380 "error must name the dependent pack: {msg}"
2381 );
2382 assert!(
2383 msg.contains("kg_dep"),
2384 "error must name the missing dep: {msg}"
2385 );
2386 }
2387
2388 #[test]
2389 fn test_pack_deps_circular() {
2390 let mut builder = VerbRegistryBuilder::new();
2391 builder.register(ADepPack);
2392 builder.register(BDepPack);
2393 let err = match builder.build() {
2394 Ok(_) => panic!("expected Err, got Ok"),
2395 Err(e) => e,
2396 };
2397 assert!(
2398 matches!(err, RuntimeError::CircularPackDependency(_)),
2399 "expected CircularPackDependency, got {err:?}"
2400 );
2401 let msg = err.to_string();
2402 assert!(msg.contains("pack_a"), "error must name pack_a: {msg}");
2403 assert!(msg.contains("pack_b"), "error must name pack_b: {msg}");
2404 }
2405
2406 #[test]
2407 fn test_pack_deps_no_deps() {
2408 struct NoDepsA;
2409 struct NoDepsB;
2410
2411 impl Pack for NoDepsA {
2412 const NAME: &'static str = "no_deps_a";
2413 const NOTE_KINDS: &'static [&'static str] = &[];
2414 const ENTITY_KINDS: &'static [&'static str] = &[];
2415 const VERBS: &'static [VerbDef] = &[];
2416 }
2417
2418 impl Pack for NoDepsB {
2419 const NAME: &'static str = "no_deps_b";
2420 const NOTE_KINDS: &'static [&'static str] = &[];
2421 const ENTITY_KINDS: &'static [&'static str] = &[];
2422 const VERBS: &'static [VerbDef] = &[];
2423 }
2424
2425 #[async_trait]
2426 impl PackRuntime for NoDepsA {
2427 fn name(&self) -> &str {
2428 Self::NAME
2429 }
2430 fn note_kinds(&self) -> &'static [&'static str] {
2431 Self::NOTE_KINDS
2432 }
2433 fn entity_kinds(&self) -> &'static [&'static str] {
2434 Self::ENTITY_KINDS
2435 }
2436 fn verbs(&self) -> &'static [VerbDef] {
2437 Self::VERBS
2438 }
2439 async fn dispatch(
2440 &self,
2441 verb: &str,
2442 _: Value,
2443 _: &VerbRegistry,
2444 ) -> Result<Value, RuntimeError> {
2445 Err(RuntimeError::InvalidInput(format!("NoDepsA: {verb}")))
2446 }
2447 }
2448
2449 #[async_trait]
2450 impl PackRuntime for NoDepsB {
2451 fn name(&self) -> &str {
2452 Self::NAME
2453 }
2454 fn note_kinds(&self) -> &'static [&'static str] {
2455 Self::NOTE_KINDS
2456 }
2457 fn entity_kinds(&self) -> &'static [&'static str] {
2458 Self::ENTITY_KINDS
2459 }
2460 fn verbs(&self) -> &'static [VerbDef] {
2461 Self::VERBS
2462 }
2463 async fn dispatch(
2464 &self,
2465 verb: &str,
2466 _: Value,
2467 _: &VerbRegistry,
2468 ) -> Result<Value, RuntimeError> {
2469 Err(RuntimeError::InvalidInput(format!("NoDepsB: {verb}")))
2470 }
2471 }
2472
2473 let mut builder = VerbRegistryBuilder::new();
2474 builder.register(NoDepsA);
2475 builder.register(NoDepsB);
2476 let reg = builder.build().expect("packs with REQUIRES=&[] build");
2477 assert_eq!(reg.pack_requires("no_deps_a").unwrap(), &[] as &[&str]);
2478 assert_eq!(reg.pack_requires("no_deps_b").unwrap(), &[] as &[&str]);
2479 }
2480}
2481
2482#[cfg(test)]
2485mod hook_tests {
2486 use super::*;
2487 use async_trait::async_trait;
2488 use khive_types::Pack;
2489 use std::sync::atomic::{AtomicUsize, Ordering};
2490 use std::sync::Mutex as StdMutex;
2491
2492 struct SimplePack;
2493
2494 impl Pack for SimplePack {
2495 const NAME: &'static str = "simple";
2496 const NOTE_KINDS: &'static [&'static str] = &[];
2497 const ENTITY_KINDS: &'static [&'static str] = &[];
2498 const VERBS: &'static [VerbDef] = &[VerbDef {
2499 name: "ping",
2500 description: "ping",
2501 }];
2502 }
2503
2504 #[async_trait]
2505 impl PackRuntime for SimplePack {
2506 fn name(&self) -> &str {
2507 SimplePack::NAME
2508 }
2509 fn note_kinds(&self) -> &'static [&'static str] {
2510 SimplePack::NOTE_KINDS
2511 }
2512 fn entity_kinds(&self) -> &'static [&'static str] {
2513 SimplePack::ENTITY_KINDS
2514 }
2515 fn verbs(&self) -> &'static [VerbDef] {
2516 SimplePack::VERBS
2517 }
2518 async fn dispatch(
2519 &self,
2520 verb: &str,
2521 _params: Value,
2522 _registry: &VerbRegistry,
2523 ) -> Result<Value, RuntimeError> {
2524 Ok(serde_json::json!({ "verb": verb }))
2525 }
2526 }
2527
2528 #[derive(Default)]
2530 struct CountingHook {
2531 calls: AtomicUsize,
2532 last_verb: StdMutex<String>,
2533 }
2534
2535 #[async_trait]
2536 impl DispatchHook for CountingHook {
2537 async fn on_dispatch(&self, event: &Event) {
2538 self.calls.fetch_add(1, Ordering::SeqCst);
2539 *self.last_verb.lock().unwrap() = event.verb.clone();
2540 }
2541 }
2542
2543 #[tokio::test]
2544 async fn dispatch_hook_fires_on_successful_dispatch() {
2545 let hook = Arc::new(CountingHook::default());
2546 let mut builder = VerbRegistryBuilder::new();
2547 builder.register(SimplePack);
2548 builder.with_dispatch_hook(hook.clone());
2549 let reg = builder.build().expect("registry builds");
2550
2551 reg.dispatch("ping", Value::Null).await.unwrap();
2552
2553 assert_eq!(
2554 hook.calls.load(Ordering::SeqCst),
2555 1,
2556 "hook must fire once per successful dispatch"
2557 );
2558 assert_eq!(
2559 hook.last_verb.lock().unwrap().as_str(),
2560 "ping",
2561 "hook event must carry the dispatched verb"
2562 );
2563 }
2564
2565 #[tokio::test]
2566 async fn dispatch_hook_fires_multiple_times() {
2567 let hook = Arc::new(CountingHook::default());
2568 let mut builder = VerbRegistryBuilder::new();
2569 builder.register(SimplePack);
2570 builder.with_dispatch_hook(hook.clone());
2571 let reg = builder.build().expect("registry builds");
2572
2573 reg.dispatch("ping", Value::Null).await.unwrap();
2574 reg.dispatch("ping", Value::Null).await.unwrap();
2575 reg.dispatch("ping", Value::Null).await.unwrap();
2576
2577 assert_eq!(
2578 hook.calls.load(Ordering::SeqCst),
2579 3,
2580 "hook must fire once per successful dispatch"
2581 );
2582 }
2583
2584 #[tokio::test]
2585 async fn dispatch_hook_does_not_fire_on_unknown_verb() {
2586 let hook = Arc::new(CountingHook::default());
2587 let mut builder = VerbRegistryBuilder::new();
2588 builder.register(SimplePack);
2589 builder.with_dispatch_hook(hook.clone());
2590 let reg = builder.build().expect("registry builds");
2591
2592 let _ = reg.dispatch("nonexistent", Value::Null).await;
2593
2594 assert_eq!(
2595 hook.calls.load(Ordering::SeqCst),
2596 0,
2597 "hook must NOT fire for unknown verb (dispatch returns error)"
2598 );
2599 }
2600
2601 #[tokio::test]
2602 async fn dispatch_hook_does_not_fire_on_gate_deny() {
2603 use khive_gate::{Gate, GateDecision, GateError};
2604
2605 #[derive(Debug)]
2606 struct AlwaysDenyGate;
2607 impl Gate for AlwaysDenyGate {
2608 fn check(&self, _req: &GateRequest) -> Result<GateDecision, GateError> {
2609 Ok(GateDecision::deny("test deny"))
2610 }
2611 }
2612
2613 let hook = Arc::new(CountingHook::default());
2614 let mut builder = VerbRegistryBuilder::new();
2615 builder.register(SimplePack);
2616 builder.with_gate(Arc::new(AlwaysDenyGate));
2617 builder.with_dispatch_hook(hook.clone());
2618 let reg = builder.build().expect("registry builds");
2619
2620 let err = reg.dispatch("ping", Value::Null).await.unwrap_err();
2621 assert!(matches!(err, RuntimeError::PermissionDenied { .. }));
2622
2623 assert_eq!(
2624 hook.calls.load(Ordering::SeqCst),
2625 0,
2626 "hook must NOT fire when gate denies dispatch"
2627 );
2628 }
2629
2630 #[tokio::test]
2631 async fn dispatch_hook_event_carries_namespace_from_params() {
2632 let hook = Arc::new(CountingHook::default());
2633
2634 #[derive(Default)]
2635 struct NsCapturingHook {
2636 ns: StdMutex<String>,
2637 }
2638
2639 #[async_trait]
2640 impl DispatchHook for NsCapturingHook {
2641 async fn on_dispatch(&self, event: &Event) {
2642 *self.ns.lock().unwrap() = event.namespace.clone();
2643 }
2644 }
2645
2646 let ns_hook = Arc::new(NsCapturingHook::default());
2647 let mut builder = VerbRegistryBuilder::new();
2648 builder.register(SimplePack);
2649 builder.with_dispatch_hook(ns_hook.clone());
2650 let reg = builder.build().expect("registry builds");
2651
2652 reg.dispatch("ping", serde_json::json!({"namespace": "tenant-abc"}))
2653 .await
2654 .unwrap();
2655
2656 assert_eq!(
2657 ns_hook.ns.lock().unwrap().as_str(),
2658 "tenant-abc",
2659 "dispatch hook event must carry the resolved namespace"
2660 );
2661
2662 drop(hook);
2664 }
2665
2666 #[tokio::test]
2667 async fn no_dispatch_hook_configured_dispatch_succeeds() {
2668 let mut builder = VerbRegistryBuilder::new();
2670 builder.register(SimplePack);
2671 let reg = builder.build().expect("registry builds");
2673
2674 let res = reg.dispatch("ping", Value::Null).await.unwrap();
2675 assert_eq!(res["verb"], "ping");
2676 }
2677}