kitt_score 0.1.0

Decision engine at the core of Project KITT — in-memory stateful matching with pluggable scoring backends.
Documentation
//! Time source abstraction.
//!
//! Used to (a) make action expiry deterministic under test; (b) allow
//! benchmarks to step time without wall-clock interference; (c) simplify
//! property tests.

use crate::UnixTime;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};

/// Monotonic enough for our purposes (second-resolution action expiry).
pub trait Clock: Send + Sync {
    /// Return the current time in Unix seconds since the epoch.
    fn now(&self) -> UnixTime;
}

/// Production impl: wraps `SystemTime::now`.
#[derive(Debug, Clone, Copy)]
pub struct SystemClock;

impl Clock for SystemClock {
    #[allow(clippy::cast_possible_wrap)]
    fn now(&self) -> UnixTime {
        // SystemTime can be before UNIX_EPOCH on misconfigured systems. We
        // clamp to 0 in that (pathological) case rather than panic, because
        // action expiry semantics would be nonsense either way.
        // as_secs returns ~time-since-epoch; fits comfortably in i64 until year ~292 billion.
        SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|d| d.as_secs() as i64)
            .unwrap_or(0)
    }
}

/// Test impl: atomically advance-able clock.
#[derive(Debug, Default)]
pub struct TestClock {
    t: std::sync::atomic::AtomicI64,
}

impl TestClock {
    /// Create a new `TestClock` starting at the given Unix time.
    #[must_use]
    pub fn at(t: UnixTime) -> Arc<Self> {
        Arc::new(Self {
            t: std::sync::atomic::AtomicI64::new(t),
        })
    }

    /// Advance the clock by the given number of seconds.
    pub fn advance(&self, secs: i64) {
        self.t.fetch_add(secs, std::sync::atomic::Ordering::Relaxed);
    }

    /// Set the clock to an exact Unix time.
    pub fn set(&self, t: UnixTime) {
        self.t.store(t, std::sync::atomic::Ordering::Relaxed);
    }
}

impl Clock for TestClock {
    fn now(&self) -> UnixTime {
        self.t.load(std::sync::atomic::Ordering::Relaxed)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_clock_advances_monotonically() {
        let c = TestClock::at(100);
        assert_eq!(c.now(), 100);
        c.advance(5);
        assert_eq!(c.now(), 105);
        c.set(42);
        assert_eq!(c.now(), 42);
    }

    #[test]
    fn system_clock_is_nonnegative_and_near_now() {
        let c = SystemClock;
        // 2026-01-01 as a reasonable lower bound.
        assert!(c.now() > 1_735_689_600);
    }
}