use core::num::NonZeroU8;
use embassy_time::{Duration, Instant};
use crate::acl::AccessReq;
use crate::crypto::{CanonAeadKey, Crypto};
use crate::dm::endpoints::ROOT_ENDPOINT_ID;
use crate::dm::{
Access, ArrayAttributeRead, Cluster, Dataver, HandlerContext, InvokeContext, LifecycleOp,
ReadContext,
};
use crate::error::{Error, ErrorCode};
use crate::fabric::MAX_FABRICS;
use crate::im::encoding::GenericPath;
use crate::persist::{KvBlobStore, Persist, ICD_REGISTERED_CLIENTS_KEY};
use crate::sc::checkin::{CheckIn, CheckInCounter};
use crate::tlv::{FromTLV, TLVBuilderParent, TLVElement, ToTLV};
use crate::utils::cell::RefCell;
use crate::utils::init::{init, Init};
use crate::utils::storage::Vec;
use crate::utils::sync::blocking::Mutex;
use crate::utils::sync::Notification;
use crate::with;
use crate::Matter;
pub use crate::dm::clusters::decl::icd_management::*;
pub const CLIENTS_PER_FABRIC: usize = 2;
pub const MAX_REGISTERED_CLIENTS: usize = CLIENTS_PER_FABRIC * MAX_FABRICS;
pub const STAY_ACTIVE_MAX_MS: u32 = 30_000;
#[derive(Debug, Clone, FromTLV, ToTLV)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct MonitoringRegistration {
pub fab_idx: NonZeroU8,
pub check_in_node_id: u64,
pub monitored_subject: u64,
pub client_type: ClientTypeEnum,
pub key: CanonAeadKey,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum KeyVerdict {
NotFound,
Match,
Mismatch,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub struct IcdModeConfig {
pub idle_mode_duration_s: u32,
pub active_mode_duration_ms: u32,
pub active_mode_threshold_ms: u16,
pub user_active_mode_trigger_hint: u32,
pub user_active_mode_trigger_instruction: &'static str,
}
struct IcdState {
clients: Vec<MonitoringRegistration, MAX_REGISTERED_CLIENTS>,
counter: CheckInCounter,
stay_active_until: Option<Instant>,
}
impl IcdState {
fn init(counter: CheckInCounter) -> impl Init<Self> {
init!(Self {
clients <- Vec::init(),
counter: counter,
stay_active_until: None,
})
}
}
pub struct Icd {
state: Mutex<RefCell<IcdState>>,
mode: IcdModeConfig,
registrations_changed: Notification,
active_extended: Notification,
}
impl Icd {
pub const fn new(counter: CheckInCounter, mode: IcdModeConfig) -> Self {
Self {
state: Mutex::new(RefCell::new(IcdState {
clients: Vec::new(),
counter,
stay_active_until: None,
})),
mode,
registrations_changed: Notification::new(),
active_extended: Notification::new(),
}
}
pub fn init(counter: CheckInCounter, mode: IcdModeConfig) -> impl Init<Self> {
init!(Self {
state <- Mutex::init(RefCell::init(IcdState::init(counter))),
mode: mode,
registrations_changed <- Notification::init(),
active_extended <- Notification::init(),
})
}
pub fn mode(&self) -> IcdModeConfig {
self.mode
}
pub fn registrations_len(&self) -> usize {
self.state.lock(|s| s.borrow().clients.len())
}
pub fn registrations_is_empty(&self) -> bool {
self.registrations_len() == 0
}
pub fn operating_mode(&self) -> OperatingModeEnum {
if self.registrations_is_empty() {
OperatingModeEnum::SIT
} else {
OperatingModeEnum::LIT
}
}
pub fn fabric_registrations_len(&self, fab_idx: NonZeroU8) -> usize {
self.state.lock(|s| {
s.borrow()
.clients
.iter()
.filter(|c| c.fab_idx == fab_idx)
.count()
})
}
pub fn register(&self, registration: MonitoringRegistration) -> Result<(), Error> {
self.state.lock(|s| -> Result<(), Error> {
let clients = &mut s.borrow_mut().clients;
if let Some(existing) = clients.iter_mut().find(|c| {
c.fab_idx == registration.fab_idx
&& c.check_in_node_id == registration.check_in_node_id
}) {
*existing = registration;
} else {
if clients
.iter()
.filter(|c| c.fab_idx == registration.fab_idx)
.count()
>= CLIENTS_PER_FABRIC
{
Err(ErrorCode::ResourceExhausted)?;
}
clients
.push(registration)
.map_err(|_| ErrorCode::ResourceExhausted)?;
}
Ok(())
})?;
self.registrations_changed.notify();
Ok(())
}
pub fn unregister(&self, fab_idx: NonZeroU8, check_in_node_id: u64) -> Result<(), Error> {
let removed = self.state.lock(|s| {
let clients = &mut s.borrow_mut().clients;
let before = clients.len();
clients.retain(|c| !(c.fab_idx == fab_idx && c.check_in_node_id == check_in_node_id));
clients.len() != before
});
if !removed {
Err(ErrorCode::NotFound)?;
}
self.registrations_changed.notify();
Ok(())
}
pub fn verify_key(
&self,
fab_idx: NonZeroU8,
check_in_node_id: u64,
key: Option<&[u8]>,
) -> KeyVerdict {
self.state.lock(|s| {
let state = s.borrow();
let Some(entry) = state
.clients
.iter()
.find(|c| c.fab_idx == fab_idx && c.check_in_node_id == check_in_node_id)
else {
return KeyVerdict::NotFound;
};
match key {
Some(key) if key == entry.key.access() => KeyVerdict::Match,
_ => KeyVerdict::Mismatch,
}
})
}
pub fn remove_fabric(&self, fab_idx: NonZeroU8) -> bool {
let removed = self.state.lock(|s| {
let clients = &mut s.borrow_mut().clients;
let before = clients.len();
clients.retain(|c| c.fab_idx != fab_idx);
clients.len() != before
});
if removed {
self.registrations_changed.notify();
}
removed
}
pub fn with_registrations<R>(&self, f: impl FnOnce(&[MonitoringRegistration]) -> R) -> R {
self.state.lock(|s| f(&s.borrow().clients))
}
pub async fn wait_registrations_changed(&self) {
self.registrations_changed.wait().await;
}
pub fn load_registrations<S: KvBlobStore>(
&self,
mut kv: S,
buf: &mut [u8],
) -> Result<(), Error> {
let clients = match kv.load(ICD_REGISTERED_CLIENTS_KEY, buf)? {
Some(data) => Vec::from_tlv(&TLVElement::new(data))?,
None => Vec::new(),
};
self.state.lock(|s| s.borrow_mut().clients = clients);
Ok(())
}
pub fn store_registrations<C: HandlerContext>(&self, ctx: &C) -> Result<(), Error> {
let mut persist = Persist::new(ctx.kv());
self.state
.lock(|s| persist.store_tlv(ICD_REGISTERED_CLIENTS_KEY, &s.borrow().clients))?;
persist.run()
}
pub fn active_until(&self) -> Option<Instant> {
self.state.lock(|s| s.borrow().stay_active_until)
}
pub async fn wait_active_extended(&self) {
self.active_extended.wait().await;
}
fn extend_active(&self, duration_ms: u32) -> u32 {
let now = Instant::now();
let requested = now.saturating_add(Duration::from_millis(duration_ms as u64));
let deadline = self.state.lock(|s| {
let stay = &mut s.borrow_mut().stay_active_until;
let deadline = stay.map_or(requested, |current| current.max(requested));
*stay = Some(deadline);
deadline
});
self.active_extended.notify();
deadline.saturating_duration_since(now).as_millis() as u32
}
pub fn next_counter(&self) -> u32 {
self.state.lock(|s| s.borrow().counter.next())
}
pub fn advance_counter<S: KvBlobStore>(&self, mut kv: S, buf: &mut [u8]) -> Result<(), Error> {
let to_persist = self.state.lock(|s| s.borrow_mut().counter.advance());
if let Some(value) = to_persist {
kv.store(
crate::persist::ICD_CHECK_IN_COUNTER_KEY,
&value.to_le_bytes(),
buf,
)?;
}
Ok(())
}
#[must_use = "a moved boundary must be persisted via persist_counter"]
pub fn invalidate_counter(&self, delta: u32) -> bool {
self.state
.lock(|s| s.borrow_mut().counter.advance_by(delta))
.is_some()
}
pub fn persist_counter<S: KvBlobStore>(&self, mut kv: S, buf: &mut [u8]) -> Result<(), Error> {
let value = self.state.lock(|s| s.borrow().counter.persist_value());
kv.store(
crate::persist::ICD_CHECK_IN_COUNTER_KEY,
&value.to_le_bytes(),
buf,
)
}
pub fn load_counter<S: KvBlobStore>(
&self,
mut kv: S,
epoch: u32,
buf: &mut [u8],
) -> Result<(), Error> {
let start = match kv.load(crate::persist::ICD_CHECK_IN_COUNTER_KEY, buf)? {
Some(data) => u32::from_le_bytes(data.try_into().map_err(|_| ErrorCode::Invalid)?),
None => return Ok(()),
};
self.state
.lock(|s| s.borrow_mut().counter = CheckInCounter::new(start, epoch));
Ok(())
}
pub async fn send_one_check_in<C: Crypto>(
&self,
matter: &Matter<'_>,
crypto: C,
fab_idx: NonZeroU8,
node_id: u64,
counter: u32,
buf: &mut [u8],
) -> Result<(), Error> {
let key = self
.state
.lock(|s| {
s.borrow()
.clients
.iter()
.find(|c| c.fab_idx == fab_idx && c.check_in_node_id == node_id)
.map(|c| c.key.clone())
})
.ok_or(ErrorCode::NotFound)?;
let app_data = self.mode.active_mode_threshold_ms.to_le_bytes();
CheckIn::new(key.reference())
.send_to(matter, crypto, fab_idx, node_id, counter, &app_data, buf)
.await
}
pub async fn send_check_in<C: Crypto, const NS: usize>(
&self,
matter: &Matter<'_>,
crypto: C,
subscriptions: &crate::im::subscriptions::Subscriptions<NS>,
kv: impl KvBlobStore,
buf: &mut [u8],
) -> Result<(), Error> {
let mut targets: Vec<(NonZeroU8, u64, CanonAeadKey), MAX_REGISTERED_CLIENTS> = Vec::new();
let counter = self.state.lock(|s| {
let state = s.borrow();
for c in &state.clients {
if subscriptions.has_subscription_for(c.fab_idx, c.monitored_subject) {
continue;
}
let _ = targets.push((c.fab_idx, c.check_in_node_id, c.key.clone()));
}
state.counter.next()
});
if targets.is_empty() {
return Ok(());
}
let app_data = self.mode.active_mode_threshold_ms.to_le_bytes();
for (fab_idx, node_id, key) in &targets {
let _ = CheckIn::new(key.reference())
.send_to(matter, &crypto, *fab_idx, *node_id, counter, &app_data, buf)
.await;
}
self.advance_counter(kv, buf)
}
}
pub struct IcdMgmtHandler<'a> {
dataver: Dataver,
icd: &'a Icd,
}
impl<'a> IcdMgmtHandler<'a> {
pub const fn new(dataver: Dataver, icd: &'a Icd) -> Self {
Self { dataver, icd }
}
pub const fn adapt(self) -> HandlerAdaptor<Self> {
HandlerAdaptor(self)
}
fn cmd_fabric(ctx: &impl InvokeContext) -> Result<NonZeroU8, Error> {
ctx.accessor()?.fab_idx()
}
fn caller_is_admin(ctx: &impl InvokeContext) -> Result<bool, Error> {
let accessor = ctx.accessor()?;
let cmd = ctx.cmd();
let path = GenericPath::new(
Some(cmd.endpoint_id),
Some(cmd.cluster_id),
Some(cmd.cmd_id),
);
let mut req = AccessReq::new(&accessor, path, Access::WRITE, &[]);
req.set_target_perms(Access::WRITE | Access::NEED_ADMIN);
Ok(req.allow())
}
fn sync_icd_mode(&self, ctx: &impl HandlerContext) {
ctx.matter().set_icd_mode(Some(self.icd.operating_mode()));
}
}
impl ClusterHandler for IcdMgmtHandler<'_> {
const CLUSTER: Cluster<'static> = FULL_CLUSTER
.with_features(
Feature::CHECK_IN_PROTOCOL_SUPPORT
.union(Feature::LONG_IDLE_TIME_SUPPORT)
.union(Feature::USER_ACTIVE_MODE_TRIGGER)
.union(Feature::DYNAMIC_SIT_LIT_SUPPORT)
.bits(),
)
.with_attrs(with!(required;
AttributeId::RegisteredClients
| AttributeId::ICDCounter
| AttributeId::ClientsSupportedPerFabric
| AttributeId::MaximumCheckInBackOff
| AttributeId::OperatingMode
| AttributeId::UserActiveModeTriggerHint
| AttributeId::UserActiveModeTriggerInstruction));
fn dataver(&self) -> u32 {
self.dataver.get()
}
fn dataver_changed(&self) {
self.dataver.changed();
}
fn lifecycle(&self, ctx: impl HandlerContext, op: LifecycleOp) -> Result<(), Error> {
match op {
LifecycleOp::Startup | LifecycleOp::FactoryReset => Ok(()),
LifecycleOp::FabricRemoval { fab_idx } => {
let mode_before = self.icd.operating_mode();
if self.icd.remove_fabric(fab_idx) {
self.icd.store_registrations(&ctx)?;
if self.icd.operating_mode() != mode_before {
ctx.notify_attr_changed(
ROOT_ENDPOINT_ID,
Self::CLUSTER.id,
AttributeId::OperatingMode as _,
);
}
self.sync_icd_mode(&ctx);
}
Ok(())
}
}
}
fn idle_mode_duration(&self, _ctx: impl ReadContext) -> Result<u32, Error> {
Ok(self.icd.mode.idle_mode_duration_s)
}
fn active_mode_duration(&self, _ctx: impl ReadContext) -> Result<u32, Error> {
Ok(self.icd.mode.active_mode_duration_ms)
}
fn active_mode_threshold(&self, _ctx: impl ReadContext) -> Result<u16, Error> {
Ok(self.icd.mode.active_mode_threshold_ms)
}
fn clients_supported_per_fabric(&self, _ctx: impl ReadContext) -> Result<u16, Error> {
Ok(CLIENTS_PER_FABRIC as u16)
}
fn maximum_check_in_back_off(&self, _ctx: impl ReadContext) -> Result<u32, Error> {
Ok(self.icd.mode.idle_mode_duration_s)
}
fn operating_mode(&self, _ctx: impl ReadContext) -> Result<OperatingModeEnum, Error> {
Ok(self.icd.operating_mode())
}
fn user_active_mode_trigger_hint(
&self,
_ctx: impl ReadContext,
) -> Result<UserActiveModeTriggerBitmap, Error> {
Ok(UserActiveModeTriggerBitmap::from_bits_truncate(
self.icd.mode.user_active_mode_trigger_hint,
))
}
fn user_active_mode_trigger_instruction<P: TLVBuilderParent>(
&self,
_ctx: impl ReadContext,
builder: crate::tlv::Utf8StrBuilder<P>,
) -> Result<P, Error> {
builder.set(self.icd.mode.user_active_mode_trigger_instruction)
}
fn icd_counter(&self, _ctx: impl ReadContext) -> Result<u32, Error> {
Ok(self.icd.next_counter())
}
fn registered_clients<P: TLVBuilderParent>(
&self,
ctx: impl ReadContext,
builder: ArrayAttributeRead<
MonitoringRegistrationStructArrayBuilder<P>,
MonitoringRegistrationStructBuilder<P>,
>,
) -> Result<P, Error> {
let attr = ctx.attr();
let fab_filter = attr
.fab_filter
.then(|| NonZeroU8::new(attr.fab_idx).ok_or(ErrorCode::UnsupportedAccess))
.transpose()?;
self.icd.with_registrations(|clients| {
let mut iter = clients
.iter()
.filter(|c| fab_filter.is_none_or(|f| c.fab_idx == f));
match builder {
ArrayAttributeRead::ReadAll(mut array) => {
for c in iter {
array = array
.push()?
.check_in_node_id(Some(c.check_in_node_id))?
.monitored_subject(Some(c.monitored_subject))?
.client_type(Some(c.client_type))?
.fabric_index(Some(c.fab_idx.get()))?
.end()?;
}
array.end()
}
ArrayAttributeRead::ReadOne(index, item) => {
let Some(c) = iter.nth(index as usize) else {
return Err(ErrorCode::ConstraintError.into());
};
item.check_in_node_id(Some(c.check_in_node_id))?
.monitored_subject(Some(c.monitored_subject))?
.client_type(Some(c.client_type))?
.fabric_index(Some(c.fab_idx.get()))?
.end()
}
ArrayAttributeRead::ReadNone(array) => array.end(),
}
})
}
fn handle_register_client<P: TLVBuilderParent>(
&self,
ctx: impl InvokeContext,
request: RegisterClientRequest<'_>,
response: RegisterClientResponseBuilder<P>,
) -> Result<P, Error> {
let fab_idx = Self::cmd_fabric(&ctx)?;
let node_id = request.check_in_node_id()?;
if !Self::caller_is_admin(&ctx)? {
let presented = request.verification_key()?.map(|k| k.0);
if self.icd.verify_key(fab_idx, node_id, presented) == KeyVerdict::Mismatch {
Err(ErrorCode::Failure)?;
}
}
let key = request.key()?;
self.icd.register(MonitoringRegistration {
fab_idx,
check_in_node_id: node_id,
monitored_subject: request.monitored_subject()?,
client_type: request
.client_type()
.map_err(|_| ErrorCode::ConstraintError)?,
key: key.0.try_into().map_err(|_| ErrorCode::ConstraintError)?,
})?;
self.icd.store_registrations(&ctx)?;
ctx.notify_own_cluster_changed();
self.sync_icd_mode(&ctx);
response.icd_counter(self.icd.next_counter())?.end()
}
fn handle_unregister_client(
&self,
ctx: impl InvokeContext,
request: UnregisterClientRequest<'_>,
) -> Result<(), Error> {
let fab_idx = Self::cmd_fabric(&ctx)?;
let node_id = request.check_in_node_id()?;
if !Self::caller_is_admin(&ctx)? {
let presented = request.verification_key()?.map(|k| k.0);
match self.icd.verify_key(fab_idx, node_id, presented) {
KeyVerdict::NotFound => Err(ErrorCode::NotFound)?,
KeyVerdict::Mismatch => Err(ErrorCode::Failure)?,
KeyVerdict::Match => {}
}
}
self.icd.unregister(fab_idx, node_id)?;
self.icd.store_registrations(&ctx)?;
ctx.notify_own_cluster_changed();
self.sync_icd_mode(&ctx);
Ok(())
}
fn handle_stay_active_request<P: TLVBuilderParent>(
&self,
_ctx: impl InvokeContext,
request: StayActiveRequestRequest<'_>,
response: StayActiveResponseBuilder<P>,
) -> Result<P, Error> {
let requested = request.stay_active_duration()?.min(STAY_ACTIVE_MAX_MS);
let promised = self.icd.extend_active(requested);
response.promised_active_duration(promised)?.end()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fab(i: u8) -> NonZeroU8 {
NonZeroU8::new(i).unwrap()
}
fn reg(fab: u8, node: u64) -> MonitoringRegistration {
MonitoringRegistration {
fab_idx: NonZeroU8::new(fab).unwrap(),
check_in_node_id: node,
monitored_subject: node,
client_type: ClientTypeEnum::Permanent,
key: CanonAeadKey::new(),
}
}
fn icd() -> Icd {
Icd::new(CheckInCounter::new(0, 10), mode())
}
fn nodes(icd: &Icd, fab_filter: Option<NonZeroU8>) -> alloc::vec::Vec<u64> {
icd.with_registrations(|clients| {
clients
.iter()
.filter(|c| fab_filter.is_none_or(|f| c.fab_idx == f))
.map(|c| c.check_in_node_id)
.collect()
})
}
#[test]
fn register_adds_and_updates() {
let icd = icd();
icd.register(reg(1, 100)).unwrap();
assert_eq!(icd.registrations_len(), 1);
assert_eq!(icd.fabric_registrations_len(fab(1)), 1);
let mut updated = reg(1, 100);
updated.monitored_subject = 999;
icd.register(updated).unwrap();
assert_eq!(icd.registrations_len(), 1);
let subject = icd.with_registrations(|c| c[0].monitored_subject);
assert_eq!(subject, 999);
}
#[test]
fn per_fabric_limit_is_enforced_independently() {
let icd = icd();
for i in 0..CLIENTS_PER_FABRIC {
icd.register(reg(1, 100 + i as u64)).unwrap();
}
assert_eq!(icd.fabric_registrations_len(fab(1)), CLIENTS_PER_FABRIC);
assert!(icd
.register(reg(1, 100 + CLIENTS_PER_FABRIC as u64))
.is_err());
assert_eq!(icd.fabric_registrations_len(fab(1)), CLIENTS_PER_FABRIC);
icd.register(reg(1, 100)).unwrap();
assert_eq!(icd.fabric_registrations_len(fab(1)), CLIENTS_PER_FABRIC);
icd.register(reg(2, 200)).unwrap();
assert_eq!(icd.fabric_registrations_len(fab(2)), 1);
}
#[test]
fn unregister_and_remove_fabric() {
let icd = icd();
icd.register(reg(1, 100)).unwrap();
icd.register(reg(2, 200)).unwrap();
assert!(icd.unregister(fab(1), 999).is_err()); icd.unregister(fab(1), 100).unwrap();
assert_eq!(icd.registrations_len(), 1);
assert!(icd.remove_fabric(fab(2)));
assert!(icd.registrations_is_empty());
assert!(!icd.remove_fabric(fab(2))); }
#[test]
fn operating_mode_follows_the_registration_set() {
let icd = icd();
assert_eq!(icd.operating_mode(), OperatingModeEnum::SIT);
icd.register(reg(1, 100)).unwrap();
assert_eq!(icd.operating_mode(), OperatingModeEnum::LIT);
icd.register(reg(1, 101)).unwrap();
icd.unregister(fab(1), 100).unwrap();
assert_eq!(icd.operating_mode(), OperatingModeEnum::LIT);
icd.unregister(fab(1), 101).unwrap();
assert_eq!(icd.operating_mode(), OperatingModeEnum::SIT);
}
#[test]
fn verify_key_matches_only_the_stored_key() {
let icd = icd();
let mut r = reg(1, 100);
let stored = [7u8; 16];
r.key.try_load_from_slice(&stored).unwrap();
icd.register(r).unwrap();
assert_eq!(
icd.verify_key(fab(1), 999, Some(&stored)),
KeyVerdict::NotFound
);
assert_eq!(
icd.verify_key(fab(2), 100, Some(&stored)),
KeyVerdict::NotFound
);
assert_eq!(
icd.verify_key(fab(1), 100, Some(&stored)),
KeyVerdict::Match
);
assert_eq!(
icd.verify_key(fab(1), 100, Some(&[0u8; 16])),
KeyVerdict::Mismatch
);
assert_eq!(icd.verify_key(fab(1), 100, None), KeyVerdict::Mismatch);
}
#[test]
fn with_registrations_honors_the_fabric_filter() {
let icd = icd();
icd.register(reg(1, 100)).unwrap();
icd.register(reg(2, 200)).unwrap();
let mut all = nodes(&icd, None);
all.sort_unstable();
assert_eq!(all, [100, 200]);
assert_eq!(nodes(&icd, Some(fab(1))), [100]);
}
#[derive(Default)]
struct MemKv {
value: Option<alloc::vec::Vec<u8>>,
}
impl KvBlobStore for &mut MemKv {
fn load<'a>(&mut self, _key: u16, buf: &'a mut [u8]) -> Result<Option<&'a [u8]>, Error> {
Ok(self.value.as_ref().map(|v| {
buf[..v.len()].copy_from_slice(v);
&buf[..v.len()]
}))
}
fn store(&mut self, _key: u16, data: &[u8], _buf: &mut [u8]) -> Result<(), Error> {
self.value = Some(data.to_vec());
Ok(())
}
fn remove(&mut self, _key: u16, _buf: &mut [u8]) -> Result<(), Error> {
self.value = None;
Ok(())
}
}
fn mode() -> IcdModeConfig {
IcdModeConfig {
idle_mode_duration_s: 60,
active_mode_duration_ms: 300,
active_mode_threshold_ms: 500,
user_active_mode_trigger_hint: 0,
user_active_mode_trigger_instruction: "",
}
}
#[test]
fn stay_active_combines_with_max_and_reports_remaining() {
let icd = Icd::new(CheckInCounter::new(0, 10), mode());
assert!(icd.active_until().is_none());
let promised = icd.extend_active(STAY_ACTIVE_MAX_MS);
assert!(promised <= STAY_ACTIVE_MAX_MS);
assert!(promised > STAY_ACTIVE_MAX_MS - 1_000, "promised {promised}");
let deadline = icd.active_until().expect("deadline now set");
let promised2 = icd.extend_active(1_000);
assert!(
promised2 > 1_000,
"shorter request must not shrink: {promised2}"
);
assert_eq!(icd.active_until(), Some(deadline), "deadline unchanged");
icd.extend_active(2 * STAY_ACTIVE_MAX_MS);
assert!(icd.active_until().unwrap() > deadline);
}
#[test]
fn stay_active_request_clamps_to_the_guaranteed_max() {
let icd = Icd::new(CheckInCounter::new(0, 10), mode());
let requested = STAY_ACTIVE_MAX_MS + 5_000;
let promised = icd.extend_active(requested.min(STAY_ACTIVE_MAX_MS));
assert!(promised <= STAY_ACTIVE_MAX_MS, "must clamp: {promised}");
}
#[test]
fn counter_persists_at_boundary_and_resumes_across_restart() {
const EPOCH: u32 = 10;
let mut kv = MemKv::default();
let mut buf = [0u8; 16];
let icd = Icd::new(CheckInCounter::new(100, EPOCH), mode());
assert_eq!(icd.next_counter(), 101);
for _ in 0..9 {
icd.advance_counter(&mut kv, &mut buf).unwrap();
}
assert_eq!(kv.value, None, "no persist before the boundary");
let last_used = icd.next_counter();
icd.advance_counter(&mut kv, &mut buf).unwrap();
assert_eq!(last_used, 110);
assert!(kv.value.is_some(), "boundary crossing must persist");
let icd2 = Icd::new(CheckInCounter::new(0, EPOCH), mode());
icd2.load_counter(&mut kv, EPOCH, &mut buf).unwrap();
assert!(icd2.next_counter() > last_used);
}
}