1use std::collections::BTreeMap;
8
9use crate::abi::{ArkheError, CapabilityMask, EntityId, InstanceId, Principal, Tick, TypeCode};
10use crate::state::authz::authorize;
11use crate::state::{
12 Action, ActionContext, Effect, Instance, InstanceConfig, ScheduledActionId, Unverified,
13};
14
15use super::apply::{apply_stage, discard_stage};
16use super::dispatch::dispatch;
17use super::event::{KernelEvent, ObserverHandle};
18use super::observer::{KernelObserver, ObserverRegistry};
19use super::registry::ActionRegistry;
20use super::stage::StepStage;
21
22use crate::persist::{AuthDecisionAnnotation, Wal, WalWriter};
23
24pub struct Kernel {
36 instances: BTreeMap<InstanceId, Instance>,
37 action_registry: ActionRegistry,
38 observers: ObserverRegistry,
39 next_instance_id: u64,
40 wal: Option<WalWriter>,
41}
42
43#[derive(Debug, Clone, Default, PartialEq, Eq)]
45pub struct StepReport {
46 pub actions_executed: u32,
48 pub effects_applied: u32,
50 pub effects_denied: u32,
52 pub observers_evicted: u32,
54 pub domain_events_emitted: u32,
56}
57
58#[derive(Debug, Clone, Default, PartialEq, Eq)]
60pub struct Stats {
61 pub instance_count: usize,
63 pub scheduled_action_count: usize,
65 pub entity_count: u32,
67 pub component_byte_count: u64,
69 pub observer_count: usize,
71 pub wal_record_count: usize,
73}
74
75impl Default for Kernel {
76 fn default() -> Self {
77 Self::new()
78 }
79}
80
81impl Kernel {
82 pub fn new() -> Self {
87 Self {
88 instances: BTreeMap::new(),
89 action_registry: ActionRegistry::new(),
90 observers: ObserverRegistry::new(),
91 next_instance_id: 0,
92 wal: None,
93 }
94 }
95
96 pub fn new_with_wal(world_id: [u8; 32], manifest_digest: [u8; 32]) -> Self {
99 Self {
100 instances: BTreeMap::new(),
101 action_registry: ActionRegistry::new(),
102 observers: ObserverRegistry::new(),
103 next_instance_id: 0,
104 wal: Some(WalWriter::new(world_id, manifest_digest)),
105 }
106 }
107
108 pub fn new_with_wal_signed(
113 world_id: [u8; 32],
114 manifest_digest: [u8; 32],
115 sig_class: crate::persist::SignatureClass,
116 ) -> Self {
117 Self {
118 instances: BTreeMap::new(),
119 action_registry: ActionRegistry::new(),
120 observers: ObserverRegistry::new(),
121 next_instance_id: 0,
122 wal: Some(WalWriter::with_signature(
123 world_id,
124 manifest_digest,
125 sig_class,
126 )),
127 }
128 }
129
130 pub fn wal_chain_tip(&self) -> Option<[u8; 32]> {
132 self.wal.as_ref().map(|w| w.chain_tip())
133 }
134
135 pub fn wal_record_count(&self) -> Option<usize> {
137 self.wal.as_ref().map(|w| w.record_count())
138 }
139
140 pub fn export_wal(self) -> Option<Wal> {
142 self.wal.map(Wal::from_writer)
143 }
144
145 pub fn register_action<A: Action>(&mut self) {
149 self.action_registry.register::<A>();
150 }
151
152 pub fn register_observer(&mut self, obs: Box<dyn KernelObserver>) -> ObserverHandle {
156 self.observers.register(obs)
157 }
158
159 pub fn register_observer_filtered(
165 &mut self,
166 obs: Box<dyn KernelObserver>,
167 mask: super::event::EventMask,
168 ) -> ObserverHandle {
169 self.observers.register_filtered(obs, mask)
170 }
171
172 pub fn create_instance(&mut self, config: InstanceConfig) -> InstanceId {
175 self.next_instance_id = self.next_instance_id.saturating_add(1);
176 let id = InstanceId::new(self.next_instance_id).expect("instance id > 0");
177 self.instances.insert(id, Instance::new(id, config));
178 id
179 }
180
181 pub fn instances_len(&self) -> usize {
183 self.instances.len()
184 }
185
186 pub fn instance_view(&self, id: InstanceId) -> Option<super::view::InstanceView<'_>> {
190 self.instances
191 .get(&id)
192 .map(|instance| super::view::InstanceView { instance })
193 }
194
195 pub fn snapshot(&self) -> crate::persist::KernelSnapshot {
200 let instances = self
201 .instances
202 .iter()
203 .map(|(id, inst)| (*id, inst.to_snapshot()))
204 .collect();
205 crate::persist::KernelSnapshot::__construct(instances, self.next_instance_id)
206 }
207
208 pub fn from_snapshot(snap: crate::persist::KernelSnapshot) -> Self {
212 let (instances_in, next_instance_id) = snap.__into_parts();
213 let instances = instances_in
214 .into_iter()
215 .map(|(id, s)| (id, Instance::from_snapshot(s)))
216 .collect();
217 Self {
218 instances,
219 action_registry: ActionRegistry::new(),
220 observers: ObserverRegistry::new(),
221 next_instance_id,
222 wal: None,
223 }
224 }
225
226 pub fn stats(&self) -> Stats {
228 let mut scheduled = 0usize;
229 let mut entities = 0u32;
230 let mut bytes = 0u64;
231 for inst in self.instances.values() {
232 scheduled = scheduled.saturating_add(inst.scheduler().len());
233 entities = entities.saturating_add(inst.ledger().total_entities());
234 bytes = bytes.saturating_add(inst.ledger().total_bytes());
235 }
236 Stats {
237 instance_count: self.instances.len(),
238 scheduled_action_count: scheduled,
239 entity_count: entities,
240 component_byte_count: bytes,
241 observer_count: self.observers.len(),
242 wal_record_count: self.wal.as_ref().map(|w| w.record_count()).unwrap_or(0),
243 }
244 }
245
246 pub fn force_unload(
253 &mut self,
254 route_id: crate::abi::RouteId,
255 caps: CapabilityMask,
256 ) -> Result<usize, ArkheError> {
257 if !caps.contains(CapabilityMask::ADMIN_UNLOAD) {
258 return Err(ArkheError::CapabilityDenied);
259 }
260
261 let mut total_live_refs: u32 = 0;
262 for inst in self.instances.values_mut() {
263 if let Some(refs) = inst.inflight_refs_mut().remove(&route_id) {
264 total_live_refs = total_live_refs.saturating_add(refs);
265 }
266 }
267
268 let event = KernelEvent::ModuleForceUnloaded {
269 route_id,
270 live_refs_at_unload: total_live_refs,
271 };
272 let _ = self.observers.deliver(&event);
273
274 Ok(total_live_refs as usize)
275 }
276
277 pub fn submit(
286 &mut self,
287 instance: InstanceId,
288 principal: Principal,
289 actor: Option<EntityId>,
290 at: Tick,
291 action_type_code: TypeCode,
292 action_bytes: Vec<u8>,
293 ) -> Result<ScheduledActionId, ArkheError> {
294 let inst = self
295 .instances
296 .get_mut(&instance)
297 .ok_or(ArkheError::InstanceNotFound)?;
298 let max_scheduled = inst.config().max_scheduled;
302 if max_scheduled > 0 && inst.scheduler().len() >= max_scheduled as usize {
303 return Err(ArkheError::QuotaExceeded);
304 }
305 let counters = inst.id_counters_mut();
306 counters.next_scheduled = counters.next_scheduled.saturating_add(1);
307 let id = ScheduledActionId::new(counters.next_scheduled).expect("scheduled id > 0");
308 inst.scheduler_mut().schedule_with_id(
309 id,
310 at,
311 actor,
312 principal,
313 action_type_code,
314 action_bytes,
315 );
316 Ok(id)
317 }
318
319 pub fn step(&mut self, now: Tick, caps: CapabilityMask) -> StepReport {
322 let mut report = StepReport::default();
323
324 let instance_ids: Vec<InstanceId> = self.instances.keys().copied().collect();
325 for inst_id in instance_ids {
326 let entry = match self.instances.get_mut(&inst_id) {
327 Some(inst) => inst.scheduler_mut().pop_due(now),
328 None => continue,
329 };
330 let entry = match entry {
331 Some(e) => e,
332 None => continue,
333 };
334 report.actions_executed = report.actions_executed.saturating_add(1);
335
336 let reg = match self.action_registry.get(entry.action_type_code).cloned() {
337 Some(r) => r,
338 None => continue,
339 };
340
341 let action = match (reg.deserializer)(reg.schema_version, &entry.action_bytes) {
342 Ok(a) => a,
343 Err(_) => continue,
344 };
345
346 let inst_ref = self.instances.get(&inst_id).expect("instance present");
347 let ctx = ActionContext::new(entry.actor, now, inst_id, inst_ref);
348 let ops = action.compute_dyn(&ctx);
349
350 let mut stage = StepStage::default();
351 let mut next_scheduled_id = inst_ref.id_counters_snapshot().next_scheduled;
352 let budget = inst_ref.config().memory_budget_bytes;
353 let baseline_bytes: i64 =
354 i64::try_from(inst_ref.ledger().total_bytes()).unwrap_or(i64::MAX);
355 let mut any_denied = false;
356 for op in ops {
357 let principal_clone = match &entry.principal {
358 Principal::Unauthenticated => Principal::Unauthenticated,
359 Principal::External(e) => Principal::External(*e),
360 Principal::System => Principal::System,
361 };
362 let eff: Effect<'_, Unverified> = Effect::new(inst_id, principal_clone, op);
363 match authorize(caps, eff) {
364 Ok(authorized) => {
365 if budget > 0 {
370 let op_size: i64 = match &authorized.op {
374 crate::state::Op::SetComponent { size, .. } => {
375 i64::try_from(*size).unwrap_or(i64::MAX)
376 }
377 crate::state::Op::RemoveComponent { size, .. } => {
378 i64::try_from(*size).unwrap_or(i64::MAX).saturating_neg()
379 }
380 _ => 0,
381 };
382 let projected = baseline_bytes
383 .saturating_add(super::stage::bytes_delta(&stage))
384 .saturating_add(op_size);
385 if projected > i64::try_from(budget).unwrap_or(i64::MAX) {
386 report.effects_denied = report.effects_denied.saturating_add(1);
387 stage.events.push_back(KernelEvent::EffectFailed {
388 instance: inst_id,
389 reason: bytes::Bytes::from_static(b"budget_exceeded"),
390 });
391 continue;
392 }
393 }
394 let max_entities = inst_ref.config().max_entities;
399 if max_entities > 0
400 && matches!(&authorized.op, crate::state::Op::SpawnEntity { .. })
401 {
402 let projected_entities = (inst_ref.entities_len() as u64)
403 .saturating_add(stage.id_counters.next_entity_advance);
404 if projected_entities >= max_entities as u64 {
405 report.effects_denied = report.effects_denied.saturating_add(1);
406 stage.events.push_back(KernelEvent::EffectFailed {
407 instance: inst_id,
408 reason: bytes::Bytes::from_static(b"entity_quota_exceeded"),
409 });
410 continue;
411 }
412 }
413 let max_scheduled = inst_ref.config().max_scheduled;
417 if max_scheduled > 0
418 && matches!(&authorized.op, crate::state::Op::ScheduleAction { .. })
419 {
420 let projected_scheduled = (inst_ref.scheduler().len() as u64)
421 .saturating_add(stage.id_counters.next_scheduled_advance);
422 if projected_scheduled >= max_scheduled as u64 {
423 report.effects_denied = report.effects_denied.saturating_add(1);
424 stage.events.push_back(KernelEvent::EffectFailed {
425 instance: inst_id,
426 reason: bytes::Bytes::from_static(b"scheduled_quota_exceeded"),
427 });
428 continue;
429 }
430 }
431 dispatch(authorized, &mut stage, now, &mut next_scheduled_id);
432 report.effects_applied = report.effects_applied.saturating_add(1);
433 }
434 Err(_) => {
435 report.effects_denied = report.effects_denied.saturating_add(1);
436 any_denied = true;
437 }
438 }
439 }
440
441 if any_denied {
442 let inst_mut = self.instances.get_mut(&inst_id).expect("instance present");
443 discard_stage(inst_mut, stage);
444 continue;
445 }
446
447 let domain_emit_count = stage
450 .events
451 .iter()
452 .filter(|e| matches!(e, KernelEvent::DomainEventEmitted { .. }))
453 .count();
454 report.domain_events_emitted = report
455 .domain_events_emitted
456 .saturating_add(domain_emit_count as u32);
457 let events_to_deliver: Vec<KernelEvent> = stage.events.iter().cloned().collect();
458
459 let wal_stage = if self.wal.is_some() {
461 Some(stage.clone())
462 } else {
463 None
464 };
465 let principal_for_wal = match &entry.principal {
466 Principal::Unauthenticated => Principal::Unauthenticated,
467 Principal::External(e) => Principal::External(*e),
468 Principal::System => Principal::System,
469 };
470 let action_bytes_for_wal = entry.action_bytes.clone();
471 let action_type_for_wal = entry.action_type_code;
472 let actor_for_wal = entry.actor;
473
474 let inst_mut = self.instances.get_mut(&inst_id).expect("instance present");
475 apply_stage(inst_mut, stage);
476
477 if let (Some(wal), Some(s)) = (self.wal.as_mut(), wal_stage) {
478 let _ = wal.append(
479 now,
480 inst_id,
481 principal_for_wal,
482 actor_for_wal,
483 action_type_for_wal,
484 action_bytes_for_wal,
485 caps.bits(),
486 s,
487 AuthDecisionAnnotation::AllAuthorized,
488 );
489 }
490
491 for event in events_to_deliver {
492 let evicted = self.observers.deliver(&event);
493 report.observers_evicted = report
494 .observers_evicted
495 .saturating_add(evicted.len() as u32);
496 }
497
498 let action_executed = KernelEvent::ActionExecuted {
499 instance: inst_id,
500 action_type: entry.action_type_code,
501 at: now,
502 };
503 let evicted = self.observers.deliver(&action_executed);
504 report.observers_evicted = report
505 .observers_evicted
506 .saturating_add(evicted.len() as u32);
507 }
508
509 report
510 }
511}
512
513#[cfg(test)]
514mod tests {
515 use super::*;
516 use bytes::Bytes;
517 use std::sync::atomic::{AtomicU32, Ordering};
518 use std::sync::Arc;
519
520 use crate::abi::{ExternalId, RouteId};
521 use crate::state::traits::_sealed::Sealed;
522 use crate::state::{ActionCompute, ActionDeriv, Op};
523 use serde::{Deserialize, Serialize};
524
525 #[derive(Serialize, Deserialize)]
527 struct SpawnOneAction;
528 impl Sealed for SpawnOneAction {}
529 impl ActionDeriv for SpawnOneAction {
530 const TYPE_CODE: TypeCode = TypeCode(100);
531 const SCHEMA_VERSION: u32 = 1;
532 }
533 impl ActionCompute for SpawnOneAction {
534 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
535 vec![Op::SpawnEntity {
536 id: EntityId::new(42).unwrap(),
537 owner: Principal::System,
538 }]
539 }
540 }
541
542 #[derive(Serialize, Deserialize)]
543 struct EmitAction;
544 impl Sealed for EmitAction {}
545 impl ActionDeriv for EmitAction {
546 const TYPE_CODE: TypeCode = TypeCode(101);
547 const SCHEMA_VERSION: u32 = 1;
548 }
549 impl ActionCompute for EmitAction {
550 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
551 vec![Op::EmitEvent {
552 actor: None,
553 event_type_code: TypeCode(7),
554 event_bytes: Bytes::from_static(b"hello"),
555 }]
556 }
557 }
558
559 #[derive(Serialize, Deserialize)]
560 struct SignalAction;
561 impl Sealed for SignalAction {}
562 impl ActionDeriv for SignalAction {
563 const TYPE_CODE: TypeCode = TypeCode(102);
564 const SCHEMA_VERSION: u32 = 1;
565 }
566 impl ActionCompute for SignalAction {
567 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
568 vec![Op::SendSignal {
570 target: InstanceId::new(1).unwrap(),
571 route: RouteId(1),
572 payload: Bytes::new(),
573 }]
574 }
575 }
576
577 #[derive(Serialize, Deserialize)]
579 struct SpawnThreeAction;
580 impl Sealed for SpawnThreeAction {}
581 impl ActionDeriv for SpawnThreeAction {
582 const TYPE_CODE: TypeCode = TypeCode(103);
583 const SCHEMA_VERSION: u32 = 1;
584 }
585 impl ActionCompute for SpawnThreeAction {
586 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
587 (1..=3)
588 .map(|n| Op::SpawnEntity {
589 id: EntityId::new(n).unwrap(),
590 owner: Principal::System,
591 })
592 .collect()
593 }
594 }
595
596 #[derive(Serialize, Deserialize)]
599 struct OversizedSetAction;
600 impl Sealed for OversizedSetAction {}
601 impl ActionDeriv for OversizedSetAction {
602 const TYPE_CODE: TypeCode = TypeCode(104);
603 const SCHEMA_VERSION: u32 = 1;
604 }
605 impl ActionCompute for OversizedSetAction {
606 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
607 vec![Op::SetComponent {
608 entity: EntityId::new(1).unwrap(),
609 type_code: TypeCode(7),
610 bytes: Bytes::from_static(b"x"),
611 size: u64::MAX,
612 }]
613 }
614 }
615
616 struct CountingObserver {
617 count: Arc<AtomicU32>,
618 }
619 impl KernelObserver for CountingObserver {
620 fn on_event(&self, _event: &KernelEvent) {
621 self.count.fetch_add(1, Ordering::SeqCst);
622 }
623 }
624
625 struct PanicObserver;
626 impl KernelObserver for PanicObserver {
627 fn on_event(&self, _event: &KernelEvent) {
628 panic!("observer intentional panic");
629 }
630 }
631
632 #[test]
633 fn create_instance_returns_monotonic_ids() {
634 let mut k = Kernel::new();
635 let i1 = k.create_instance(InstanceConfig::default());
636 let i2 = k.create_instance(InstanceConfig::default());
637 let i3 = k.create_instance(InstanceConfig::default());
638 assert!(i1 < i2);
639 assert!(i2 < i3);
640 assert_eq!(i1.get(), 1);
641 assert_eq!(i3.get(), 3);
642 assert_eq!(k.instances_len(), 3);
643 }
644
645 #[test]
646 fn submit_unknown_instance_returns_error() {
647 let mut k = Kernel::new();
648 let bogus = InstanceId::new(99).unwrap();
649 let result = k.submit(
650 bogus,
651 Principal::System,
652 None,
653 Tick(0),
654 TypeCode(100),
655 Vec::new(),
656 );
657 assert!(matches!(result, Err(ArkheError::InstanceNotFound)));
658 }
659
660 #[test]
661 fn submit_back_pressures_at_max_scheduled() {
662 let mut k = Kernel::new();
665 let inst = k.create_instance(InstanceConfig {
666 max_scheduled: 2,
667 ..Default::default()
668 });
669 let sub = |k: &mut Kernel| {
670 k.submit(inst, Principal::System, None, Tick(0), TypeCode(100), Vec::new())
671 };
672 assert!(sub(&mut k).is_ok());
673 assert!(sub(&mut k).is_ok());
674 assert!(matches!(sub(&mut k), Err(ArkheError::QuotaExceeded)));
675 }
676
677 #[test]
678 fn step_denies_spawns_past_max_entities() {
679 let mut k = Kernel::new();
682 k.register_action::<SpawnThreeAction>();
683 let inst = k.create_instance(InstanceConfig {
684 max_entities: 2,
685 ..Default::default()
686 });
687 k.submit(inst, Principal::System, None, Tick(0), TypeCode(103), Vec::new())
688 .unwrap();
689 let report = k.step(Tick(0), CapabilityMask::SYSTEM);
690 assert_eq!(report.effects_applied, 2);
691 assert_eq!(report.effects_denied, 1);
692 assert_eq!(k.instances.get(&inst).unwrap().entities_len(), 2);
693 }
694
695 #[test]
696 fn step_denies_oversized_component_no_budget_bypass() {
697 let mut k = Kernel::new();
700 k.register_action::<OversizedSetAction>();
701 let inst = k.create_instance(InstanceConfig {
702 memory_budget_bytes: 1000,
703 ..Default::default()
704 });
705 k.submit(inst, Principal::System, None, Tick(0), TypeCode(104), Vec::new())
706 .unwrap();
707 let report = k.step(Tick(0), CapabilityMask::SYSTEM);
708 assert_eq!(report.effects_applied, 0);
709 assert_eq!(report.effects_denied, 1);
710 }
711
712 #[test]
713 fn submit_then_step_executes_action_and_spawns_entity() {
714 let mut k = Kernel::new();
715 k.register_action::<SpawnOneAction>();
716 let inst = k.create_instance(InstanceConfig::default());
717 k.submit(
718 inst,
719 Principal::System,
720 None,
721 Tick(0),
722 TypeCode(100),
723 Vec::new(),
724 )
725 .unwrap();
726
727 let report = k.step(Tick(5), CapabilityMask::SYSTEM);
728 assert_eq!(report.actions_executed, 1);
729 assert_eq!(report.effects_applied, 1);
730 assert_eq!(report.effects_denied, 0);
731 let inst_ref = k.instances.get(&inst).unwrap();
733 assert_eq!(inst_ref.entities_len(), 1);
734 }
735
736 #[test]
737 fn step_with_unknown_type_code_skips_action() {
738 let mut k = Kernel::new();
739 let inst = k.create_instance(InstanceConfig::default());
741 k.submit(
742 inst,
743 Principal::System,
744 None,
745 Tick(0),
746 TypeCode(999),
747 Vec::new(),
748 )
749 .unwrap();
750
751 let report = k.step(Tick(5), CapabilityMask::SYSTEM);
752 assert_eq!(report.actions_executed, 1);
753 assert_eq!(report.effects_applied, 0);
754 assert_eq!(k.instances.get(&inst).unwrap().entities_len(), 0);
755 }
756
757 #[test]
758 fn observer_receives_action_executed_event() {
759 let mut k = Kernel::new();
760 k.register_action::<SpawnOneAction>();
761 let count = Arc::new(AtomicU32::new(0));
762 let _h = k.register_observer(Box::new(CountingObserver {
763 count: count.clone(),
764 }));
765 let inst = k.create_instance(InstanceConfig::default());
766 k.submit(
767 inst,
768 Principal::System,
769 None,
770 Tick(0),
771 TypeCode(100),
772 Vec::new(),
773 )
774 .unwrap();
775 k.step(Tick(5), CapabilityMask::SYSTEM);
776 assert_eq!(count.load(Ordering::SeqCst), 1);
778 }
779
780 #[test]
781 fn observer_receives_domain_event_emitted() {
782 let mut k = Kernel::new();
783 k.register_action::<EmitAction>();
784 let count = Arc::new(AtomicU32::new(0));
785 k.register_observer(Box::new(CountingObserver {
786 count: count.clone(),
787 }));
788 let inst = k.create_instance(InstanceConfig::default());
789 k.submit(
790 inst,
791 Principal::System,
792 None,
793 Tick(0),
794 TypeCode(101),
795 Vec::new(),
796 )
797 .unwrap();
798 let report = k.step(Tick(5), CapabilityMask::SYSTEM);
799 assert_eq!(report.domain_events_emitted, 1);
800 assert_eq!(count.load(Ordering::SeqCst), 2);
802 }
803
804 #[test]
805 fn panic_observer_evicted_after_first_event() {
806 let mut k = Kernel::new();
807 k.register_action::<SpawnOneAction>();
808 let h = k.register_observer(Box::new(PanicObserver));
809 let inst = k.create_instance(InstanceConfig::default());
810 k.submit(
811 inst,
812 Principal::System,
813 None,
814 Tick(0),
815 TypeCode(100),
816 Vec::new(),
817 )
818 .unwrap();
819 let report = k.step(Tick(5), CapabilityMask::SYSTEM);
820 assert!(report.observers_evicted >= 1);
821 assert!(k.observers.is_evicted(h));
822 }
823
824 #[test]
825 fn unauthenticated_principal_denies_all_effects() {
826 let mut k = Kernel::new();
827 k.register_action::<SpawnOneAction>();
828 let inst = k.create_instance(InstanceConfig::default());
829 k.submit(
830 inst,
831 Principal::Unauthenticated,
832 None,
833 Tick(0),
834 TypeCode(100),
835 Vec::new(),
836 )
837 .unwrap();
838 let report = k.step(Tick(5), CapabilityMask::SYSTEM);
839 assert_eq!(report.effects_denied, 1);
840 assert_eq!(report.effects_applied, 0);
841 assert_eq!(k.instances.get(&inst).unwrap().entities_len(), 0);
843 }
844
845 #[test]
846 fn external_without_system_cap_denies_signal() {
847 let mut k = Kernel::new();
848 k.register_action::<SignalAction>();
849 let inst = k.create_instance(InstanceConfig::default());
850 k.submit(
851 inst,
852 Principal::External(ExternalId(7)),
853 None,
854 Tick(0),
855 TypeCode(102),
856 Vec::new(),
857 )
858 .unwrap();
859 let report = k.step(Tick(5), CapabilityMask::default());
860 assert_eq!(report.effects_denied, 1);
861 assert_eq!(report.effects_applied, 0);
862 }
863
864 #[test]
865 fn wal_attached_kernel_records_committed_step() {
866 let mut k = Kernel::new_with_wal([7u8; 32], [3u8; 32]);
867 k.register_action::<SpawnOneAction>();
868 let inst = k.create_instance(InstanceConfig::default());
869 k.submit(
870 inst,
871 Principal::System,
872 None,
873 Tick(0),
874 TypeCode(100),
875 Vec::new(),
876 )
877 .unwrap();
878 assert_eq!(k.wal_record_count(), Some(0));
879 let pre_tip = k.wal_chain_tip().unwrap();
880 k.step(Tick(5), CapabilityMask::SYSTEM);
881 assert_eq!(k.wal_record_count(), Some(1));
882 let post_tip = k.wal_chain_tip().unwrap();
883 assert_ne!(pre_tip, post_tip);
884 }
885
886 #[test]
887 fn wal_kernel_export_then_verify_chain() {
888 let mut k = Kernel::new_with_wal([11u8; 32], [0u8; 32]);
889 k.register_action::<SpawnOneAction>();
890 let inst = k.create_instance(InstanceConfig::default());
891 for _ in 0..3 {
892 k.submit(
893 inst,
894 Principal::System,
895 None,
896 Tick(0),
897 TypeCode(100),
898 Vec::new(),
899 )
900 .unwrap();
901 k.step(Tick(0), CapabilityMask::SYSTEM);
902 }
903 let wal = k.export_wal().expect("wal attached");
904 assert_eq!(wal.records.len(), 3);
905 wal.verify_chain([11u8; 32]).expect("chain verifies");
906 }
907
908 #[test]
909 fn replay_reconstructs_chain_tip() {
910 use crate::persist::replay_into;
911 let mut k1 = Kernel::new_with_wal([42u8; 32], [0u8; 32]);
913 k1.register_action::<SpawnOneAction>();
914 let i1 = k1.create_instance(InstanceConfig::default());
915 for _ in 0..4 {
916 k1.submit(
917 i1,
918 Principal::System,
919 None,
920 Tick(0),
921 TypeCode(100),
922 Vec::new(),
923 )
924 .unwrap();
925 k1.step(Tick(0), CapabilityMask::SYSTEM);
926 }
927 let original_tip = k1.wal_chain_tip().unwrap();
928 let wal = k1.export_wal().unwrap();
929
930 let mut k2 = Kernel::new_with_wal([42u8; 32], [0u8; 32]);
932 k2.register_action::<SpawnOneAction>();
933 let _i2 = k2.create_instance(InstanceConfig::default());
936 let report = replay_into(&mut k2, &wal).expect("replay ok");
937 assert_eq!(report.records_replayed, 4);
938 let replayed_tip = k2.wal_chain_tip().unwrap();
939 assert_eq!(replayed_tip, original_tip);
940 assert_eq!(report.final_chain_tip, original_tip);
941 }
942
943 #[test]
944 fn step_processes_instances_in_ascending_order() {
945 let mut k = Kernel::new();
949 k.register_action::<SpawnOneAction>();
950 let i1 = k.create_instance(InstanceConfig::default());
951 let i2 = k.create_instance(InstanceConfig::default());
952 k.submit(
953 i2,
954 Principal::System,
955 None,
956 Tick(0),
957 TypeCode(100),
958 Vec::new(),
959 )
960 .unwrap();
961 k.submit(
962 i1,
963 Principal::System,
964 None,
965 Tick(0),
966 TypeCode(100),
967 Vec::new(),
968 )
969 .unwrap();
970 let report = k.step(Tick(5), CapabilityMask::SYSTEM);
971 assert_eq!(report.actions_executed, 2);
972 assert_eq!(report.effects_applied, 2);
973 }
974
975 #[test]
976 fn stats_aggregate_reflects_instances_and_scheduler() {
977 let mut k = Kernel::new();
978 k.register_action::<SpawnOneAction>();
979 assert_eq!(k.stats(), Stats::default());
980
981 let i1 = k.create_instance(InstanceConfig::default());
982 let i2 = k.create_instance(InstanceConfig::default());
983 let stats_pre = k.stats();
984 assert_eq!(stats_pre.instance_count, 2);
985 assert_eq!(stats_pre.scheduled_action_count, 0);
986 assert_eq!(stats_pre.entity_count, 0);
987
988 k.submit(
989 i1,
990 Principal::System,
991 None,
992 Tick(0),
993 TypeCode(100),
994 Vec::new(),
995 )
996 .unwrap();
997 k.submit(
998 i2,
999 Principal::System,
1000 None,
1001 Tick(0),
1002 TypeCode(100),
1003 Vec::new(),
1004 )
1005 .unwrap();
1006 let stats_queued = k.stats();
1007 assert_eq!(stats_queued.scheduled_action_count, 2);
1008
1009 let _ = k.step(Tick(1), CapabilityMask::SYSTEM);
1010 let stats_post = k.stats();
1011 assert_eq!(stats_post.scheduled_action_count, 0);
1012 assert_eq!(stats_post.entity_count, 2);
1013 }
1014
1015 #[test]
1016 fn stats_counts_observers() {
1017 struct NullObs;
1018 impl KernelObserver for NullObs {
1019 fn on_event(&self, _e: &KernelEvent) {}
1020 }
1021
1022 let mut k = Kernel::new();
1023 k.register_observer(Box::new(NullObs));
1024 k.register_observer(Box::new(NullObs));
1025 assert_eq!(k.stats().observer_count, 2);
1026 }
1027
1028 #[test]
1029 fn stats_wal_record_count_reflects_writer() {
1030 let mut k = Kernel::new_with_wal([1u8; 32], [0u8; 32]);
1031 k.register_action::<SpawnOneAction>();
1032 assert_eq!(k.stats().wal_record_count, 0);
1033
1034 let i = k.create_instance(InstanceConfig::default());
1035 k.submit(
1036 i,
1037 Principal::System,
1038 None,
1039 Tick(0),
1040 TypeCode(100),
1041 Vec::new(),
1042 )
1043 .unwrap();
1044 let _ = k.step(Tick(1), CapabilityMask::SYSTEM);
1045 assert_eq!(k.stats().wal_record_count, 1);
1046 }
1047
1048 struct ForceUnloadCapture {
1052 seen: Arc<std::sync::Mutex<Vec<(RouteId, u32)>>>,
1053 }
1054 impl KernelObserver for ForceUnloadCapture {
1055 fn on_event(&self, event: &KernelEvent) {
1056 if let KernelEvent::ModuleForceUnloaded {
1057 route_id,
1058 live_refs_at_unload,
1059 } = event
1060 {
1061 self.seen
1062 .lock()
1063 .unwrap()
1064 .push((*route_id, *live_refs_at_unload));
1065 }
1066 }
1067 }
1068
1069 #[test]
1070 fn force_unload_without_cap_denied() {
1071 let mut k = Kernel::new();
1072 let result = k.force_unload(RouteId(1), CapabilityMask::default());
1073 assert!(matches!(result, Err(ArkheError::CapabilityDenied)));
1074 }
1075
1076 #[test]
1077 fn force_unload_removes_inflight_refs() {
1078 let mut k = Kernel::new();
1079 k.register_action::<SignalAction>();
1080 let inst = k.create_instance(InstanceConfig::default());
1081 k.submit(
1084 inst,
1085 Principal::System,
1086 None,
1087 Tick(0),
1088 TypeCode(102),
1089 Vec::new(),
1090 )
1091 .unwrap();
1092 let report = k.step(Tick(0), CapabilityMask::SYSTEM);
1093 assert_eq!(report.effects_applied, 1);
1094 assert_eq!(
1095 k.instances
1096 .get(&inst)
1097 .unwrap()
1098 .inflight_refs_for(RouteId(1)),
1099 1
1100 );
1101
1102 let dropped = k
1103 .force_unload(RouteId(1), CapabilityMask::ADMIN_UNLOAD)
1104 .expect("admin_unload caps");
1105 assert_eq!(dropped, 1);
1106 assert_eq!(
1107 k.instances
1108 .get(&inst)
1109 .unwrap()
1110 .inflight_refs_for(RouteId(1)),
1111 0
1112 );
1113 assert_eq!(k.instances.get(&inst).unwrap().inflight_refs_len(), 0);
1114 }
1115
1116 #[test]
1117 fn force_unload_emits_module_unloaded_event() {
1118 let mut k = Kernel::new();
1119 k.register_action::<SignalAction>();
1120 let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
1121 k.register_observer(Box::new(ForceUnloadCapture { seen: seen.clone() }));
1122 let inst = k.create_instance(InstanceConfig::default());
1123 k.submit(
1124 inst,
1125 Principal::System,
1126 None,
1127 Tick(0),
1128 TypeCode(102),
1129 Vec::new(),
1130 )
1131 .unwrap();
1132 let _ = k.step(Tick(0), CapabilityMask::SYSTEM);
1133
1134 k.force_unload(RouteId(1), CapabilityMask::ADMIN_UNLOAD)
1135 .expect("admin_unload caps");
1136
1137 let captured = seen.lock().unwrap().clone();
1138 assert_eq!(captured, vec![(RouteId(1), 1)]);
1139 }
1140
1141 #[test]
1142 fn force_unload_no_live_refs_returns_zero() {
1143 let mut k = Kernel::new();
1144 let _ = k.create_instance(InstanceConfig::default());
1145 let dropped = k
1146 .force_unload(RouteId(99), CapabilityMask::ADMIN_UNLOAD)
1147 .expect("admin_unload caps");
1148 assert_eq!(dropped, 0);
1149 }
1150
1151 #[derive(Serialize, Deserialize)]
1158 struct SetCompAction {
1159 size: u64,
1160 entity_id: u64,
1161 }
1162 impl Sealed for SetCompAction {}
1163 impl ActionDeriv for SetCompAction {
1164 const TYPE_CODE: TypeCode = TypeCode(200);
1165 const SCHEMA_VERSION: u32 = 1;
1166 }
1167 impl ActionCompute for SetCompAction {
1168 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
1169 let entity = EntityId::new(self.entity_id).unwrap();
1170 vec![
1171 Op::SpawnEntity {
1172 id: entity,
1173 owner: Principal::System,
1174 },
1175 Op::SetComponent {
1176 entity,
1177 type_code: TypeCode(7),
1178 bytes: Bytes::from(vec![0u8; self.size as usize]),
1179 size: self.size,
1180 },
1181 ]
1182 }
1183 }
1184
1185 #[derive(Serialize, Deserialize)]
1189 struct TwoSetCompAction {
1190 a: u64,
1191 b: u64,
1192 }
1193 impl Sealed for TwoSetCompAction {}
1194 impl ActionDeriv for TwoSetCompAction {
1195 const TYPE_CODE: TypeCode = TypeCode(201);
1196 const SCHEMA_VERSION: u32 = 1;
1197 }
1198 impl ActionCompute for TwoSetCompAction {
1199 fn compute(&self, _ctx: &ActionContext) -> Vec<Op> {
1200 let e1 = EntityId::new(1).unwrap();
1201 let e2 = EntityId::new(2).unwrap();
1202 vec![
1203 Op::SpawnEntity {
1204 id: e1,
1205 owner: Principal::System,
1206 },
1207 Op::SpawnEntity {
1208 id: e2,
1209 owner: Principal::System,
1210 },
1211 Op::SetComponent {
1212 entity: e1,
1213 type_code: TypeCode(7),
1214 bytes: Bytes::from(vec![0u8; self.a as usize]),
1215 size: self.a,
1216 },
1217 Op::SetComponent {
1218 entity: e2,
1219 type_code: TypeCode(7),
1220 bytes: Bytes::from(vec![0u8; self.b as usize]),
1221 size: self.b,
1222 },
1223 ]
1224 }
1225 }
1226
1227 fn cfg_with_budget(budget: u64) -> InstanceConfig {
1228 InstanceConfig {
1229 memory_budget_bytes: budget,
1230 ..Default::default()
1231 }
1232 }
1233
1234 fn submit_set(k: &mut Kernel, inst: InstanceId, size: u64, entity_id: u64) {
1235 let action = SetCompAction { size, entity_id };
1236 let bytes = Action::canonical_bytes(&action);
1237 k.submit(
1238 inst,
1239 Principal::System,
1240 None,
1241 Tick(0),
1242 SetCompAction::TYPE_CODE,
1243 bytes,
1244 )
1245 .expect("submit ok");
1246 }
1247
1248 #[test]
1249 fn budget_zero_allows_unlimited() {
1250 let mut k = Kernel::new();
1253 k.register_action::<SetCompAction>();
1254 let inst = k.create_instance(InstanceConfig::default());
1255 submit_set(&mut k, inst, 1_000_000, 1);
1256 let report = k.step(Tick(0), CapabilityMask::SYSTEM);
1257 assert_eq!(report.effects_applied, 2);
1258 assert_eq!(report.effects_denied, 0);
1259 assert_eq!(k.instances.get(&inst).unwrap().components_len(), 1);
1260 }
1261
1262 #[test]
1263 fn budget_exceeded_denies_op() {
1264 let mut k = Kernel::new();
1267 k.register_action::<SetCompAction>();
1268 let inst = k.create_instance(cfg_with_budget(100));
1269 submit_set(&mut k, inst, 500, 1);
1270 let report = k.step(Tick(0), CapabilityMask::SYSTEM);
1271 assert_eq!(report.effects_applied, 1); assert_eq!(report.effects_denied, 1); assert_eq!(report.actions_executed, 1);
1274 assert_eq!(k.instances.get(&inst).unwrap().entities_len(), 1);
1275 assert_eq!(k.instances.get(&inst).unwrap().components_len(), 0);
1276 }
1277
1278 #[test]
1279 fn budget_at_edge_allows_equal() {
1280 let mut k = Kernel::new();
1283 k.register_action::<SetCompAction>();
1284 let inst = k.create_instance(cfg_with_budget(500));
1285 submit_set(&mut k, inst, 500, 1);
1286 let report = k.step(Tick(0), CapabilityMask::SYSTEM);
1287 assert_eq!(report.effects_applied, 2);
1288 assert_eq!(report.effects_denied, 0);
1289 assert_eq!(k.instances.get(&inst).unwrap().components_len(), 1);
1290 assert_eq!(k.instances.get(&inst).unwrap().ledger().total_bytes(), 500);
1291 }
1292
1293 #[test]
1294 fn multi_op_stage_respects_running_delta() {
1295 let mut k = Kernel::new();
1301 k.register_action::<TwoSetCompAction>();
1302 let inst = k.create_instance(cfg_with_budget(600));
1303 let action = TwoSetCompAction { a: 400, b: 400 };
1304 let bytes = Action::canonical_bytes(&action);
1305 k.submit(
1306 inst,
1307 Principal::System,
1308 None,
1309 Tick(0),
1310 TwoSetCompAction::TYPE_CODE,
1311 bytes,
1312 )
1313 .unwrap();
1314 let report = k.step(Tick(0), CapabilityMask::SYSTEM);
1315 assert_eq!(report.effects_applied, 3);
1316 assert_eq!(report.effects_denied, 1);
1317 assert_eq!(k.instances.get(&inst).unwrap().entities_len(), 2);
1318 assert_eq!(k.instances.get(&inst).unwrap().components_len(), 1);
1319 assert_eq!(k.instances.get(&inst).unwrap().ledger().total_bytes(), 400);
1320 }
1321
1322 struct EffectFailedCapture {
1324 seen: Arc<std::sync::Mutex<Vec<Bytes>>>,
1325 }
1326 impl KernelObserver for EffectFailedCapture {
1327 fn on_event(&self, event: &KernelEvent) {
1328 if let KernelEvent::EffectFailed { reason, .. } = event {
1329 self.seen.lock().unwrap().push(reason.clone());
1330 }
1331 }
1332 }
1333
1334 #[test]
1335 fn effect_failed_event_on_budget_deny() {
1336 let mut k = Kernel::new();
1337 k.register_action::<SetCompAction>();
1338 let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
1339 k.register_observer(Box::new(EffectFailedCapture { seen: seen.clone() }));
1340 let inst = k.create_instance(cfg_with_budget(100));
1341 submit_set(&mut k, inst, 500, 1);
1342 let _ = k.step(Tick(0), CapabilityMask::SYSTEM);
1343
1344 let captured = seen.lock().unwrap().clone();
1345 assert_eq!(captured.len(), 1);
1346 assert_eq!(captured[0].as_ref(), b"budget_exceeded");
1347 }
1348
1349 use crate::runtime::event::EventMask;
1352
1353 #[derive(Default)]
1356 struct VariantCounters {
1357 action_executed: AtomicU32,
1358 action_failed: AtomicU32,
1359 domain_event: AtomicU32,
1360 effect_failed: AtomicU32,
1361 other: AtomicU32,
1362 }
1363
1364 struct VariantTallyObserver {
1365 counters: Arc<VariantCounters>,
1366 }
1367 impl KernelObserver for VariantTallyObserver {
1368 fn on_event(&self, event: &KernelEvent) {
1369 match event {
1370 KernelEvent::ActionExecuted { .. } => {
1371 self.counters.action_executed.fetch_add(1, Ordering::SeqCst);
1372 }
1373 KernelEvent::ActionFailed { .. } => {
1374 self.counters.action_failed.fetch_add(1, Ordering::SeqCst);
1375 }
1376 KernelEvent::DomainEventEmitted { .. } => {
1377 self.counters.domain_event.fetch_add(1, Ordering::SeqCst);
1378 }
1379 KernelEvent::EffectFailed { .. } => {
1380 self.counters.effect_failed.fetch_add(1, Ordering::SeqCst);
1381 }
1382 KernelEvent::ObserverPanic { .. }
1383 | KernelEvent::ObserverEvicted { .. }
1384 | KernelEvent::SignalDropped { .. }
1385 | KernelEvent::ModuleForceUnloaded { .. }
1386 | KernelEvent::ActionDeferredToNextTick { .. }
1387 | KernelEvent::ObserversFlushed { .. } => {
1388 self.counters.other.fetch_add(1, Ordering::SeqCst);
1389 }
1390 }
1391 }
1392 }
1393
1394 #[test]
1395 fn event_mask_default_is_all() {
1396 let m = EventMask::default();
1397 assert_eq!(m, EventMask::ALL);
1398 assert!(m.contains(EventMask::ACTION_EXECUTED));
1399 assert!(m.contains(EventMask::DOMAIN_EVENT_EMITTED));
1400 assert!(m.contains(EventMask::MODULE_FORCE_UNLOADED));
1401 }
1402
1403 #[test]
1404 fn register_observer_backward_compat_receives_all() {
1405 let mut k = Kernel::new();
1408 k.register_action::<EmitAction>();
1409 let counters = Arc::new(VariantCounters::default());
1410 k.register_observer(Box::new(VariantTallyObserver {
1411 counters: counters.clone(),
1412 }));
1413 let inst = k.create_instance(InstanceConfig::default());
1414 k.submit(
1415 inst,
1416 Principal::System,
1417 None,
1418 Tick(0),
1419 TypeCode(101),
1420 Vec::new(),
1421 )
1422 .unwrap();
1423 let _ = k.step(Tick(0), CapabilityMask::SYSTEM);
1424 assert_eq!(counters.action_executed.load(Ordering::SeqCst), 1);
1425 assert_eq!(counters.domain_event.load(Ordering::SeqCst), 1);
1426 }
1427
1428 #[test]
1429 fn filter_only_action_executed() {
1430 let mut k = Kernel::new();
1432 k.register_action::<EmitAction>();
1433 let counters = Arc::new(VariantCounters::default());
1434 k.register_observer_filtered(
1435 Box::new(VariantTallyObserver {
1436 counters: counters.clone(),
1437 }),
1438 EventMask::ACTION_EXECUTED,
1439 );
1440 let inst = k.create_instance(InstanceConfig::default());
1441 k.submit(
1442 inst,
1443 Principal::System,
1444 None,
1445 Tick(0),
1446 TypeCode(101),
1447 Vec::new(),
1448 )
1449 .unwrap();
1450 let _ = k.step(Tick(0), CapabilityMask::SYSTEM);
1451 assert_eq!(counters.action_executed.load(Ordering::SeqCst), 1);
1452 assert_eq!(counters.domain_event.load(Ordering::SeqCst), 0);
1453 }
1454
1455 #[test]
1456 fn filter_domain_event_only() {
1457 let mut k = Kernel::new();
1459 k.register_action::<EmitAction>();
1460 let counters = Arc::new(VariantCounters::default());
1461 k.register_observer_filtered(
1462 Box::new(VariantTallyObserver {
1463 counters: counters.clone(),
1464 }),
1465 EventMask::DOMAIN_EVENT_EMITTED,
1466 );
1467 let inst = k.create_instance(InstanceConfig::default());
1468 k.submit(
1469 inst,
1470 Principal::System,
1471 None,
1472 Tick(0),
1473 TypeCode(101),
1474 Vec::new(),
1475 )
1476 .unwrap();
1477 let _ = k.step(Tick(0), CapabilityMask::SYSTEM);
1478 assert_eq!(counters.action_executed.load(Ordering::SeqCst), 0);
1479 assert_eq!(counters.domain_event.load(Ordering::SeqCst), 1);
1480 }
1481
1482 #[test]
1483 fn multiple_observers_independent_masks() {
1484 let mut k = Kernel::new();
1487 k.register_action::<EmitAction>();
1488 let ca = Arc::new(VariantCounters::default());
1489 let cb = Arc::new(VariantCounters::default());
1490 k.register_observer_filtered(
1491 Box::new(VariantTallyObserver {
1492 counters: ca.clone(),
1493 }),
1494 EventMask::ACTION_EXECUTED,
1495 );
1496 k.register_observer_filtered(
1497 Box::new(VariantTallyObserver {
1498 counters: cb.clone(),
1499 }),
1500 EventMask::DOMAIN_EVENT_EMITTED,
1501 );
1502 let inst = k.create_instance(InstanceConfig::default());
1503 k.submit(
1504 inst,
1505 Principal::System,
1506 None,
1507 Tick(0),
1508 TypeCode(101),
1509 Vec::new(),
1510 )
1511 .unwrap();
1512 let _ = k.step(Tick(0), CapabilityMask::SYSTEM);
1513 assert_eq!(ca.action_executed.load(Ordering::SeqCst), 1);
1514 assert_eq!(ca.domain_event.load(Ordering::SeqCst), 0);
1515 assert_eq!(cb.action_executed.load(Ordering::SeqCst), 0);
1516 assert_eq!(cb.domain_event.load(Ordering::SeqCst), 1);
1517 }
1518
1519 #[test]
1520 fn filter_empty_mask_receives_nothing() {
1521 let mut k = Kernel::new();
1523 k.register_action::<EmitAction>();
1524 let counters = Arc::new(VariantCounters::default());
1525 k.register_observer_filtered(
1526 Box::new(VariantTallyObserver {
1527 counters: counters.clone(),
1528 }),
1529 EventMask::empty(),
1530 );
1531 let inst = k.create_instance(InstanceConfig::default());
1532 k.submit(
1533 inst,
1534 Principal::System,
1535 None,
1536 Tick(0),
1537 TypeCode(101),
1538 Vec::new(),
1539 )
1540 .unwrap();
1541 let _ = k.step(Tick(0), CapabilityMask::SYSTEM);
1542 assert_eq!(counters.action_executed.load(Ordering::SeqCst), 0);
1543 assert_eq!(counters.domain_event.load(Ordering::SeqCst), 0);
1544 assert_eq!(counters.action_failed.load(Ordering::SeqCst), 0);
1545 assert_eq!(counters.effect_failed.load(Ordering::SeqCst), 0);
1546 assert_eq!(counters.other.load(Ordering::SeqCst), 0);
1547 }
1548}