1use std::collections::BTreeMap;
14
15use crate::abi::{ArkheError, CapabilityMask, EntityId, InstanceId, Principal, Tick, TypeCode};
16use crate::state::authz::{authorize, effective_caps};
17use crate::state::{
18 Action, ActionContext, Effect, InboundSignal, Instance, InstanceConfig, ScheduledActionId,
19 Unverified,
20};
21
22use super::apply::apply_stage;
23use super::dispatch::dispatch;
24use super::event::{KernelEvent, ObserverHandle, SignalDropReason};
25use super::observer::{KernelObserver, ObserverRegistry};
26use super::registry::ActionRegistry;
27use super::stage::{PendingSignal, StepStage};
28
29use crate::persist::{SkipReason, StepVerdict, Wal, WalWriter};
30
31pub struct Kernel {
43 instances: BTreeMap<InstanceId, Instance>,
44 action_registry: ActionRegistry,
45 observers: ObserverRegistry,
46 next_instance_id: u64,
47 manifest_digest: [u8; 32],
52 wal: Option<WalWriter>,
53 step_scratch: StepStage,
58}
59
60#[derive(Debug, Clone, Default, PartialEq, Eq)]
62pub struct StepReport {
63 pub actions_executed: u32,
65 pub effects_applied: u32,
67 pub effects_denied: u32,
69 pub observers_evicted: u32,
71 pub domain_events_emitted: u32,
73}
74
75#[derive(Debug, Clone, Default, PartialEq, Eq)]
77pub struct Stats {
78 pub instance_count: usize,
80 pub scheduled_action_count: usize,
82 pub entity_count: u32,
84 pub component_byte_count: u64,
86 pub observer_count: usize,
88 pub wal_record_count: usize,
90}
91
92impl Default for Kernel {
93 fn default() -> Self {
94 Self::new()
95 }
96}
97
98impl Kernel {
99 pub fn new() -> Self {
104 Self {
105 instances: BTreeMap::new(),
106 action_registry: ActionRegistry::new(),
107 observers: ObserverRegistry::new(),
108 next_instance_id: 0,
109 manifest_digest: [0u8; 32],
110 wal: None,
111 step_scratch: StepStage::default(),
112 }
113 }
114
115 pub fn new_with_wal(world_id: [u8; 32], manifest_digest: [u8; 32]) -> Self {
118 Self {
119 instances: BTreeMap::new(),
120 action_registry: ActionRegistry::new(),
121 observers: ObserverRegistry::new(),
122 next_instance_id: 0,
123 manifest_digest,
124 wal: Some(WalWriter::new(world_id, manifest_digest)),
125 step_scratch: StepStage::default(),
126 }
127 }
128
129 pub fn new_with_wal_signed(
134 world_id: [u8; 32],
135 manifest_digest: [u8; 32],
136 sig_class: crate::persist::SignatureClass,
137 ) -> Self {
138 Self {
139 instances: BTreeMap::new(),
140 action_registry: ActionRegistry::new(),
141 observers: ObserverRegistry::new(),
142 next_instance_id: 0,
143 manifest_digest,
144 wal: Some(WalWriter::with_signature(
145 world_id,
146 manifest_digest,
147 sig_class,
148 )),
149 step_scratch: StepStage::default(),
150 }
151 }
152
153 pub fn manifest_digest(&self) -> [u8; 32] {
157 self.manifest_digest
158 }
159
160 pub fn wal_chain_tip(&self) -> Option<[u8; 32]> {
162 self.wal.as_ref().map(|w| w.chain_tip())
163 }
164
165 pub fn wal_record_count(&self) -> Option<usize> {
167 self.wal.as_ref().map(|w| w.record_count())
168 }
169
170 pub fn export_wal(self) -> Option<Wal> {
172 self.wal.map(Wal::from_writer)
173 }
174
175 pub fn register_action<A: Action>(&mut self) {
179 self.action_registry.register::<A>();
180 }
181
182 pub fn register_observer(&mut self, obs: Box<dyn KernelObserver>) -> ObserverHandle {
186 self.observers.register(obs)
187 }
188
189 pub fn register_observer_filtered(
195 &mut self,
196 obs: Box<dyn KernelObserver>,
197 mask: super::event::EventMask,
198 ) -> ObserverHandle {
199 self.observers.register_filtered(obs, mask)
200 }
201
202 pub fn create_instance(&mut self, config: InstanceConfig) -> InstanceId {
205 self.next_instance_id = self.next_instance_id.saturating_add(1);
206 let id = InstanceId::new(self.next_instance_id).expect("instance id > 0");
207 self.instances.insert(id, Instance::new(id, config));
208 id
209 }
210
211 pub fn instances_len(&self) -> usize {
213 self.instances.len()
214 }
215
216 pub fn instance_view(&self, id: InstanceId) -> Option<super::view::InstanceView<'_>> {
220 self.instances
221 .get(&id)
222 .map(|instance| super::view::InstanceView { instance })
223 }
224
225 pub fn snapshot(&self) -> crate::persist::KernelSnapshot {
230 let instances = self
231 .instances
232 .iter()
233 .map(|(id, inst)| (*id, inst.to_snapshot()))
234 .collect();
235 crate::persist::KernelSnapshot::__construct(instances, self.next_instance_id)
236 }
237
238 pub fn from_snapshot(snap: crate::persist::KernelSnapshot) -> Self {
242 let (instances_in, next_instance_id) = snap.__into_parts();
243 let instances = instances_in
244 .into_iter()
245 .map(|(id, s)| (id, Instance::from_snapshot(s)))
246 .collect();
247 Self {
248 instances,
249 action_registry: ActionRegistry::new(),
250 observers: ObserverRegistry::new(),
251 next_instance_id,
252 manifest_digest: [0u8; 32],
253 wal: None,
254 step_scratch: StepStage::default(),
255 }
256 }
257
258 pub fn stats(&self) -> Stats {
260 let mut scheduled = 0usize;
261 let mut entities = 0u32;
262 let mut bytes = 0u64;
263 for inst in self.instances.values() {
264 scheduled = scheduled.saturating_add(inst.scheduler().len());
265 entities = entities.saturating_add(inst.ledger().total_entities());
266 bytes = bytes.saturating_add(inst.ledger().total_bytes());
267 }
268 Stats {
269 instance_count: self.instances.len(),
270 scheduled_action_count: scheduled,
271 entity_count: entities,
272 component_byte_count: bytes,
273 observer_count: self.observers.len(),
274 wal_record_count: self.wal.as_ref().map(|w| w.record_count()).unwrap_or(0),
275 }
276 }
277
278 pub fn force_unload(
285 &mut self,
286 route_id: crate::abi::RouteId,
287 caps: CapabilityMask,
288 ) -> Result<usize, ArkheError> {
289 if !caps.contains(CapabilityMask::ADMIN_UNLOAD) {
290 return Err(ArkheError::CapabilityDenied);
291 }
292
293 let mut total_live_refs: u32 = 0;
294 for inst in self.instances.values_mut() {
295 if let Some(refs) = inst.inflight_refs_mut().remove(&route_id) {
296 total_live_refs = total_live_refs.saturating_add(refs);
297 }
298 }
299
300 let event = KernelEvent::ModuleForceUnloaded {
301 route_id,
302 live_refs_at_unload: total_live_refs,
303 };
304 let _ = self.observers.deliver(&event);
305
306 Ok(total_live_refs as usize)
307 }
308
309 #[allow(clippy::too_many_arguments)]
325 pub fn submit(
326 &mut self,
327 instance: InstanceId,
328 principal: Principal,
329 actor: Option<EntityId>,
330 caps: CapabilityMask,
331 at: Tick,
332 action_type_code: TypeCode,
333 action_bytes: Vec<u8>,
334 ) -> Result<ScheduledActionId, ArkheError> {
335 let wal_bytes = if self.wal.is_some() {
338 Some(action_bytes.clone())
339 } else {
340 None
341 };
342 let id = {
343 let inst = self
344 .instances
345 .get_mut(&instance)
346 .ok_or(ArkheError::InstanceNotFound)?;
347 let max_scheduled = inst.config().max_scheduled;
351 if max_scheduled > 0 && inst.scheduler().len() >= max_scheduled as usize {
352 return Err(ArkheError::QuotaExceeded);
353 }
354 let counters = inst.id_counters_mut();
355 counters.next_scheduled = counters.next_scheduled.saturating_add(1);
356 let id = ScheduledActionId::new(counters.next_scheduled).expect("scheduled id > 0");
357 inst.scheduler_mut().schedule_with_id(
358 id,
359 at,
360 actor,
361 principal.clone(),
362 caps,
363 action_type_code,
364 action_bytes,
365 );
366 id
367 };
368 if let (Some(wal), Some(bytes)) = (self.wal.as_mut(), wal_bytes) {
369 let _ = wal.append_submit(
370 instance,
371 principal,
372 actor,
373 caps.bits(),
374 at,
375 action_type_code,
376 bytes,
377 id,
378 );
379 }
380 Ok(id)
381 }
382
383 #[allow(clippy::too_many_arguments)]
390 pub(crate) fn submit_with_id(
391 &mut self,
392 instance: InstanceId,
393 principal: Principal,
394 actor: Option<EntityId>,
395 caps: CapabilityMask,
396 id: ScheduledActionId,
397 at: Tick,
398 action_type_code: TypeCode,
399 action_bytes: Vec<u8>,
400 ) -> Result<(), ArkheError> {
401 let inst = self
402 .instances
403 .get_mut(&instance)
404 .ok_or(ArkheError::InstanceNotFound)?;
405 let counters = inst.id_counters_mut();
410 if id.get() > counters.next_scheduled {
411 counters.next_scheduled = id.get();
412 }
413 inst.scheduler_mut().schedule_with_id(
414 id,
415 at,
416 actor,
417 principal,
418 caps,
419 action_type_code,
420 action_bytes,
421 );
422 Ok(())
423 }
424
425 pub fn step(&mut self, now: Tick, caps: CapabilityMask) -> StepReport {
430 let mut report = StepReport::default();
431 let measure_digest = self.wal.is_some();
435
436 let ids: Vec<InstanceId> = self.instances.keys().copied().collect();
441 for inst_id in ids {
442 let Some(result) = self.process_instance_record(inst_id, now, caps, measure_digest)
443 else {
444 continue;
445 };
446 report.actions_executed = report.actions_executed.saturating_add(1);
447 report.effects_applied = report
448 .effects_applied
449 .saturating_add(result.effects_applied);
450 report.effects_denied = report.effects_denied.saturating_add(result.effects_denied);
451 report.domain_events_emitted = report
452 .domain_events_emitted
453 .saturating_add(result.domain_events_emitted);
454 report.observers_evicted = report
455 .observers_evicted
456 .saturating_add(result.observers_evicted);
457 if let Some(wal) = self.wal.as_mut() {
462 let _ = wal.append_step(
463 inst_id,
464 result.popped_id,
465 now,
466 caps.bits(),
467 result.verdict,
468 result.digest,
469 );
470 }
471 }
472
473 report
474 }
475
476 pub(crate) fn process_instance_record(
488 &mut self,
489 inst_id: InstanceId,
490 now: Tick,
491 session_caps: CapabilityMask,
492 measure_digest: bool,
493 ) -> Option<InstanceStepResult> {
494 let core = {
502 let Self {
503 instances,
504 action_registry,
505 step_scratch,
506 ..
507 } = self;
508 let inst = instances.get_mut(&inst_id)?;
509 step_one_core(
510 inst_id,
511 inst,
512 action_registry,
513 step_scratch,
514 now,
515 session_caps,
516 measure_digest,
517 )
518 }?;
519
520 let mut result = InstanceStepResult {
521 popped_id: core.popped_id,
522 verdict: core.verdict,
523 digest: core.digest,
524 effects_applied: core.effects_applied,
525 effects_denied: core.effects_denied,
526 domain_events_emitted: core.domain_events_emitted,
527 observers_evicted: 0,
528 };
529
530 for event in &core.events {
532 let evicted = self.observers.deliver(event);
533 result.observers_evicted = result
534 .observers_evicted
535 .saturating_add(evicted.len() as u32);
536 }
537 if matches!(
542 core.verdict,
543 StepVerdict::Committed | StepVerdict::BudgetPartial { .. }
544 ) {
545 let action_executed = KernelEvent::ActionExecuted {
546 instance: inst_id,
547 action_type: core.action_type,
548 at: now,
549 };
550 let evicted = self.observers.deliver(&action_executed);
551 result.observers_evicted = result
552 .observers_evicted
553 .saturating_add(evicted.len() as u32);
554 }
555
556 for sig in core.pending_signals {
562 let route = sig.route;
563 let target_id = sig.target;
564 let event = match self.instances.get_mut(&target_id) {
565 Some(target) => {
566 let max_inbox = target.config().max_inbox_per_route;
567 if max_inbox > 0 && target.inbox_len(route) >= max_inbox as usize {
568 KernelEvent::SignalDropped {
569 target: target_id,
570 route,
571 reason: SignalDropReason::QueueFull,
572 }
573 } else {
574 target.deliver_signal(
575 route,
576 InboundSignal {
577 from: inst_id,
578 principal: sig.principal,
579 payload: sig.payload,
580 seq: 0,
583 },
584 );
585 let entry = target.inflight_refs_mut().entry(route).or_insert(0);
586 *entry = entry.saturating_add(1);
587 KernelEvent::SignalDelivered {
588 from: inst_id,
589 target: target_id,
590 route,
591 }
592 }
593 }
594 None => KernelEvent::SignalDropped {
595 target: target_id,
596 route,
597 reason: SignalDropReason::TargetNotFound,
598 },
599 };
600 let evicted = self.observers.deliver(&event);
601 result.observers_evicted = result
602 .observers_evicted
603 .saturating_add(evicted.len() as u32);
604 }
605
606 Some(result)
607 }
608}
609
610pub(crate) struct InstanceStepResult {
614 pub popped_id: ScheduledActionId,
616 pub verdict: StepVerdict,
618 pub digest: [u8; 32],
621 pub effects_applied: u32,
623 pub effects_denied: u32,
625 pub domain_events_emitted: u32,
627 pub observers_evicted: u32,
629}
630
631struct StepCore {
634 popped_id: ScheduledActionId,
635 action_type: TypeCode,
636 verdict: StepVerdict,
637 digest: [u8; 32],
638 pending_signals: Vec<PendingSignal>,
639 events: Vec<KernelEvent>,
640 effects_applied: u32,
641 effects_denied: u32,
642 domain_events_emitted: u32,
643}
644
645#[allow(clippy::too_many_arguments)]
651fn step_one_core(
652 inst_id: InstanceId,
653 inst: &mut Instance,
654 registry: &ActionRegistry,
655 scratch: &mut StepStage,
656 now: Tick,
657 session_caps: CapabilityMask,
658 measure_digest: bool,
659) -> Option<StepCore> {
660 let entry = inst.scheduler_mut().pop_due(now)?;
661 let popped_id = entry.id;
662 let action_type = entry.action_type_code;
663
664 let digest_of = |inst: &Instance| -> [u8; 32] {
666 if measure_digest {
667 inst.state_digest()
668 } else {
669 [0u8; 32]
670 }
671 };
672
673 let reg = match registry.get(entry.action_type_code).cloned() {
676 Some(r) => r,
677 None => {
678 return Some(StepCore {
679 popped_id,
680 action_type,
681 verdict: StepVerdict::Skipped {
682 reason: SkipReason::Unregistered,
683 },
684 digest: digest_of(inst),
685 pending_signals: Vec::new(),
686 events: Vec::new(),
687 effects_applied: 0,
688 effects_denied: 0,
689 domain_events_emitted: 0,
690 });
691 }
692 };
693
694 let action = match (reg.deserializer)(reg.schema_version, &entry.action_bytes) {
696 Ok(a) => a,
697 Err(_) => {
698 return Some(StepCore {
699 popped_id,
700 action_type,
701 verdict: StepVerdict::Skipped {
702 reason: SkipReason::DeserFailed,
703 },
704 digest: digest_of(inst),
705 pending_signals: Vec::new(),
706 events: Vec::new(),
707 effects_applied: 0,
708 effects_denied: 0,
709 domain_events_emitted: 0,
710 });
711 }
712 };
713
714 let eff = effective_caps(inst.config().default_caps, &entry.principal, entry.caps_ceiling)
720 & session_caps;
721
722 let ctx = ActionContext::new(entry.actor, now, inst_id, &*inst);
723 let ops = action.compute_dyn(&ctx);
724
725 let inst_ref = &*inst;
728 scratch.clear();
732 let stage = &mut *scratch;
733 let mut next_scheduled_id = inst_ref.id_counters_snapshot().next_scheduled;
734 let budget = inst_ref.config().memory_budget_bytes;
735 let baseline_bytes: u64 = inst_ref.ledger().total_bytes();
736 let mut any_denied = false;
737 let mut applied_this_action: u32 = 0;
740 let mut denied_this_action: u32 = 0;
743 for op in ops {
744 let unverified: Effect<'_, Unverified> = Effect::new(inst_id, entry.principal.clone(), op);
745 match authorize(eff, unverified) {
746 Ok(authorized) => {
747 if budget > 0 {
752 let staged = super::stage::projected_component_bytes(
767 baseline_bytes,
768 &*stage,
769 inst_ref.ledger(),
770 );
771 let projected = match &authorized.op {
772 crate::state::Op::SetComponent { size, .. } => staged.saturating_add(*size),
773 crate::state::Op::RemoveComponent {
774 entity, type_code, ..
775 } => {
776 let freed = inst_ref
777 .ledger()
778 .component_size(*entity, *type_code)
779 .unwrap_or(0);
780 staged.saturating_sub(freed)
781 }
782 crate::state::Op::SpawnEntity { .. }
783 | crate::state::Op::DespawnEntity { .. }
784 | crate::state::Op::EmitEvent { .. }
785 | crate::state::Op::ScheduleAction { .. }
786 | crate::state::Op::SendSignal { .. } => staged,
787 };
788 if projected > budget {
789 denied_this_action = denied_this_action.saturating_add(1);
790 stage.events.push_back(KernelEvent::EffectFailed {
791 instance: inst_id,
792 reason: bytes::Bytes::from_static(b"budget_exceeded"),
793 });
794 continue;
795 }
796 }
797 let max_entities = inst_ref.config().max_entities;
802 if max_entities > 0
803 && matches!(&authorized.op, crate::state::Op::SpawnEntity { .. })
804 {
805 let projected_entities = (inst_ref.entities_len() as u64)
806 .saturating_add(stage.id_counters.next_entity_advance);
807 if projected_entities >= max_entities as u64 {
808 denied_this_action = denied_this_action.saturating_add(1);
809 stage.events.push_back(KernelEvent::EffectFailed {
810 instance: inst_id,
811 reason: bytes::Bytes::from_static(b"entity_quota_exceeded"),
812 });
813 continue;
814 }
815 }
816 let max_scheduled = inst_ref.config().max_scheduled;
820 if max_scheduled > 0
821 && matches!(&authorized.op, crate::state::Op::ScheduleAction { .. })
822 {
823 let projected_scheduled = (inst_ref.scheduler().len() as u64)
824 .saturating_add(stage.id_counters.next_scheduled_advance);
825 if projected_scheduled >= max_scheduled as u64 {
826 denied_this_action = denied_this_action.saturating_add(1);
827 stage.events.push_back(KernelEvent::EffectFailed {
828 instance: inst_id,
829 reason: bytes::Bytes::from_static(b"scheduled_quota_exceeded"),
830 });
831 continue;
832 }
833 }
834 dispatch(authorized, stage, now, &mut next_scheduled_id, eff);
835 applied_this_action = applied_this_action.saturating_add(1);
836 }
837 Err(_) => {
838 denied_this_action = denied_this_action.saturating_add(1);
839 any_denied = true;
840 }
841 }
842 }
843
844 if any_denied {
845 return Some(StepCore {
850 popped_id,
851 action_type,
852 verdict: StepVerdict::AuthDenied,
853 digest: digest_of(inst),
854 pending_signals: Vec::new(),
855 events: Vec::new(),
856 effects_applied: 0,
857 effects_denied: denied_this_action,
858 domain_events_emitted: 0,
859 });
860 }
861
862 let domain_emit_count = stage
865 .events
866 .iter()
867 .filter(|e| matches!(e, KernelEvent::DomainEventEmitted { .. }))
868 .count() as u32;
869 let events_to_deliver: Vec<KernelEvent> = stage.events.iter().cloned().collect();
870 let pending_signals = apply_stage(inst, stage);
871 let digest = digest_of(inst);
872
873 let verdict = if denied_this_action > 0 {
874 StepVerdict::BudgetPartial {
875 denied: denied_this_action,
876 }
877 } else {
878 StepVerdict::Committed
879 };
880
881 Some(StepCore {
882 popped_id,
883 action_type,
884 verdict,
885 digest,
886 pending_signals,
887 events: events_to_deliver,
888 effects_applied: applied_this_action,
889 effects_denied: denied_this_action,
890 domain_events_emitted: domain_emit_count,
891 })
892}
893
894#[cfg(test)]
895mod tests {
896 use super::*;
897 use bytes::Bytes;
898 use std::sync::atomic::{AtomicU32, Ordering};
899 use std::sync::Arc;
900
901 use crate::abi::{ExternalId, RouteId};
902 use crate::state::traits::_sealed::Sealed;
903 use crate::state::{ActionCompute, ActionDeriv, Op};
904 use serde::{Deserialize, Serialize};
905
906 #[derive(Serialize, Deserialize)]
908 struct SpawnOneAction;
909 impl Sealed for SpawnOneAction {}
910 impl ActionDeriv for SpawnOneAction {
911 const TYPE_CODE: TypeCode = TypeCode(100);
912 const SCHEMA_VERSION: u32 = 1;
913 }
914 impl ActionCompute for SpawnOneAction {
915 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
916 vec![Op::SpawnEntity {
917 id: EntityId::new(42).unwrap(),
918 owner: Principal::System,
919 }]
920 }
921 }
922
923 #[derive(Serialize, Deserialize)]
924 struct EmitAction;
925 impl Sealed for EmitAction {}
926 impl ActionDeriv for EmitAction {
927 const TYPE_CODE: TypeCode = TypeCode(101);
928 const SCHEMA_VERSION: u32 = 1;
929 }
930 impl ActionCompute for EmitAction {
931 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
932 vec![Op::EmitEvent {
933 actor: None,
934 event_type_code: TypeCode(7),
935 event_bytes: Bytes::from_static(b"hello"),
936 }]
937 }
938 }
939
940 #[derive(Serialize, Deserialize)]
941 struct SignalAction;
942 impl Sealed for SignalAction {}
943 impl ActionDeriv for SignalAction {
944 const TYPE_CODE: TypeCode = TypeCode(102);
945 const SCHEMA_VERSION: u32 = 1;
946 }
947 impl ActionCompute for SignalAction {
948 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
949 vec![Op::SendSignal {
951 target: InstanceId::new(1).unwrap(),
952 route: RouteId(1),
953 payload: Bytes::new(),
954 }]
955 }
956 }
957
958 #[derive(Serialize, Deserialize)]
960 struct SpawnThreeAction;
961 impl Sealed for SpawnThreeAction {}
962 impl ActionDeriv for SpawnThreeAction {
963 const TYPE_CODE: TypeCode = TypeCode(103);
964 const SCHEMA_VERSION: u32 = 1;
965 }
966 impl ActionCompute for SpawnThreeAction {
967 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
968 (1..=3)
969 .map(|n| Op::SpawnEntity {
970 id: EntityId::new(n).unwrap(),
971 owner: Principal::System,
972 })
973 .collect()
974 }
975 }
976
977 #[derive(Serialize, Deserialize)]
980 struct OversizedSetAction;
981 impl Sealed for OversizedSetAction {}
982 impl ActionDeriv for OversizedSetAction {
983 const TYPE_CODE: TypeCode = TypeCode(104);
984 const SCHEMA_VERSION: u32 = 1;
985 }
986 impl ActionCompute for OversizedSetAction {
987 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
988 vec![Op::SetComponent {
989 entity: EntityId::new(1).unwrap(),
990 type_code: TypeCode(7),
991 bytes: Bytes::from_static(b"x"),
992 size: u64::MAX,
993 }]
994 }
995 }
996
997 #[derive(Serialize, Deserialize)]
1002 struct PhantomRemovePoisonAction;
1003 impl Sealed for PhantomRemovePoisonAction {}
1004 impl ActionDeriv for PhantomRemovePoisonAction {
1005 const TYPE_CODE: TypeCode = TypeCode(105);
1006 const SCHEMA_VERSION: u32 = 1;
1007 }
1008 impl ActionCompute for PhantomRemovePoisonAction {
1009 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
1010 let e = EntityId::new(1).unwrap();
1011 vec![
1012 Op::SpawnEntity {
1013 id: e,
1014 owner: Principal::System,
1015 },
1016 Op::RemoveComponent {
1017 entity: e,
1018 type_code: TypeCode(99), size: u64::MAX, },
1021 Op::SetComponent {
1022 entity: e,
1023 type_code: TypeCode(8),
1024 bytes: Bytes::from_static(b"a"),
1025 size: 90,
1026 },
1027 Op::SetComponent {
1028 entity: e,
1029 type_code: TypeCode(9),
1030 bytes: Bytes::from_static(b"b"),
1031 size: 90,
1032 },
1033 ]
1034 }
1035 }
1036
1037 #[derive(Serialize, Deserialize)]
1041 struct SpawnThenScheduleAction;
1042 impl Sealed for SpawnThenScheduleAction {}
1043 impl ActionDeriv for SpawnThenScheduleAction {
1044 const TYPE_CODE: TypeCode = TypeCode(106);
1045 const SCHEMA_VERSION: u32 = 1;
1046 }
1047 impl ActionCompute for SpawnThenScheduleAction {
1048 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
1049 vec![
1050 Op::SpawnEntity {
1051 id: EntityId::new(1).unwrap(),
1052 owner: Principal::System,
1053 },
1054 Op::ScheduleAction {
1055 at: Tick(10),
1056 actor: None,
1057 action_type_code: TypeCode(100),
1058 action_bytes: Bytes::from_static(b""),
1059 },
1060 ]
1061 }
1062 }
1063
1064 struct CountingObserver {
1065 count: Arc<AtomicU32>,
1066 }
1067 impl KernelObserver for CountingObserver {
1068 fn on_event(&self, _event: &KernelEvent) {
1069 self.count.fetch_add(1, Ordering::SeqCst);
1070 }
1071 }
1072
1073 struct PanicObserver;
1074 impl KernelObserver for PanicObserver {
1075 fn on_event(&self, _event: &KernelEvent) {
1076 panic!("observer intentional panic");
1077 }
1078 }
1079
1080 #[test]
1081 fn create_instance_returns_monotonic_ids() {
1082 let mut k = Kernel::new();
1083 let i1 = k.create_instance(InstanceConfig::default());
1084 let i2 = k.create_instance(InstanceConfig::default());
1085 let i3 = k.create_instance(InstanceConfig::default());
1086 assert!(i1 < i2);
1087 assert!(i2 < i3);
1088 assert_eq!(i1.get(), 1);
1089 assert_eq!(i3.get(), 3);
1090 assert_eq!(k.instances_len(), 3);
1091 }
1092
1093 #[test]
1094 fn submit_unknown_instance_returns_error() {
1095 let mut k = Kernel::new();
1096 let bogus = InstanceId::new(99).unwrap();
1097 let result = k.submit(
1098 bogus,
1099 Principal::System,
1100 None,
1101 CapabilityMask::SYSTEM,
1102 Tick(0),
1103 TypeCode(100),
1104 Vec::new(),
1105 );
1106 assert!(matches!(result, Err(ArkheError::InstanceNotFound)));
1107 }
1108
1109 #[test]
1110 fn submit_back_pressures_at_max_scheduled() {
1111 let mut k = Kernel::new();
1114 let inst = k.create_instance(InstanceConfig {
1115 max_scheduled: 2,
1116 ..Default::default()
1117 });
1118 let sub = |k: &mut Kernel| {
1119 k.submit(inst, Principal::System, None, CapabilityMask::SYSTEM, Tick(0), TypeCode(100), Vec::new())
1120 };
1121 assert!(sub(&mut k).is_ok());
1122 assert!(sub(&mut k).is_ok());
1123 assert!(matches!(sub(&mut k), Err(ArkheError::QuotaExceeded)));
1124 }
1125
1126 #[test]
1127 fn step_denies_spawns_past_max_entities() {
1128 let mut k = Kernel::new();
1131 k.register_action::<SpawnThreeAction>();
1132 let inst = k.create_instance(InstanceConfig {
1133 max_entities: 2,
1134 ..Default::default()
1135 });
1136 k.submit(inst, Principal::System, None, CapabilityMask::SYSTEM, Tick(0), TypeCode(103), Vec::new())
1137 .unwrap();
1138 let report = k.step(Tick(0), CapabilityMask::SYSTEM);
1139 assert_eq!(report.effects_applied, 2);
1140 assert_eq!(report.effects_denied, 1);
1141 assert_eq!(k.instances.get(&inst).unwrap().entities_len(), 2);
1142 }
1143
1144 #[test]
1145 fn step_denies_oversized_component_no_budget_bypass() {
1146 let mut k = Kernel::new();
1149 k.register_action::<OversizedSetAction>();
1150 let inst = k.create_instance(InstanceConfig {
1151 memory_budget_bytes: 1000,
1152 ..Default::default()
1153 });
1154 k.submit(inst, Principal::System, None, CapabilityMask::SYSTEM, Tick(0), TypeCode(104), Vec::new())
1155 .unwrap();
1156 let report = k.step(Tick(0), CapabilityMask::SYSTEM);
1157 assert_eq!(report.effects_applied, 0);
1158 assert_eq!(report.effects_denied, 1);
1159 }
1160
1161 #[test]
1162 fn step_budget_above_i64_max_still_enforced() {
1163 let mut k = Kernel::new();
1169 k.register_action::<OversizedSetAction>();
1170 let inst = k.create_instance(InstanceConfig {
1171 memory_budget_bytes: 1u64 << 63, ..Default::default()
1173 });
1174 k.submit(inst, Principal::System, None, CapabilityMask::SYSTEM, Tick(0), TypeCode(104), Vec::new())
1175 .unwrap();
1176 let report = k.step(Tick(0), CapabilityMask::SYSTEM);
1177 assert_eq!(report.effects_applied, 0);
1178 assert_eq!(
1179 report.effects_denied, 1,
1180 "a u64::MAX add must trip a budget above i64::MAX"
1181 );
1182 }
1183
1184 #[test]
1185 fn step_phantom_remove_does_not_bypass_budget() {
1186 let mut k = Kernel::new();
1192 k.register_action::<PhantomRemovePoisonAction>();
1193 let inst = k.create_instance(InstanceConfig {
1194 memory_budget_bytes: 100,
1195 ..Default::default()
1196 });
1197 k.submit(inst, Principal::System, None, CapabilityMask::SYSTEM, Tick(0), TypeCode(105), Vec::new())
1198 .unwrap();
1199 let report = k.step(Tick(0), CapabilityMask::SYSTEM);
1200 assert_eq!(
1201 report.effects_denied, 1,
1202 "the second SetComponent must be denied, not slipped past a poisoned budget"
1203 );
1204 let total = k.instances.get(&inst).unwrap().ledger().total_bytes();
1205 assert_eq!(
1206 total, 90,
1207 "exactly the first 90-byte component is accounted (budget held at 100)"
1208 );
1209 }
1210
1211 #[test]
1212 fn step_rollback_does_not_count_applied_effects() {
1213 let mut k = Kernel::new();
1217 k.register_action::<SpawnThenScheduleAction>();
1218 let counters = Arc::new(VariantCounters::default());
1219 k.register_observer(Box::new(VariantTallyObserver {
1220 counters: counters.clone(),
1221 }));
1222 let inst = k.create_instance(InstanceConfig::default());
1223 k.submit(
1224 inst,
1225 Principal::External(ExternalId(7)),
1226 None,
1227 CapabilityMask::SYSTEM,
1228 Tick(0),
1229 TypeCode(106),
1230 Vec::new(),
1231 )
1232 .unwrap();
1233 let report = k.step(Tick(0), CapabilityMask::empty());
1236 assert_eq!(
1237 report.effects_applied, 0,
1238 "a rolled-back spawn must not count as applied"
1239 );
1240 assert!(report.effects_denied >= 1);
1241 assert_eq!(
1242 k.instances.get(&inst).unwrap().entities_len(),
1243 0,
1244 "rollback restores instance state"
1245 );
1246 assert_eq!(
1249 counters.action_executed.load(Ordering::SeqCst),
1250 0,
1251 "AuthDenied rollback must not emit ActionExecuted"
1252 );
1253 }
1254
1255 #[test]
1256 fn submit_then_step_executes_action_and_spawns_entity() {
1257 let mut k = Kernel::new();
1258 k.register_action::<SpawnOneAction>();
1259 let inst = k.create_instance(InstanceConfig::default());
1260 k.submit(
1261 inst,
1262 Principal::System,
1263 None,
1264 CapabilityMask::SYSTEM,
1265 Tick(0),
1266 TypeCode(100),
1267 Vec::new(),
1268 )
1269 .unwrap();
1270
1271 let report = k.step(Tick(5), CapabilityMask::SYSTEM);
1272 assert_eq!(report.actions_executed, 1);
1273 assert_eq!(report.effects_applied, 1);
1274 assert_eq!(report.effects_denied, 0);
1275 let inst_ref = k.instances.get(&inst).unwrap();
1277 assert_eq!(inst_ref.entities_len(), 1);
1278 }
1279
1280 #[test]
1281 fn step_with_unknown_type_code_skips_action() {
1282 let mut k = Kernel::new();
1283 let inst = k.create_instance(InstanceConfig::default());
1285 k.submit(
1286 inst,
1287 Principal::System,
1288 None,
1289 CapabilityMask::SYSTEM,
1290 Tick(0),
1291 TypeCode(999),
1292 Vec::new(),
1293 )
1294 .unwrap();
1295
1296 let report = k.step(Tick(5), CapabilityMask::SYSTEM);
1297 assert_eq!(report.actions_executed, 1);
1298 assert_eq!(report.effects_applied, 0);
1299 assert_eq!(k.instances.get(&inst).unwrap().entities_len(), 0);
1300 }
1301
1302 #[test]
1303 fn observer_receives_action_executed_event() {
1304 let mut k = Kernel::new();
1305 k.register_action::<SpawnOneAction>();
1306 let count = Arc::new(AtomicU32::new(0));
1307 let _h = k.register_observer(Box::new(CountingObserver {
1308 count: count.clone(),
1309 }));
1310 let inst = k.create_instance(InstanceConfig::default());
1311 k.submit(
1312 inst,
1313 Principal::System,
1314 None,
1315 CapabilityMask::SYSTEM,
1316 Tick(0),
1317 TypeCode(100),
1318 Vec::new(),
1319 )
1320 .unwrap();
1321 k.step(Tick(5), CapabilityMask::SYSTEM);
1322 assert_eq!(count.load(Ordering::SeqCst), 1);
1324 }
1325
1326 #[test]
1327 fn observer_receives_domain_event_emitted() {
1328 let mut k = Kernel::new();
1329 k.register_action::<EmitAction>();
1330 let count = Arc::new(AtomicU32::new(0));
1331 k.register_observer(Box::new(CountingObserver {
1332 count: count.clone(),
1333 }));
1334 let inst = k.create_instance(InstanceConfig::default());
1335 k.submit(
1336 inst,
1337 Principal::System,
1338 None,
1339 CapabilityMask::SYSTEM,
1340 Tick(0),
1341 TypeCode(101),
1342 Vec::new(),
1343 )
1344 .unwrap();
1345 let report = k.step(Tick(5), CapabilityMask::SYSTEM);
1346 assert_eq!(report.domain_events_emitted, 1);
1347 assert_eq!(count.load(Ordering::SeqCst), 2);
1349 }
1350
1351 #[test]
1352 fn panic_observer_evicted_after_first_event() {
1353 let mut k = Kernel::new();
1354 k.register_action::<SpawnOneAction>();
1355 let h = k.register_observer(Box::new(PanicObserver));
1356 let inst = k.create_instance(InstanceConfig::default());
1357 k.submit(
1358 inst,
1359 Principal::System,
1360 None,
1361 CapabilityMask::SYSTEM,
1362 Tick(0),
1363 TypeCode(100),
1364 Vec::new(),
1365 )
1366 .unwrap();
1367 let report = k.step(Tick(5), CapabilityMask::SYSTEM);
1368 assert!(report.observers_evicted >= 1);
1369 assert!(k.observers.is_evicted(h));
1370 }
1371
1372 #[test]
1373 fn unauthenticated_principal_denies_all_effects() {
1374 let mut k = Kernel::new();
1375 k.register_action::<SpawnOneAction>();
1376 let inst = k.create_instance(InstanceConfig::default());
1377 k.submit(
1378 inst,
1379 Principal::Unauthenticated,
1380 None,
1381 CapabilityMask::SYSTEM,
1382 Tick(0),
1383 TypeCode(100),
1384 Vec::new(),
1385 )
1386 .unwrap();
1387 let report = k.step(Tick(5), CapabilityMask::SYSTEM);
1388 assert_eq!(report.effects_denied, 1);
1389 assert_eq!(report.effects_applied, 0);
1390 assert_eq!(k.instances.get(&inst).unwrap().entities_len(), 0);
1392 }
1393
1394 #[test]
1395 fn external_without_system_cap_denies_signal() {
1396 let mut k = Kernel::new();
1397 k.register_action::<SignalAction>();
1398 let inst = k.create_instance(InstanceConfig::default());
1399 k.submit(
1400 inst,
1401 Principal::External(ExternalId(7)),
1402 None,
1403 CapabilityMask::SYSTEM,
1404 Tick(0),
1405 TypeCode(102),
1406 Vec::new(),
1407 )
1408 .unwrap();
1409 let report = k.step(Tick(5), CapabilityMask::default());
1410 assert_eq!(report.effects_denied, 1);
1411 assert_eq!(report.effects_applied, 0);
1412 }
1413
1414 #[test]
1415 fn wal_attached_kernel_records_submit_then_step() {
1416 let mut k = Kernel::new_with_wal([7u8; 32], [3u8; 32]);
1419 k.register_action::<SpawnOneAction>();
1420 let inst = k.create_instance(InstanceConfig::default());
1421 assert_eq!(k.wal_record_count(), Some(0));
1422 k.submit(
1423 inst,
1424 Principal::System,
1425 None,
1426 CapabilityMask::SYSTEM,
1427 Tick(0),
1428 TypeCode(100),
1429 Vec::new(),
1430 )
1431 .unwrap();
1432 assert_eq!(k.wal_record_count(), Some(1));
1433 let pre_tip = k.wal_chain_tip().unwrap();
1434 k.step(Tick(5), CapabilityMask::SYSTEM);
1435 assert_eq!(k.wal_record_count(), Some(2));
1436 let post_tip = k.wal_chain_tip().unwrap();
1437 assert_ne!(pre_tip, post_tip);
1438 }
1439
1440 #[test]
1441 fn wal_kernel_export_then_verify_chain() {
1442 let mut k = Kernel::new_with_wal([11u8; 32], [0u8; 32]);
1443 k.register_action::<SpawnOneAction>();
1444 let inst = k.create_instance(InstanceConfig::default());
1445 for _ in 0..3 {
1446 k.submit(
1447 inst,
1448 Principal::System,
1449 None,
1450 CapabilityMask::SYSTEM,
1451 Tick(0),
1452 TypeCode(100),
1453 Vec::new(),
1454 )
1455 .unwrap();
1456 k.step(Tick(0), CapabilityMask::SYSTEM);
1457 }
1458 let wal = k.export_wal().expect("wal attached");
1459 assert_eq!(wal.records.len(), 6);
1461 wal.verify_chain([11u8; 32]).expect("chain verifies");
1462 }
1463
1464 #[test]
1465 fn replay_reconstructs_chain_tip() {
1466 use crate::persist::replay_into;
1467 let mut k1 = Kernel::new_with_wal([42u8; 32], [0u8; 32]);
1469 k1.register_action::<SpawnOneAction>();
1470 let i1 = k1.create_instance(InstanceConfig::default());
1471 for _ in 0..4 {
1472 k1.submit(
1473 i1,
1474 Principal::System,
1475 None,
1476 CapabilityMask::SYSTEM,
1477 Tick(0),
1478 TypeCode(100),
1479 Vec::new(),
1480 )
1481 .unwrap();
1482 k1.step(Tick(0), CapabilityMask::SYSTEM);
1483 }
1484 let original_tip = k1.wal_chain_tip().unwrap();
1485 let wal = k1.export_wal().unwrap();
1486
1487 let mut k2 = Kernel::new_with_wal([42u8; 32], [0u8; 32]);
1489 k2.register_action::<SpawnOneAction>();
1490 let _i2 = k2.create_instance(InstanceConfig::default());
1493 let report = replay_into(&mut k2, &wal).expect("replay ok");
1494 assert_eq!(report.submits_replayed, 4);
1496 assert_eq!(report.steps_replayed, 4);
1497 assert_eq!(report.final_chain_tip, original_tip);
1501 }
1502
1503 #[test]
1504 fn step_processes_instances_in_ascending_order() {
1505 let mut k = Kernel::new();
1509 k.register_action::<SpawnOneAction>();
1510 let i1 = k.create_instance(InstanceConfig::default());
1511 let i2 = k.create_instance(InstanceConfig::default());
1512 k.submit(
1513 i2,
1514 Principal::System,
1515 None,
1516 CapabilityMask::SYSTEM,
1517 Tick(0),
1518 TypeCode(100),
1519 Vec::new(),
1520 )
1521 .unwrap();
1522 k.submit(
1523 i1,
1524 Principal::System,
1525 None,
1526 CapabilityMask::SYSTEM,
1527 Tick(0),
1528 TypeCode(100),
1529 Vec::new(),
1530 )
1531 .unwrap();
1532 let report = k.step(Tick(5), CapabilityMask::SYSTEM);
1533 assert_eq!(report.actions_executed, 2);
1534 assert_eq!(report.effects_applied, 2);
1535 }
1536
1537 #[test]
1538 fn stats_aggregate_reflects_instances_and_scheduler() {
1539 let mut k = Kernel::new();
1540 k.register_action::<SpawnOneAction>();
1541 assert_eq!(k.stats(), Stats::default());
1542
1543 let i1 = k.create_instance(InstanceConfig::default());
1544 let i2 = k.create_instance(InstanceConfig::default());
1545 let stats_pre = k.stats();
1546 assert_eq!(stats_pre.instance_count, 2);
1547 assert_eq!(stats_pre.scheduled_action_count, 0);
1548 assert_eq!(stats_pre.entity_count, 0);
1549
1550 k.submit(
1551 i1,
1552 Principal::System,
1553 None,
1554 CapabilityMask::SYSTEM,
1555 Tick(0),
1556 TypeCode(100),
1557 Vec::new(),
1558 )
1559 .unwrap();
1560 k.submit(
1561 i2,
1562 Principal::System,
1563 None,
1564 CapabilityMask::SYSTEM,
1565 Tick(0),
1566 TypeCode(100),
1567 Vec::new(),
1568 )
1569 .unwrap();
1570 let stats_queued = k.stats();
1571 assert_eq!(stats_queued.scheduled_action_count, 2);
1572
1573 let _ = k.step(Tick(1), CapabilityMask::SYSTEM);
1574 let stats_post = k.stats();
1575 assert_eq!(stats_post.scheduled_action_count, 0);
1576 assert_eq!(stats_post.entity_count, 2);
1577 }
1578
1579 #[test]
1580 fn stats_counts_observers() {
1581 struct NullObs;
1582 impl KernelObserver for NullObs {
1583 fn on_event(&self, _e: &KernelEvent) {}
1584 }
1585
1586 let mut k = Kernel::new();
1587 k.register_observer(Box::new(NullObs));
1588 k.register_observer(Box::new(NullObs));
1589 assert_eq!(k.stats().observer_count, 2);
1590 }
1591
1592 #[test]
1593 fn stats_wal_record_count_reflects_writer() {
1594 let mut k = Kernel::new_with_wal([1u8; 32], [0u8; 32]);
1595 k.register_action::<SpawnOneAction>();
1596 assert_eq!(k.stats().wal_record_count, 0);
1597
1598 let i = k.create_instance(InstanceConfig::default());
1599 k.submit(
1600 i,
1601 Principal::System,
1602 None,
1603 CapabilityMask::SYSTEM,
1604 Tick(0),
1605 TypeCode(100),
1606 Vec::new(),
1607 )
1608 .unwrap();
1609 let _ = k.step(Tick(1), CapabilityMask::SYSTEM);
1610 assert_eq!(k.stats().wal_record_count, 2);
1612 }
1613
1614 struct ForceUnloadCapture {
1618 seen: Arc<std::sync::Mutex<Vec<(RouteId, u32)>>>,
1619 }
1620 impl KernelObserver for ForceUnloadCapture {
1621 fn on_event(&self, event: &KernelEvent) {
1622 if let KernelEvent::ModuleForceUnloaded {
1623 route_id,
1624 live_refs_at_unload,
1625 } = event
1626 {
1627 self.seen
1628 .lock()
1629 .unwrap()
1630 .push((*route_id, *live_refs_at_unload));
1631 }
1632 }
1633 }
1634
1635 #[test]
1636 fn force_unload_without_cap_denied() {
1637 let mut k = Kernel::new();
1638 let result = k.force_unload(RouteId(1), CapabilityMask::default());
1639 assert!(matches!(result, Err(ArkheError::CapabilityDenied)));
1640 }
1641
1642 #[test]
1643 fn force_unload_removes_inflight_refs() {
1644 let mut k = Kernel::new();
1645 k.register_action::<SignalAction>();
1646 let inst = k.create_instance(InstanceConfig {
1650 default_caps: CapabilityMask::SYSTEM,
1651 ..Default::default()
1652 });
1653 k.submit(
1657 inst,
1658 Principal::System,
1659 None,
1660 CapabilityMask::SYSTEM,
1661 Tick(0),
1662 TypeCode(102),
1663 Vec::new(),
1664 )
1665 .unwrap();
1666 let report = k.step(Tick(0), CapabilityMask::SYSTEM);
1667 assert_eq!(report.effects_applied, 1);
1668 assert_eq!(
1669 k.instances
1670 .get(&inst)
1671 .unwrap()
1672 .inflight_refs_for(RouteId(1)),
1673 1
1674 );
1675
1676 let dropped = k
1677 .force_unload(RouteId(1), CapabilityMask::ADMIN_UNLOAD)
1678 .expect("admin_unload caps");
1679 assert_eq!(dropped, 1);
1680 assert_eq!(
1681 k.instances
1682 .get(&inst)
1683 .unwrap()
1684 .inflight_refs_for(RouteId(1)),
1685 0
1686 );
1687 assert_eq!(k.instances.get(&inst).unwrap().inflight_refs_len(), 0);
1688 }
1689
1690 #[test]
1691 fn force_unload_emits_module_unloaded_event() {
1692 let mut k = Kernel::new();
1693 k.register_action::<SignalAction>();
1694 let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
1695 k.register_observer(Box::new(ForceUnloadCapture { seen: seen.clone() }));
1696 let inst = k.create_instance(InstanceConfig {
1697 default_caps: CapabilityMask::SYSTEM,
1698 ..Default::default()
1699 });
1700 k.submit(
1701 inst,
1702 Principal::System,
1703 None,
1704 CapabilityMask::SYSTEM,
1705 Tick(0),
1706 TypeCode(102),
1707 Vec::new(),
1708 )
1709 .unwrap();
1710 let _ = k.step(Tick(0), CapabilityMask::SYSTEM);
1711
1712 k.force_unload(RouteId(1), CapabilityMask::ADMIN_UNLOAD)
1713 .expect("admin_unload caps");
1714
1715 let captured = seen.lock().unwrap().clone();
1716 assert_eq!(captured, vec![(RouteId(1), 1)]);
1717 }
1718
1719 #[test]
1720 fn force_unload_no_live_refs_returns_zero() {
1721 let mut k = Kernel::new();
1722 let _ = k.create_instance(InstanceConfig::default());
1723 let dropped = k
1724 .force_unload(RouteId(99), CapabilityMask::ADMIN_UNLOAD)
1725 .expect("admin_unload caps");
1726 assert_eq!(dropped, 0);
1727 }
1728
1729 #[derive(Serialize, Deserialize)]
1733 struct SignalTwiceAction;
1734 impl Sealed for SignalTwiceAction {}
1735 impl ActionDeriv for SignalTwiceAction {
1736 const TYPE_CODE: TypeCode = TypeCode(110);
1737 const SCHEMA_VERSION: u32 = 1;
1738 }
1739 impl ActionCompute for SignalTwiceAction {
1740 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
1741 let one = || Op::SendSignal {
1742 target: InstanceId::new(1).unwrap(),
1743 route: RouteId(1),
1744 payload: Bytes::new(),
1745 };
1746 vec![one(), one()]
1747 }
1748 }
1749
1750 #[derive(Serialize, Deserialize)]
1752 struct SignalMissingAction;
1753 impl Sealed for SignalMissingAction {}
1754 impl ActionDeriv for SignalMissingAction {
1755 const TYPE_CODE: TypeCode = TypeCode(111);
1756 const SCHEMA_VERSION: u32 = 1;
1757 }
1758 impl ActionCompute for SignalMissingAction {
1759 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
1760 vec![Op::SendSignal {
1761 target: InstanceId::new(99).unwrap(),
1762 route: RouteId(1),
1763 payload: Bytes::new(),
1764 }]
1765 }
1766 }
1767
1768 #[derive(Serialize, Deserialize)]
1770 struct ScheduleSignalChildAction;
1771 impl Sealed for ScheduleSignalChildAction {}
1772 impl ActionDeriv for ScheduleSignalChildAction {
1773 const TYPE_CODE: TypeCode = TypeCode(112);
1774 const SCHEMA_VERSION: u32 = 1;
1775 }
1776 impl ActionCompute for ScheduleSignalChildAction {
1777 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
1778 vec![Op::ScheduleAction {
1779 at: Tick(1),
1780 actor: None,
1781 action_type_code: SignalAction::TYPE_CODE,
1782 action_bytes: Bytes::from_static(b""),
1783 }]
1784 }
1785 }
1786
1787 #[derive(Default)]
1788 struct SignalEventCounters {
1789 delivered: AtomicU32,
1790 dropped_not_found: AtomicU32,
1791 dropped_queue_full: AtomicU32,
1792 }
1793 struct SignalEventCapture {
1794 c: Arc<SignalEventCounters>,
1795 }
1796 impl KernelObserver for SignalEventCapture {
1797 fn on_event(&self, event: &KernelEvent) {
1798 match event {
1799 KernelEvent::SignalDelivered { .. } => {
1800 self.c.delivered.fetch_add(1, Ordering::SeqCst);
1801 }
1802 KernelEvent::SignalDropped { reason, .. } => match reason {
1803 crate::runtime::event::SignalDropReason::TargetNotFound => {
1804 self.c.dropped_not_found.fetch_add(1, Ordering::SeqCst);
1805 }
1806 crate::runtime::event::SignalDropReason::QueueFull => {
1807 self.c.dropped_queue_full.fetch_add(1, Ordering::SeqCst);
1808 }
1809 crate::runtime::event::SignalDropReason::Cancelled => {}
1810 },
1811 KernelEvent::ActionExecuted { .. }
1814 | KernelEvent::ActionFailed { .. }
1815 | KernelEvent::EffectFailed { .. }
1816 | KernelEvent::ObserverPanic { .. }
1817 | KernelEvent::ObserverEvicted { .. }
1818 | KernelEvent::ModuleForceUnloaded { .. }
1819 | KernelEvent::ActionDeferredToNextTick { .. }
1820 | KernelEvent::ObserversFlushed { .. }
1821 | KernelEvent::DomainEventEmitted { .. } => {}
1822 }
1823 }
1824 }
1825
1826 fn cfg_sys() -> InstanceConfig {
1827 InstanceConfig {
1828 default_caps: CapabilityMask::SYSTEM,
1829 max_inbox_per_route: 8,
1830 ..Default::default()
1831 }
1832 }
1833
1834 #[test]
1835 fn system_no_longer_blanket_bypasses() {
1836 let mut k = Kernel::new();
1839 k.register_action::<SignalAction>();
1840 let inst = k.create_instance(InstanceConfig::default()); k.submit(
1842 inst,
1843 Principal::System,
1844 None,
1845 CapabilityMask::SYSTEM,
1846 Tick(0),
1847 TypeCode(102),
1848 Vec::new(),
1849 )
1850 .unwrap();
1851 let report = k.step(Tick(0), CapabilityMask::SYSTEM);
1852 assert_eq!(
1853 report.effects_denied, 1,
1854 "System SendSignal denied when default_caps withholds SYSTEM"
1855 );
1856 assert_eq!(report.effects_applied, 0);
1857 }
1858
1859 #[test]
1860 fn signal_delivered_routes_to_inbox() {
1861 let mut k = Kernel::new();
1862 k.register_action::<SignalAction>();
1863 let counters = Arc::new(SignalEventCounters::default());
1864 k.register_observer(Box::new(SignalEventCapture {
1865 c: counters.clone(),
1866 }));
1867 let inst = k.create_instance(cfg_sys());
1868 k.submit(
1869 inst,
1870 Principal::System,
1871 None,
1872 CapabilityMask::SYSTEM,
1873 Tick(0),
1874 TypeCode(102),
1875 Vec::new(),
1876 )
1877 .unwrap();
1878 let report = k.step(Tick(0), CapabilityMask::SYSTEM);
1879 assert_eq!(report.effects_applied, 1);
1880 let target = k.instances.get(&inst).unwrap();
1881 assert_eq!(target.inbox_len(RouteId(1)), 1);
1882 assert_eq!(target.inflight_refs_for(RouteId(1)), 1);
1883 assert_eq!(counters.delivered.load(Ordering::SeqCst), 1);
1884 }
1885
1886 #[test]
1887 fn signal_dropped_target_not_found() {
1888 let mut k = Kernel::new();
1889 k.register_action::<SignalMissingAction>();
1890 let counters = Arc::new(SignalEventCounters::default());
1891 k.register_observer(Box::new(SignalEventCapture {
1892 c: counters.clone(),
1893 }));
1894 let inst = k.create_instance(cfg_sys());
1895 k.submit(
1896 inst,
1897 Principal::System,
1898 None,
1899 CapabilityMask::SYSTEM,
1900 Tick(0),
1901 TypeCode(111),
1902 Vec::new(),
1903 )
1904 .unwrap();
1905 let report = k.step(Tick(0), CapabilityMask::SYSTEM);
1906 assert_eq!(report.effects_applied, 1);
1909 assert_eq!(counters.dropped_not_found.load(Ordering::SeqCst), 1);
1910 assert_eq!(counters.delivered.load(Ordering::SeqCst), 0);
1911 assert_eq!(
1912 k.instances.get(&inst).unwrap().inflight_refs_for(RouteId(1)),
1913 0
1914 );
1915 }
1916
1917 #[test]
1918 fn signal_dropped_queue_full() {
1919 let mut k = Kernel::new();
1920 k.register_action::<SignalTwiceAction>();
1921 let counters = Arc::new(SignalEventCounters::default());
1922 k.register_observer(Box::new(SignalEventCapture {
1923 c: counters.clone(),
1924 }));
1925 let inst = k.create_instance(InstanceConfig {
1927 default_caps: CapabilityMask::SYSTEM,
1928 max_inbox_per_route: 1,
1929 ..Default::default()
1930 });
1931 k.submit(
1932 inst,
1933 Principal::System,
1934 None,
1935 CapabilityMask::SYSTEM,
1936 Tick(0),
1937 TypeCode(110),
1938 Vec::new(),
1939 )
1940 .unwrap();
1941 let _ = k.step(Tick(0), CapabilityMask::SYSTEM);
1942 assert_eq!(counters.delivered.load(Ordering::SeqCst), 1);
1943 assert_eq!(counters.dropped_queue_full.load(Ordering::SeqCst), 1);
1944 let target = k.instances.get(&inst).unwrap();
1945 assert_eq!(target.inbox_len(RouteId(1)), 1, "exactly one fit the inbox");
1946 assert_eq!(target.inflight_refs_for(RouteId(1)), 1);
1947 }
1948
1949 #[test]
1950 fn cap_time_shift_unrepresentable() {
1951 let mut k = Kernel::new();
1957 k.register_action::<ScheduleSignalChildAction>();
1958 k.register_action::<SignalAction>();
1959 let inst = k.create_instance(cfg_sys());
1960 k.submit(
1961 inst,
1962 Principal::System,
1963 None,
1964 CapabilityMask::SYSTEM,
1965 Tick(0),
1966 TypeCode(112),
1967 Vec::new(),
1968 )
1969 .unwrap();
1970 let r0 = k.step(Tick(0), CapabilityMask::SYSTEM);
1972 assert_eq!(r0.effects_applied, 1);
1973 let r1 = k.step(Tick(1), CapabilityMask::empty());
1975 assert_eq!(r1.actions_executed, 1, "child popped");
1976 assert_eq!(r1.effects_applied, 0, "child SendSignal denied under narrowed session");
1977 assert_eq!(
1978 k.instances.get(&inst).unwrap().inbox_len(RouteId(1)),
1979 0,
1980 "no signal delivered — caps could not be time-shifted"
1981 );
1982 }
1983
1984 #[derive(Serialize, Deserialize)]
1991 struct SetCompAction {
1992 size: u64,
1993 entity_id: u64,
1994 }
1995 impl Sealed for SetCompAction {}
1996 impl ActionDeriv for SetCompAction {
1997 const TYPE_CODE: TypeCode = TypeCode(200);
1998 const SCHEMA_VERSION: u32 = 1;
1999 }
2000 impl ActionCompute for SetCompAction {
2001 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
2002 let entity = EntityId::new(self.entity_id).unwrap();
2003 vec![
2004 Op::SpawnEntity {
2005 id: entity,
2006 owner: Principal::System,
2007 },
2008 Op::SetComponent {
2009 entity,
2010 type_code: TypeCode(7),
2011 bytes: Bytes::from(vec![0u8; self.size as usize]),
2012 size: self.size,
2013 },
2014 ]
2015 }
2016 }
2017
2018 #[derive(Serialize, Deserialize)]
2022 struct TwoSetCompAction {
2023 a: u64,
2024 b: u64,
2025 }
2026 impl Sealed for TwoSetCompAction {}
2027 impl ActionDeriv for TwoSetCompAction {
2028 const TYPE_CODE: TypeCode = TypeCode(201);
2029 const SCHEMA_VERSION: u32 = 1;
2030 }
2031 impl ActionCompute for TwoSetCompAction {
2032 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
2033 let e1 = EntityId::new(1).unwrap();
2034 let e2 = EntityId::new(2).unwrap();
2035 vec![
2036 Op::SpawnEntity {
2037 id: e1,
2038 owner: Principal::System,
2039 },
2040 Op::SpawnEntity {
2041 id: e2,
2042 owner: Principal::System,
2043 },
2044 Op::SetComponent {
2045 entity: e1,
2046 type_code: TypeCode(7),
2047 bytes: Bytes::from(vec![0u8; self.a as usize]),
2048 size: self.a,
2049 },
2050 Op::SetComponent {
2051 entity: e2,
2052 type_code: TypeCode(7),
2053 bytes: Bytes::from(vec![0u8; self.b as usize]),
2054 size: self.b,
2055 },
2056 ]
2057 }
2058 }
2059
2060 fn cfg_with_budget(budget: u64) -> InstanceConfig {
2061 InstanceConfig {
2062 memory_budget_bytes: budget,
2063 ..Default::default()
2064 }
2065 }
2066
2067 fn submit_set(k: &mut Kernel, inst: InstanceId, size: u64, entity_id: u64) {
2068 let action = SetCompAction { size, entity_id };
2069 let bytes = Action::canonical_bytes(&action);
2070 k.submit(
2071 inst,
2072 Principal::System,
2073 None,
2074 CapabilityMask::SYSTEM,
2075 Tick(0),
2076 SetCompAction::TYPE_CODE,
2077 bytes,
2078 )
2079 .expect("submit ok");
2080 }
2081
2082 #[test]
2083 fn budget_zero_allows_unlimited() {
2084 let mut k = Kernel::new();
2087 k.register_action::<SetCompAction>();
2088 let inst = k.create_instance(InstanceConfig::default());
2089 submit_set(&mut k, inst, 1_000_000, 1);
2090 let report = k.step(Tick(0), CapabilityMask::SYSTEM);
2091 assert_eq!(report.effects_applied, 2);
2092 assert_eq!(report.effects_denied, 0);
2093 assert_eq!(k.instances.get(&inst).unwrap().components_len(), 1);
2094 }
2095
2096 #[test]
2097 fn budget_exceeded_denies_op() {
2098 let mut k = Kernel::new();
2101 k.register_action::<SetCompAction>();
2102 let inst = k.create_instance(cfg_with_budget(100));
2103 submit_set(&mut k, inst, 500, 1);
2104 let report = k.step(Tick(0), CapabilityMask::SYSTEM);
2105 assert_eq!(report.effects_applied, 1); assert_eq!(report.effects_denied, 1); assert_eq!(report.actions_executed, 1);
2108 assert_eq!(k.instances.get(&inst).unwrap().entities_len(), 1);
2109 assert_eq!(k.instances.get(&inst).unwrap().components_len(), 0);
2110 }
2111
2112 #[test]
2113 fn budget_at_edge_allows_equal() {
2114 let mut k = Kernel::new();
2117 k.register_action::<SetCompAction>();
2118 let inst = k.create_instance(cfg_with_budget(500));
2119 submit_set(&mut k, inst, 500, 1);
2120 let report = k.step(Tick(0), CapabilityMask::SYSTEM);
2121 assert_eq!(report.effects_applied, 2);
2122 assert_eq!(report.effects_denied, 0);
2123 assert_eq!(k.instances.get(&inst).unwrap().components_len(), 1);
2124 assert_eq!(k.instances.get(&inst).unwrap().ledger().total_bytes(), 500);
2125 }
2126
2127 #[test]
2128 fn multi_op_stage_respects_running_delta() {
2129 let mut k = Kernel::new();
2135 k.register_action::<TwoSetCompAction>();
2136 let inst = k.create_instance(cfg_with_budget(600));
2137 let action = TwoSetCompAction { a: 400, b: 400 };
2138 let bytes = Action::canonical_bytes(&action);
2139 k.submit(
2140 inst,
2141 Principal::System,
2142 None,
2143 CapabilityMask::SYSTEM,
2144 Tick(0),
2145 TwoSetCompAction::TYPE_CODE,
2146 bytes,
2147 )
2148 .unwrap();
2149 let report = k.step(Tick(0), CapabilityMask::SYSTEM);
2150 assert_eq!(report.effects_applied, 3);
2151 assert_eq!(report.effects_denied, 1);
2152 assert_eq!(k.instances.get(&inst).unwrap().entities_len(), 2);
2153 assert_eq!(k.instances.get(&inst).unwrap().components_len(), 1);
2154 assert_eq!(k.instances.get(&inst).unwrap().ledger().total_bytes(), 400);
2155 }
2156
2157 struct EffectFailedCapture {
2159 seen: Arc<std::sync::Mutex<Vec<Bytes>>>,
2160 }
2161 impl KernelObserver for EffectFailedCapture {
2162 fn on_event(&self, event: &KernelEvent) {
2163 if let KernelEvent::EffectFailed { reason, .. } = event {
2164 self.seen.lock().unwrap().push(reason.clone());
2165 }
2166 }
2167 }
2168
2169 #[test]
2170 fn effect_failed_event_on_budget_deny() {
2171 let mut k = Kernel::new();
2172 k.register_action::<SetCompAction>();
2173 let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
2174 k.register_observer(Box::new(EffectFailedCapture { seen: seen.clone() }));
2175 let inst = k.create_instance(cfg_with_budget(100));
2176 submit_set(&mut k, inst, 500, 1);
2177 let _ = k.step(Tick(0), CapabilityMask::SYSTEM);
2178
2179 let captured = seen.lock().unwrap().clone();
2180 assert_eq!(captured.len(), 1);
2181 assert_eq!(captured[0].as_ref(), b"budget_exceeded");
2182 }
2183
2184 use crate::runtime::event::EventMask;
2187
2188 #[derive(Default)]
2191 struct VariantCounters {
2192 action_executed: AtomicU32,
2193 action_failed: AtomicU32,
2194 domain_event: AtomicU32,
2195 effect_failed: AtomicU32,
2196 other: AtomicU32,
2197 }
2198
2199 struct VariantTallyObserver {
2200 counters: Arc<VariantCounters>,
2201 }
2202 impl KernelObserver for VariantTallyObserver {
2203 fn on_event(&self, event: &KernelEvent) {
2204 match event {
2205 KernelEvent::ActionExecuted { .. } => {
2206 self.counters.action_executed.fetch_add(1, Ordering::SeqCst);
2207 }
2208 KernelEvent::ActionFailed { .. } => {
2209 self.counters.action_failed.fetch_add(1, Ordering::SeqCst);
2210 }
2211 KernelEvent::DomainEventEmitted { .. } => {
2212 self.counters.domain_event.fetch_add(1, Ordering::SeqCst);
2213 }
2214 KernelEvent::EffectFailed { .. } => {
2215 self.counters.effect_failed.fetch_add(1, Ordering::SeqCst);
2216 }
2217 KernelEvent::ObserverPanic { .. }
2218 | KernelEvent::ObserverEvicted { .. }
2219 | KernelEvent::SignalDropped { .. }
2220 | KernelEvent::SignalDelivered { .. }
2221 | KernelEvent::ModuleForceUnloaded { .. }
2222 | KernelEvent::ActionDeferredToNextTick { .. }
2223 | KernelEvent::ObserversFlushed { .. } => {
2224 self.counters.other.fetch_add(1, Ordering::SeqCst);
2225 }
2226 }
2227 }
2228 }
2229
2230 #[test]
2231 fn event_mask_default_is_all() {
2232 let m = EventMask::default();
2233 assert_eq!(m, EventMask::ALL);
2234 assert!(m.contains(EventMask::ACTION_EXECUTED));
2235 assert!(m.contains(EventMask::DOMAIN_EVENT_EMITTED));
2236 assert!(m.contains(EventMask::MODULE_FORCE_UNLOADED));
2237 }
2238
2239 #[test]
2240 fn register_observer_backward_compat_receives_all() {
2241 let mut k = Kernel::new();
2244 k.register_action::<EmitAction>();
2245 let counters = Arc::new(VariantCounters::default());
2246 k.register_observer(Box::new(VariantTallyObserver {
2247 counters: counters.clone(),
2248 }));
2249 let inst = k.create_instance(InstanceConfig::default());
2250 k.submit(
2251 inst,
2252 Principal::System,
2253 None,
2254 CapabilityMask::SYSTEM,
2255 Tick(0),
2256 TypeCode(101),
2257 Vec::new(),
2258 )
2259 .unwrap();
2260 let _ = k.step(Tick(0), CapabilityMask::SYSTEM);
2261 assert_eq!(counters.action_executed.load(Ordering::SeqCst), 1);
2262 assert_eq!(counters.domain_event.load(Ordering::SeqCst), 1);
2263 }
2264
2265 #[test]
2266 fn filter_only_action_executed() {
2267 let mut k = Kernel::new();
2269 k.register_action::<EmitAction>();
2270 let counters = Arc::new(VariantCounters::default());
2271 k.register_observer_filtered(
2272 Box::new(VariantTallyObserver {
2273 counters: counters.clone(),
2274 }),
2275 EventMask::ACTION_EXECUTED,
2276 );
2277 let inst = k.create_instance(InstanceConfig::default());
2278 k.submit(
2279 inst,
2280 Principal::System,
2281 None,
2282 CapabilityMask::SYSTEM,
2283 Tick(0),
2284 TypeCode(101),
2285 Vec::new(),
2286 )
2287 .unwrap();
2288 let _ = k.step(Tick(0), CapabilityMask::SYSTEM);
2289 assert_eq!(counters.action_executed.load(Ordering::SeqCst), 1);
2290 assert_eq!(counters.domain_event.load(Ordering::SeqCst), 0);
2291 }
2292
2293 #[test]
2294 fn filter_domain_event_only() {
2295 let mut k = Kernel::new();
2297 k.register_action::<EmitAction>();
2298 let counters = Arc::new(VariantCounters::default());
2299 k.register_observer_filtered(
2300 Box::new(VariantTallyObserver {
2301 counters: counters.clone(),
2302 }),
2303 EventMask::DOMAIN_EVENT_EMITTED,
2304 );
2305 let inst = k.create_instance(InstanceConfig::default());
2306 k.submit(
2307 inst,
2308 Principal::System,
2309 None,
2310 CapabilityMask::SYSTEM,
2311 Tick(0),
2312 TypeCode(101),
2313 Vec::new(),
2314 )
2315 .unwrap();
2316 let _ = k.step(Tick(0), CapabilityMask::SYSTEM);
2317 assert_eq!(counters.action_executed.load(Ordering::SeqCst), 0);
2318 assert_eq!(counters.domain_event.load(Ordering::SeqCst), 1);
2319 }
2320
2321 #[test]
2322 fn multiple_observers_independent_masks() {
2323 let mut k = Kernel::new();
2326 k.register_action::<EmitAction>();
2327 let ca = Arc::new(VariantCounters::default());
2328 let cb = Arc::new(VariantCounters::default());
2329 k.register_observer_filtered(
2330 Box::new(VariantTallyObserver {
2331 counters: ca.clone(),
2332 }),
2333 EventMask::ACTION_EXECUTED,
2334 );
2335 k.register_observer_filtered(
2336 Box::new(VariantTallyObserver {
2337 counters: cb.clone(),
2338 }),
2339 EventMask::DOMAIN_EVENT_EMITTED,
2340 );
2341 let inst = k.create_instance(InstanceConfig::default());
2342 k.submit(
2343 inst,
2344 Principal::System,
2345 None,
2346 CapabilityMask::SYSTEM,
2347 Tick(0),
2348 TypeCode(101),
2349 Vec::new(),
2350 )
2351 .unwrap();
2352 let _ = k.step(Tick(0), CapabilityMask::SYSTEM);
2353 assert_eq!(ca.action_executed.load(Ordering::SeqCst), 1);
2354 assert_eq!(ca.domain_event.load(Ordering::SeqCst), 0);
2355 assert_eq!(cb.action_executed.load(Ordering::SeqCst), 0);
2356 assert_eq!(cb.domain_event.load(Ordering::SeqCst), 1);
2357 }
2358
2359 #[test]
2360 fn filter_empty_mask_receives_nothing() {
2361 let mut k = Kernel::new();
2363 k.register_action::<EmitAction>();
2364 let counters = Arc::new(VariantCounters::default());
2365 k.register_observer_filtered(
2366 Box::new(VariantTallyObserver {
2367 counters: counters.clone(),
2368 }),
2369 EventMask::empty(),
2370 );
2371 let inst = k.create_instance(InstanceConfig::default());
2372 k.submit(
2373 inst,
2374 Principal::System,
2375 None,
2376 CapabilityMask::SYSTEM,
2377 Tick(0),
2378 TypeCode(101),
2379 Vec::new(),
2380 )
2381 .unwrap();
2382 let _ = k.step(Tick(0), CapabilityMask::SYSTEM);
2383 assert_eq!(counters.action_executed.load(Ordering::SeqCst), 0);
2384 assert_eq!(counters.domain_event.load(Ordering::SeqCst), 0);
2385 assert_eq!(counters.action_failed.load(Ordering::SeqCst), 0);
2386 assert_eq!(counters.effect_failed.load(Ordering::SeqCst), 0);
2387 assert_eq!(counters.other.load(Ordering::SeqCst), 0);
2388 }
2389}