use arkhe_kernel::abi::{CapabilityMask, EntityId, InstanceId, Principal, Tick};
use arkhe_kernel::state::{ActionContext as KernelActionContext, Op};
use crate::action::ActionCompute;
use crate::actor::ActorId;
use crate::context::ActionContext;
pub fn kernel_compute<A>(action: &A, kernel_ctx: &KernelActionContext<'_>) -> Vec<Op>
where
A: ActionCompute,
{
kernel_compute_inner(
action,
kernel_ctx.instance_id,
kernel_ctx.now,
kernel_ctx.actor,
)
}
fn kernel_compute_inner<A>(
action: &A,
instance_id: InstanceId,
now: Tick,
actor: Option<EntityId>,
) -> Vec<Op>
where
A: ActionCompute,
{
let mut forge_ctx = ActionContext::new(
[0u8; 32],
instance_id,
now,
Principal::System,
CapabilityMask::SYSTEM,
)
.with_actor(actor.map(ActorId::new));
if <A as ActionCompute>::compute(action, &mut forge_ctx).is_ok() {
forge_ctx.drain_ops()
} else {
Vec::new()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
use arkhe_kernel::abi::{EntityId, TypeCode};
use crate::component::ArkheComponent as _;
use crate::event::ArkheEvent as _;
use crate::event::UserErasureScheduled;
use crate::user::{
AuthCredential, AuthKind, GdprEraseUser, GdprStatus, KdfKind, KdfParams, RegisterUser,
UserGdprState, UserId, UserProfile,
};
fn fixture_args() -> (InstanceId, Tick) {
(InstanceId::new(7).unwrap(), Tick(99))
}
#[test]
fn ok_compute_returns_drained_ops() {
let (iid, tick) = fixture_args();
let target = EntityId::new(42).unwrap();
let action = GdprEraseUser {
schema_version: 1,
user: UserId::new(target),
};
let ops = kernel_compute_inner(&action, iid, tick, None);
assert_eq!(
ops.len(),
2,
"GdprEraseUser writes UserGdprState then emits the schedule event",
);
match &ops[0] {
Op::SetComponent {
entity,
type_code,
bytes,
..
} => {
assert_eq!(*entity, target);
assert_eq!(*type_code, TypeCode(UserGdprState::TYPE_CODE));
let state: UserGdprState = postcard::from_bytes(bytes).unwrap();
assert_eq!(state.status, GdprStatus::ErasurePending);
}
other => panic!("expected SetComponent(UserGdprState), got {:?}", other),
}
match &ops[1] {
Op::EmitEvent {
actor,
event_type_code,
event_bytes: _,
} => {
assert!(actor.is_none());
assert_eq!(*event_type_code, TypeCode(UserErasureScheduled::TYPE_CODE));
}
other => panic!("expected EmitEvent, got {:?}", other),
}
}
#[test]
fn err_compute_returns_empty_vec() {
let (iid, tick) = fixture_args();
let action = RegisterUser {
schema_version: 1,
profile: UserProfile {
schema_version: 1,
created_tick: Tick(0),
primary_auth_kind: AuthKind::Passkey,
},
credential: AuthCredential {
schema_version: 1,
kind: AuthKind::Passkey,
kdf: KdfKind::Argon2id,
salt: [0u8; 16],
credential_hash: [0u8; 32],
kdf_params: KdfParams {
m_cost: 1024,
t_cost: 1,
p_cost: 1,
},
expires_tick: None,
bound_tick: Tick(0),
},
};
let ops = kernel_compute_inner(&action, iid, tick, None);
assert!(
ops.is_empty(),
"weak-KDF RegisterUser must collapse to empty Op vec",
);
}
#[test]
fn injected_actor_reaches_compute_as_activity_author() {
use crate::activity::{
canonical_verbs, ActivityDraft, ActivityRecord, ActivityStatus, SubmitActivity,
TargetKind, VerbCode,
};
use crate::brand::ShellId;
use crate::entry::EntryId;
use bytes::Bytes;
let (iid, tick) = fixture_args();
let injected = EntityId::new(0xAC).unwrap();
let action = SubmitActivity {
schema_version: 1,
draft: ActivityDraft {
schema_version: 1,
shell_id: ShellId([0u8; 16]),
verb: VerbCode::canonical(canonical_verbs::LIKE),
target: TargetKind::Entry(EntryId::new(EntityId::new(2).unwrap())),
at_tick: tick,
status: ActivityStatus::Active,
extra_bytes: Bytes::new(),
},
idempotency_key: None,
};
let ops = kernel_compute_inner(&action, iid, tick, Some(injected));
assert_eq!(ops.len(), 2, "submit emits spawn + set");
let recorded = ops.iter().find_map(|op| match op {
Op::SetComponent {
type_code, bytes, ..
} if *type_code == TypeCode(ActivityRecord::TYPE_CODE) => {
postcard::from_bytes::<ActivityRecord>(bytes).ok()
}
_ => None,
});
let recorded = recorded.expect("ActivityRecord SetComponent present");
assert_eq!(
recorded.actor.get(),
injected,
"recorded activity author must equal the injected kernel actor",
);
}
#[test]
fn user_scoped_compute_without_injected_actor_is_suppressed() {
use crate::activity::{
canonical_verbs, ActivityDraft, ActivityStatus, SubmitActivity, TargetKind, VerbCode,
};
use crate::brand::ShellId;
use crate::entry::EntryId;
use bytes::Bytes;
let (iid, tick) = fixture_args();
let action = SubmitActivity {
schema_version: 1,
draft: ActivityDraft {
schema_version: 1,
shell_id: ShellId([0u8; 16]),
verb: VerbCode::canonical(canonical_verbs::LIKE),
target: TargetKind::Entry(EntryId::new(EntityId::new(2).unwrap())),
at_tick: tick,
status: ActivityStatus::Active,
extra_bytes: Bytes::new(),
},
idempotency_key: None,
};
let ops = kernel_compute_inner(&action, iid, tick, None);
assert!(
ops.is_empty(),
"user-scoped compute without an injected actor must produce no Ops",
);
}
#[test]
fn determinism_same_input_same_ops() {
let (iid, tick) = fixture_args();
let action = GdprEraseUser {
schema_version: 1,
user: UserId::new(EntityId::new(101).unwrap()),
};
let a = kernel_compute_inner(&action, iid, tick, None);
let b = kernel_compute_inner(&action, iid, tick, None);
assert_eq!(a.len(), b.len(), "Op count must match");
for (op_a, op_b) in a.iter().zip(b.iter()) {
let bytes_a = postcard::to_allocvec(op_a).expect("encode Op a");
let bytes_b = postcard::to_allocvec(op_b).expect("encode Op b");
assert_eq!(
bytes_a, bytes_b,
"bridge output must be byte-identical across runs",
);
}
}
}