Implements the full lore-tui crate scaffold with 6 Phase 0 modules: - message.rs: Msg (~40 variants), Screen (12), EntityKey, AppError, InputMode - clock.rs: Clock trait with SystemClock + FakeClock for deterministic testing - safety.rs: Terminal sanitizer (ANSI filter), URL policy, PII/secret redaction - db.rs: DbManager with 3 reader pool (round-robin) + dedicated writer (WAL mode) - theme.rs: Flexoki adaptive theme (19 slots), state/event colors, label styling - app.rs: Minimal LoreApp Model trait impl proving FrankenTUI integration 68 tests, clippy clean, fmt clean. Closes bd-3ddw, bd-c9gk, bd-2lg6, bd-3ir1, bd-2kop, bd-5ofk, bd-2emv, bd-1cj0.
152 lines
4.4 KiB
Rust
152 lines
4.4 KiB
Rust
//! Injected clock for deterministic time in tests and consistent frame timestamps.
|
|
//!
|
|
//! All relative-time rendering (e.g., "3h ago") uses [`Clock::now()`] rather
|
|
//! than wall-clock time directly. This enables:
|
|
//! - Deterministic snapshot tests via [`FakeClock`]
|
|
//! - Consistent timestamps within a single frame render pass
|
|
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use chrono::{DateTime, TimeDelta, Utc};
|
|
|
|
/// Trait for obtaining the current time.
|
|
///
|
|
/// Inject via `Arc<dyn Clock>` to allow swapping between real and fake clocks.
|
|
pub trait Clock: Send + Sync {
|
|
/// Returns the current time.
|
|
fn now(&self) -> DateTime<Utc>;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// SystemClock
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Real wall-clock time via `chrono::Utc::now()`.
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct SystemClock;
|
|
|
|
impl Clock for SystemClock {
|
|
fn now(&self) -> DateTime<Utc> {
|
|
Utc::now()
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// FakeClock
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// A controllable clock for tests. Returns a frozen time that can be
|
|
/// advanced or set explicitly.
|
|
///
|
|
/// `FakeClock` is `Clone` (shares the inner `Arc`) and `Send + Sync`
|
|
/// for use across `Cmd::task` threads.
|
|
#[derive(Debug, Clone)]
|
|
pub struct FakeClock {
|
|
inner: Arc<Mutex<DateTime<Utc>>>,
|
|
}
|
|
|
|
impl FakeClock {
|
|
/// Create a fake clock frozen at the given time.
|
|
#[must_use]
|
|
pub fn new(time: DateTime<Utc>) -> Self {
|
|
Self {
|
|
inner: Arc::new(Mutex::new(time)),
|
|
}
|
|
}
|
|
|
|
/// Advance the clock by `duration`. Uses `checked_add` to handle overflow
|
|
/// gracefully — if the addition would overflow, the time is not changed.
|
|
pub fn advance(&self, duration: TimeDelta) {
|
|
let mut guard = self.inner.lock().expect("FakeClock mutex poisoned");
|
|
if let Some(advanced) = guard.checked_add_signed(duration) {
|
|
*guard = advanced;
|
|
}
|
|
}
|
|
|
|
/// Set the clock to an exact time.
|
|
pub fn set(&self, time: DateTime<Utc>) {
|
|
let mut guard = self.inner.lock().expect("FakeClock mutex poisoned");
|
|
*guard = time;
|
|
}
|
|
}
|
|
|
|
impl Clock for FakeClock {
|
|
fn now(&self) -> DateTime<Utc> {
|
|
*self.inner.lock().expect("FakeClock mutex poisoned")
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use chrono::TimeZone;
|
|
|
|
fn fixed_time() -> DateTime<Utc> {
|
|
Utc.with_ymd_and_hms(2026, 2, 12, 12, 0, 0).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn test_fake_clock_frozen() {
|
|
let clock = FakeClock::new(fixed_time());
|
|
let t1 = clock.now();
|
|
let t2 = clock.now();
|
|
assert_eq!(t1, t2);
|
|
assert_eq!(t1, fixed_time());
|
|
}
|
|
|
|
#[test]
|
|
fn test_fake_clock_advance() {
|
|
let clock = FakeClock::new(fixed_time());
|
|
clock.advance(TimeDelta::hours(3));
|
|
let expected = Utc.with_ymd_and_hms(2026, 2, 12, 15, 0, 0).unwrap();
|
|
assert_eq!(clock.now(), expected);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fake_clock_set() {
|
|
let clock = FakeClock::new(fixed_time());
|
|
let new_time = Utc.with_ymd_and_hms(2030, 1, 1, 0, 0, 0).unwrap();
|
|
clock.set(new_time);
|
|
assert_eq!(clock.now(), new_time);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fake_clock_clone_shares_state() {
|
|
let clock1 = FakeClock::new(fixed_time());
|
|
let clock2 = clock1.clone();
|
|
clock1.advance(TimeDelta::minutes(30));
|
|
// Both clones see the advanced time.
|
|
assert_eq!(clock1.now(), clock2.now());
|
|
}
|
|
|
|
#[test]
|
|
fn test_system_clock_returns_reasonable_time() {
|
|
let clock = SystemClock;
|
|
let now = clock.now();
|
|
// Sanity: time should be after 2025.
|
|
assert!(now.year() >= 2025);
|
|
}
|
|
|
|
#[test]
|
|
fn test_fake_clock_is_send_sync() {
|
|
fn assert_send_sync<T: Send + Sync>() {}
|
|
assert_send_sync::<FakeClock>();
|
|
assert_send_sync::<SystemClock>();
|
|
}
|
|
|
|
#[test]
|
|
fn test_clock_trait_object_works() {
|
|
let fake: Arc<dyn Clock> = Arc::new(FakeClock::new(fixed_time()));
|
|
assert_eq!(fake.now(), fixed_time());
|
|
|
|
let real: Arc<dyn Clock> = Arc::new(SystemClock);
|
|
let _ = real.now(); // Just verify it doesn't panic.
|
|
}
|
|
|
|
use chrono::Datelike;
|
|
}
|