Add clock mocking mechanism for dev use only

This commit is contained in:
Albert Armea 2025-12-28 21:26:54 -05:00
parent 0f3bc8f690
commit 133a55035a
13 changed files with 388 additions and 27 deletions

View file

@ -82,6 +82,8 @@ TODO
## Development ## Development
TODO: `./run-dev`, options TODO: `./run-dev`, options
* Set the `SHEPHERD_MOCK_TIME` environment variable to mock the time,
such as `SHEPHERD_MOCK_TIME="2025-12-25 15:30:00" ./run-dev`
## Contributing ## Contributing

View file

@ -19,7 +19,7 @@ impl Event {
pub fn new(payload: EventPayload) -> Self { pub fn new(payload: EventPayload) -> Self {
Self { Self {
api_version: API_VERSION, api_version: API_VERSION,
timestamp: Local::now(), timestamp: shepherd_util::now(),
payload, payload,
} }
} }

View file

@ -479,7 +479,7 @@ impl CoreEngine {
}); });
// Build entry views for the snapshot // Build entry views for the snapshot
let entries = self.list_entries(Local::now()); let entries = self.list_entries(shepherd_util::now());
DaemonStateSnapshot { DaemonStateSnapshot {
api_version: API_VERSION, api_version: API_VERSION,

View file

@ -213,7 +213,7 @@ mod tests {
#[test] #[test]
fn test_session_creation() { fn test_session_creation() {
let plan = make_test_plan(300); let plan = make_test_plan(300);
let now = Local::now(); let now = shepherd_util::now();
let now_mono = MonotonicInstant::now(); let now_mono = MonotonicInstant::now();
let session = ActiveSession::new(plan, now, now_mono); let session = ActiveSession::new(plan, now, now_mono);
@ -261,7 +261,7 @@ mod tests {
#[test] #[test]
fn test_pending_warnings() { fn test_pending_warnings() {
let plan = make_test_plan(300); let plan = make_test_plan(300);
let now = Local::now(); let now = shepherd_util::now();
let now_mono = MonotonicInstant::now(); let now_mono = MonotonicInstant::now();
let mut session = ActiveSession::new(plan, now, now_mono); let mut session = ActiveSession::new(plan, now, now_mono);

View file

@ -179,6 +179,32 @@ fn build_hud_content(state: SharedState) -> gtk4::Box {
.halign(gtk4::Align::End) .halign(gtk4::Align::End)
.build(); .build();
// Wall clock display (shows mock time indicator in debug builds)
let clock_box = gtk4::Box::builder()
.orientation(gtk4::Orientation::Horizontal)
.spacing(4)
.build();
let clock_icon = gtk4::Image::from_icon_name("preferences-system-time-symbolic");
clock_icon.set_pixel_size(20);
clock_box.append(&clock_icon);
let clock_label = gtk4::Label::new(Some("--:--"));
clock_label.add_css_class("clock-label");
clock_box.append(&clock_label);
// Add mock indicator if mock time is active (debug builds only)
#[cfg(debug_assertions)]
{
if shepherd_util::is_mock_time_active() {
let mock_indicator = gtk4::Label::new(Some("(MOCK)"));
mock_indicator.add_css_class("mock-time-indicator");
clock_box.append(&mock_indicator);
}
}
right_box.append(&clock_box);
// Volume control with slider // Volume control with slider
let volume_box = gtk4::Box::builder() let volume_box = gtk4::Box::builder()
.orientation(gtk4::Orientation::Horizontal) .orientation(gtk4::Orientation::Horizontal)
@ -345,8 +371,13 @@ fn build_hud_content(state: SharedState) -> gtk4::Box {
let volume_slider_clone = volume_slider.clone(); let volume_slider_clone = volume_slider.clone();
let volume_label_clone = volume_label.clone(); let volume_label_clone = volume_label.clone();
let slider_changing_for_update = slider_changing.clone(); let slider_changing_for_update = slider_changing.clone();
let clock_label_clone = clock_label.clone();
glib::timeout_add_local(Duration::from_millis(500), move || { glib::timeout_add_local(Duration::from_millis(500), move || {
// Update wall clock display
let current_time = shepherd_util::now();
clock_label_clone.set_text(&shepherd_util::format_clock_time(&current_time));
// Update session state // Update session state
let session_state = state.session_state(); let session_state = state.session_state();
match &session_state { match &session_state {
@ -567,6 +598,19 @@ fn load_css() {
min-width: 3em; min-width: 3em;
text-align: right; text-align: right;
} }
.clock-label {
font-family: monospace;
font-size: 14px;
color: var(--text-primary);
}
.mock-time-indicator {
font-size: 10px;
font-weight: bold;
color: var(--color-warning);
margin-left: 4px;
}
"#; "#;
let provider = gtk4::CssProvider::new(); let provider = gtk4::CssProvider::new();

View file

@ -182,7 +182,7 @@ impl SharedState {
label, label,
deadline, deadline,
} => { } => {
let now = chrono::Local::now(); let now = shepherd_util::now();
// For unlimited sessions (deadline=None), time_remaining is None // For unlimited sessions (deadline=None), time_remaining is None
let time_remaining = deadline.and_then(|d| { let time_remaining = deadline.and_then(|d| {
if d > now { if d > now {
@ -244,7 +244,7 @@ impl SharedState {
EventPayload::StateChanged(snapshot) => { EventPayload::StateChanged(snapshot) => {
if let Some(session) = &snapshot.current_session { if let Some(session) = &snapshot.current_session {
let now = chrono::Local::now(); let now = shepherd_util::now();
// For unlimited sessions (deadline=None), time_remaining is None // For unlimited sessions (deadline=None), time_remaining is None
let time_remaining = session.deadline.and_then(|d| { let time_remaining = session.deadline.and_then(|d| {
if d > now { if d > now {

View file

@ -190,7 +190,7 @@ impl LauncherApp {
match payload { match payload {
shepherd_api::ResponsePayload::LaunchApproved { session_id, deadline } => { shepherd_api::ResponsePayload::LaunchApproved { session_id, deadline } => {
info!(session_id = %session_id, "Launch approved, setting SessionActive"); info!(session_id = %session_id, "Launch approved, setting SessionActive");
let now = chrono::Local::now(); let now = shepherd_util::now();
// For unlimited sessions (deadline=None), time_remaining is None // For unlimited sessions (deadline=None), time_remaining is None
let time_remaining = deadline.and_then(|d| { let time_remaining = deadline.and_then(|d| {
if d > now { if d > now {

View file

@ -139,7 +139,7 @@ impl DaemonClient {
match payload { match payload {
ResponsePayload::State(snapshot) => { ResponsePayload::State(snapshot) => {
if let Some(session) = snapshot.current_session { if let Some(session) = snapshot.current_session {
let now = chrono::Local::now(); let now = shepherd_util::now();
// For unlimited sessions (deadline=None), time_remaining is None // For unlimited sessions (deadline=None), time_remaining is None
let time_remaining = session.deadline.and_then(|d| { let time_remaining = session.deadline.and_then(|d| {
if d > now { if d > now {
@ -166,7 +166,7 @@ impl DaemonClient {
} }
} }
ResponsePayload::LaunchApproved { session_id, deadline } => { ResponsePayload::LaunchApproved { session_id, deadline } => {
let now = chrono::Local::now(); let now = shepherd_util::now();
// For unlimited sessions (deadline=None), time_remaining is None // For unlimited sessions (deadline=None), time_remaining is None
let time_remaining = deadline.and_then(|d| { let time_remaining = deadline.and_then(|d| {
if d > now { if d > now {

View file

@ -73,7 +73,7 @@ impl SharedState {
deadline, deadline,
} => { } => {
tracing::info!(session_id = %session_id, label = %label, "Session started event"); tracing::info!(session_id = %session_id, label = %label, "Session started event");
let now = chrono::Local::now(); let now = shepherd_util::now();
// For unlimited sessions (deadline=None), time_remaining is None // For unlimited sessions (deadline=None), time_remaining is None
let time_remaining = deadline.and_then(|d| { let time_remaining = deadline.and_then(|d| {
if d > now { if d > now {
@ -123,7 +123,7 @@ impl SharedState {
fn apply_snapshot(&self, snapshot: DaemonStateSnapshot) { fn apply_snapshot(&self, snapshot: DaemonStateSnapshot) {
if let Some(session) = snapshot.current_session { if let Some(session) = snapshot.current_session {
let now = chrono::Local::now(); let now = shepherd_util::now();
// For unlimited sessions (deadline=None), time_remaining is None // For unlimited sessions (deadline=None), time_remaining is None
let time_remaining = session.deadline.and_then(|d| { let time_remaining = session.deadline.and_then(|d| {
if d > now { if d > now {

View file

@ -86,7 +86,7 @@ impl AuditEvent {
pub fn new(event: AuditEventType) -> Self { pub fn new(event: AuditEventType) -> Self {
Self { Self {
id: 0, // Will be set by store id: 0, // Will be set by store
timestamp: Local::now(), timestamp: shepherd_util::now(),
event, event,
} }
} }

View file

@ -2,11 +2,103 @@
//! //!
//! Provides both monotonic time (for countdown enforcement) and //! Provides both monotonic time (for countdown enforcement) and
//! wall-clock time (for availability windows). //! wall-clock time (for availability windows).
//!
//! # Mock Time for Development
//!
//! In debug builds, the `SHEPHERD_MOCK_TIME` environment variable can be set
//! to override the system time for all time-sensitive operations. This is useful
//! for testing availability windows and time-based policies.
//!
//! Format: `YYYY-MM-DD HH:MM:SS` (e.g., `2025-12-25 14:30:00`)
//!
//! Example:
//! ```bash
//! SHEPHERD_MOCK_TIME="2025-12-25 14:30:00" ./run-dev
//! ```
use chrono::{DateTime, Datelike, Local, NaiveTime, Timelike, Weekday}; use chrono::{DateTime, Datelike, Local, NaiveDateTime, NaiveTime, TimeZone, Timelike, Weekday};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::OnceLock;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
/// Environment variable name for mock time (debug builds only)
pub const MOCK_TIME_ENV_VAR: &str = "SHEPHERD_MOCK_TIME";
/// Cached mock time offset from the real time when the process started.
/// This allows mock time to advance naturally.
static MOCK_TIME_OFFSET: OnceLock<Option<chrono::Duration>> = OnceLock::new();
/// Initialize the mock time offset based on the environment variable.
/// Returns the offset between mock time and real time at process start.
fn get_mock_time_offset() -> Option<chrono::Duration> {
*MOCK_TIME_OFFSET.get_or_init(|| {
#[cfg(debug_assertions)]
{
if let Ok(mock_time_str) = std::env::var(MOCK_TIME_ENV_VAR) {
// Parse the mock time string
if let Ok(naive_dt) = NaiveDateTime::parse_from_str(&mock_time_str, "%Y-%m-%d %H:%M:%S") {
if let Some(mock_dt) = Local.from_local_datetime(&naive_dt).single() {
let real_now = chrono::Local::now();
let offset = mock_dt.signed_duration_since(real_now);
tracing::info!(
mock_time = %mock_time_str,
offset_secs = offset.num_seconds(),
"Mock time enabled"
);
return Some(offset);
} else {
tracing::warn!(
mock_time = %mock_time_str,
"Failed to convert mock time to local timezone"
);
}
} else {
tracing::warn!(
mock_time = %mock_time_str,
expected_format = "%Y-%m-%d %H:%M:%S",
"Invalid mock time format"
);
}
}
None
}
#[cfg(not(debug_assertions))]
{
None
}
})
}
/// Returns whether mock time is currently active.
pub fn is_mock_time_active() -> bool {
get_mock_time_offset().is_some()
}
/// Get the current local time, respecting mock time settings in debug builds.
///
/// In release builds, this always returns the real system time.
/// In debug builds, if `SHEPHERD_MOCK_TIME` is set, this returns a time
/// that advances from the mock time at the same rate as real time.
pub fn now() -> DateTime<Local> {
let real_now = chrono::Local::now();
if let Some(offset) = get_mock_time_offset() {
real_now + offset
} else {
real_now
}
}
/// Format a DateTime for display in the HUD clock.
pub fn format_clock_time(dt: &DateTime<Local>) -> String {
dt.format("%H:%M").to_string()
}
/// Format a DateTime for display with full date and time.
pub fn format_datetime_full(dt: &DateTime<Local>) -> String {
dt.format("%Y-%m-%d %H:%M:%S").to_string()
}
/// Represents a point in monotonic time for countdown enforcement. /// Represents a point in monotonic time for countdown enforcement.
/// This is immune to wall-clock changes. /// This is immune to wall-clock changes.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
@ -298,4 +390,229 @@ mod tests {
assert!(t2 > t1); assert!(t2 > t1);
assert!(t2.duration_since(t1) >= Duration::from_millis(10)); assert!(t2.duration_since(t1) >= Duration::from_millis(10));
} }
#[test]
fn test_format_clock_time() {
let dt = Local.with_ymd_and_hms(2025, 12, 25, 14, 30, 45).unwrap();
assert_eq!(format_clock_time(&dt), "14:30");
}
#[test]
fn test_format_datetime_full() {
let dt = Local.with_ymd_and_hms(2025, 12, 25, 14, 30, 45).unwrap();
assert_eq!(format_datetime_full(&dt), "2025-12-25 14:30:45");
}
#[test]
fn test_now_returns_time() {
// Basic test that now() returns a valid time
let t = now();
// Should be a reasonable year (after 2020, before 2100)
assert!(t.year() >= 2020);
assert!(t.year() <= 2100);
}
#[test]
fn test_mock_time_env_var_name() {
// Verify the environment variable name is correct
assert_eq!(MOCK_TIME_ENV_VAR, "SHEPHERD_MOCK_TIME");
}
#[test]
fn test_parse_mock_time_format() {
// Test that the expected format parses correctly
let valid_formats = [
"2025-12-25 14:30:00",
"2025-01-01 00:00:00",
"2025-12-31 23:59:59",
"2020-06-15 12:00:00",
];
for format_str in &valid_formats {
let result = NaiveDateTime::parse_from_str(format_str, "%Y-%m-%d %H:%M:%S");
assert!(
result.is_ok(),
"Expected '{}' to parse successfully, got {:?}",
format_str,
result
);
}
}
#[test]
fn test_parse_mock_time_invalid_formats() {
// Test that invalid formats are rejected
let invalid_formats = [
"2025-12-25", // Missing time
"14:30:00", // Missing date
"2025/12/25 14:30:00", // Wrong date separator
"2025-12-25T14:30:00", // ISO format (not supported)
"Dec 25, 2025 14:30", // Wrong format
"25-12-2025 14:30:00", // Wrong date order
"", // Empty string
"not a date", // Invalid string
];
for format_str in &invalid_formats {
let result = NaiveDateTime::parse_from_str(format_str, "%Y-%m-%d %H:%M:%S");
assert!(
result.is_err(),
"Expected '{}' to fail parsing, but it succeeded",
format_str
);
}
}
#[test]
fn test_mock_time_offset_calculation() {
// Test that the offset calculation works correctly
let mock_time_str = "2025-12-25 14:30:00";
let naive_dt = NaiveDateTime::parse_from_str(mock_time_str, "%Y-%m-%d %H:%M:%S").unwrap();
let mock_dt = Local.from_local_datetime(&naive_dt).single().unwrap();
let real_now = chrono::Local::now();
let offset = mock_dt.signed_duration_since(real_now);
// The offset should be applied correctly
let simulated_now = real_now + offset;
// The simulated time should be very close to the mock time
// (within a second, accounting for test execution time)
let diff = (simulated_now - mock_dt).num_seconds().abs();
assert!(
diff <= 1,
"Expected simulated time to be within 1 second of mock time, got {} seconds difference",
diff
);
}
#[test]
fn test_mock_time_advances_with_real_time() {
// Test that mock time advances at the same rate as real time
// This tests the concept, not the actual implementation (since OnceLock is static)
let mock_time_str = "2025-12-25 14:30:00";
let naive_dt = NaiveDateTime::parse_from_str(mock_time_str, "%Y-%m-%d %H:%M:%S").unwrap();
let mock_dt = Local.from_local_datetime(&naive_dt).single().unwrap();
let real_t1 = chrono::Local::now();
let offset = mock_dt.signed_duration_since(real_t1);
// Simulate time passing
std::thread::sleep(Duration::from_millis(100));
let real_t2 = chrono::Local::now();
let simulated_t1 = real_t1 + offset;
let simulated_t2 = real_t2 + offset;
// The simulated times should have advanced by the same amount as real times
let real_elapsed = real_t2.signed_duration_since(real_t1);
let simulated_elapsed = simulated_t2.signed_duration_since(simulated_t1);
assert_eq!(
real_elapsed.num_milliseconds(),
simulated_elapsed.num_milliseconds(),
"Mock time should advance at the same rate as real time"
);
}
#[test]
fn test_availability_with_specific_time() {
// Test that availability windows work correctly with a specific time
// This validates that the mock time would affect availability checks
let window = TimeWindow::new(
DaysOfWeek::ALL_DAYS,
WallClock::new(14, 0).unwrap(), // 2 PM
WallClock::new(18, 0).unwrap(), // 6 PM
);
// Time within window
let in_window = Local.with_ymd_and_hms(2025, 12, 25, 15, 0, 0).unwrap();
assert!(window.contains(&in_window), "15:00 should be within 14:00-18:00 window");
// Time before window
let before_window = Local.with_ymd_and_hms(2025, 12, 25, 10, 0, 0).unwrap();
assert!(!window.contains(&before_window), "10:00 should be before 14:00-18:00 window");
// Time after window
let after_window = Local.with_ymd_and_hms(2025, 12, 25, 20, 0, 0).unwrap();
assert!(!window.contains(&after_window), "20:00 should be after 14:00-18:00 window");
}
#[test]
fn test_availability_with_day_restriction() {
// Test that day-of-week restrictions work correctly
let window = TimeWindow::new(
DaysOfWeek::WEEKDAYS,
WallClock::new(14, 0).unwrap(),
WallClock::new(18, 0).unwrap(),
);
// Thursday at 3 PM - should be available (weekday, in time window)
let thursday = Local.with_ymd_and_hms(2025, 12, 25, 15, 0, 0).unwrap(); // Christmas 2025 is Thursday
assert!(window.contains(&thursday), "Thursday 15:00 should be in weekday afternoon window");
// Saturday at 3 PM - should NOT be available (weekend)
let saturday = Local.with_ymd_and_hms(2025, 12, 27, 15, 0, 0).unwrap();
assert!(!window.contains(&saturday), "Saturday should not be in weekday window");
// Sunday at 3 PM - should NOT be available (weekend)
let sunday = Local.with_ymd_and_hms(2025, 12, 28, 15, 0, 0).unwrap();
assert!(!window.contains(&sunday), "Sunday should not be in weekday window");
}
}
/// Tests that require running in a separate process to test environment variable handling.
/// These are integration-style tests for the mock time feature.
#[cfg(test)]
mod mock_time_integration_tests {
use super::*;
/// This test documents the expected behavior of the mock time feature.
/// Due to the static OnceLock, actual integration testing requires
/// running with the environment variable set externally.
///
/// To manually test:
/// ```bash
/// SHEPHERD_MOCK_TIME="2025-12-25 14:30:00" cargo test
/// ```
#[test]
fn test_mock_time_documentation() {
// This test verifies the mock time constants and expected behavior
assert_eq!(MOCK_TIME_ENV_VAR, "SHEPHERD_MOCK_TIME");
// The expected format is documented
let expected_format = "%Y-%m-%d %H:%M:%S";
let example = "2025-12-25 14:30:00";
assert!(NaiveDateTime::parse_from_str(example, expected_format).is_ok());
}
#[test]
#[cfg(debug_assertions)]
fn test_is_mock_time_active_in_debug() {
// In debug mode, is_mock_time_active() should return based on env var
// Since we can't control the env var within a single test run due to OnceLock,
// we just verify the function doesn't panic
let _ = is_mock_time_active();
}
#[test]
fn test_now_consistency() {
// now() should return consistent, advancing times
let t1 = now();
std::thread::sleep(Duration::from_millis(50));
let t2 = now();
// t2 should be after t1
assert!(t2 > t1, "Time should advance forward");
// The difference should be approximately 50ms (with some tolerance)
let diff = t2.signed_duration_since(t1);
assert!(
diff.num_milliseconds() >= 40 && diff.num_milliseconds() <= 200,
"Expected ~50ms difference, got {}ms",
diff.num_milliseconds()
);
}
} }

View file

@ -10,7 +10,6 @@
//! - Volume control //! - Volume control
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use chrono::Local;
use clap::Parser; use clap::Parser;
use shepherd_api::{ use shepherd_api::{
Command, DaemonStateSnapshot, ErrorCode, ErrorInfo, Event, EventPayload, HealthStatus, Command, DaemonStateSnapshot, ErrorCode, ErrorInfo, Event, EventPayload, HealthStatus,
@ -176,7 +175,7 @@ impl Daemon {
// Tick timer - check warnings and expiry // Tick timer - check warnings and expiry
_ = tick_timer.tick() => { _ = tick_timer.tick() => {
let now_mono = MonotonicInstant::now(); let now_mono = MonotonicInstant::now();
let now = Local::now(); let now = shepherd_util::now();
let events = { let events = {
let mut engine = engine.lock().await; let mut engine = engine.lock().await;
@ -207,7 +206,7 @@ impl Daemon {
ipc: &Arc<IpcServer>, ipc: &Arc<IpcServer>,
event: CoreEvent, event: CoreEvent,
_now_mono: MonotonicInstant, _now_mono: MonotonicInstant,
_now: chrono::DateTime<Local>, _now: chrono::DateTime<chrono::Local>,
) { ) {
match &event { match &event {
CoreEvent::Warning { CoreEvent::Warning {
@ -322,7 +321,7 @@ impl Daemon {
match event { match event {
HostEvent::Exited { handle, status } => { HostEvent::Exited { handle, status } => {
let now_mono = MonotonicInstant::now(); let now_mono = MonotonicInstant::now();
let now = Local::now(); let now = shepherd_util::now();
info!( info!(
session_id = %handle.session_id, session_id = %handle.session_id,
@ -454,7 +453,7 @@ impl Daemon {
request_id: u64, request_id: u64,
command: Command, command: Command,
) -> Response { ) -> Response {
let now = Local::now(); let now = shepherd_util::now();
let now_mono = MonotonicInstant::now(); let now_mono = MonotonicInstant::now();
match command { match command {

View file

@ -2,13 +2,12 @@
//! //!
//! These tests verify the end-to-end behavior of the daemon. //! These tests verify the end-to-end behavior of the daemon.
use chrono::Local;
use shepherd_api::{EntryKind, WarningSeverity, WarningThreshold}; use shepherd_api::{EntryKind, WarningSeverity, WarningThreshold};
use shepherd_config::{AvailabilityPolicy, Entry, LimitsPolicy, Policy}; use shepherd_config::{AvailabilityPolicy, Entry, LimitsPolicy, Policy};
use shepherd_core::{CoreEngine, CoreEvent, LaunchDecision}; use shepherd_core::{CoreEngine, CoreEvent, LaunchDecision};
use shepherd_host_api::{HostCapabilities, MockHost}; use shepherd_host_api::{HostCapabilities, MockHost};
use shepherd_store::{SqliteStore, Store}; use shepherd_store::{SqliteStore, Store};
use shepherd_util::{EntryId, MonotonicInstant}; use shepherd_util::{self, EntryId, MonotonicInstant};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
@ -73,7 +72,7 @@ fn test_entry_listing() {
let caps = HostCapabilities::minimal(); let caps = HostCapabilities::minimal();
let engine = CoreEngine::new(policy, store, caps); let engine = CoreEngine::new(policy, store, caps);
let entries = engine.list_entries(Local::now()); let entries = engine.list_entries(shepherd_util::now());
assert_eq!(entries.len(), 1); assert_eq!(entries.len(), 1);
assert!(entries[0].enabled); assert!(entries[0].enabled);
@ -89,7 +88,7 @@ fn test_launch_approval() {
let engine = CoreEngine::new(policy, store, caps); let engine = CoreEngine::new(policy, store, caps);
let entry_id = EntryId::new("test-game"); let entry_id = EntryId::new("test-game");
let decision = engine.request_launch(&entry_id, Local::now()); let decision = engine.request_launch(&entry_id, shepherd_util::now());
assert!(matches!(decision, LaunchDecision::Approved(plan) if plan.max_duration == Some(Duration::from_secs(10)))); assert!(matches!(decision, LaunchDecision::Approved(plan) if plan.max_duration == Some(Duration::from_secs(10))));
} }
@ -102,7 +101,7 @@ fn test_session_lifecycle() {
let mut engine = CoreEngine::new(policy, store, caps); let mut engine = CoreEngine::new(policy, store, caps);
let entry_id = EntryId::new("test-game"); let entry_id = EntryId::new("test-game");
let now = Local::now(); let now = shepherd_util::now();
let now_mono = MonotonicInstant::now(); let now_mono = MonotonicInstant::now();
// Launch // Launch
@ -131,7 +130,7 @@ fn test_warning_emission() {
let mut engine = CoreEngine::new(policy, store, caps); let mut engine = CoreEngine::new(policy, store, caps);
let entry_id = EntryId::new("test-game"); let entry_id = EntryId::new("test-game");
let now = Local::now(); let now = shepherd_util::now();
let now_mono = MonotonicInstant::now(); let now_mono = MonotonicInstant::now();
// Start session // Start session
@ -170,7 +169,7 @@ fn test_session_expiry() {
let mut engine = CoreEngine::new(policy, store, caps); let mut engine = CoreEngine::new(policy, store, caps);
let entry_id = EntryId::new("test-game"); let entry_id = EntryId::new("test-game");
let now = Local::now(); let now = shepherd_util::now();
let now_mono = MonotonicInstant::now(); let now_mono = MonotonicInstant::now();
// Start session // Start session
@ -198,7 +197,7 @@ fn test_usage_accounting() {
let mut engine = CoreEngine::new(policy, store, caps); let mut engine = CoreEngine::new(policy, store, caps);
let entry_id = EntryId::new("test-game"); let entry_id = EntryId::new("test-game");
let now = Local::now(); let now = shepherd_util::now();
let now_mono = MonotonicInstant::now(); let now_mono = MonotonicInstant::now();
// Start session // Start session
@ -302,7 +301,7 @@ fn test_session_extension() {
let mut engine = CoreEngine::new(policy, store, caps); let mut engine = CoreEngine::new(policy, store, caps);
let entry_id = EntryId::new("test-game"); let entry_id = EntryId::new("test-game");
let now = Local::now(); let now = shepherd_util::now();
let now_mono = MonotonicInstant::now(); let now_mono = MonotonicInstant::now();
// Start session // Start session