feat: complete TUI Phase 0 — Toolchain Gate
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.
This commit is contained in:
151
crates/lore-tui/src/clock.rs
Normal file
151
crates/lore-tui/src/clock.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
//! 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;
|
||||
}
|
||||
Reference in New Issue
Block a user