dpm-model 0.1.0

Domain model for DPM execution planning
Documentation
use super::*;
use chrono::TimeZone;

fn t0() -> DateTime<Utc> {
    Utc.with_ymd_and_hms(2026, 9, 1, 12, 0, 0)
        .single()
        .expect("fixed time")
}

fn hours(h: i64) -> DateTime<Utc> {
    t0() + TimeDelta::hours(h)
}

#[test]
fn positive_lag_releases_exactly_when_it_has_elapsed() {
    let event = Some(EventTime::Recorded(t0()));
    assert_eq!(
        Release::evaluate(event, 24.0, hours(23)),
        Release::Elapsing {
            event_at: t0(),
            opens_at: hours(24)
        }
    );
    assert_eq!(
        Release::evaluate(event, 24.0, hours(24)),
        Release::Released {
            at: EventTime::Recorded(hours(24))
        }
    );
    let almost = hours(24) - TimeDelta::milliseconds(1);
    assert!(matches!(
        Release::evaluate(event, 24.0, almost),
        Release::Elapsing { .. }
    ));
}

#[test]
fn negative_lag_never_releases_before_the_event() {
    assert_eq!(
        Release::evaluate(None, -48.0, hours(1000)),
        Release::AwaitingEvent
    );
    assert_eq!(
        Release::evaluate(Some(EventTime::Recorded(hours(5))), -48.0, hours(5)),
        Release::Released {
            at: EventTime::Recorded(hours(5))
        }
    );
    assert!(matches!(
        Release::evaluate(Some(EventTime::Recorded(hours(5))), -48.0, hours(4)),
        Release::Elapsing { .. }
    ));
}

#[test]
fn unrecorded_event_times_block_only_positive_lag() {
    let unknown = Some(EventTime::Unrecorded);
    assert_eq!(
        Release::evaluate(unknown, 0.0, t0()),
        Release::Released {
            at: EventTime::Unrecorded
        }
    );
    assert_eq!(
        Release::evaluate(unknown, -3.0, t0()),
        Release::Released {
            at: EventTime::Unrecorded
        }
    );
    assert_eq!(
        Release::evaluate(unknown, 0.5, hours(100_000)),
        Release::UnrecordedEventTime
    );
}

#[test]
fn unrepresentable_lag_stays_closed_without_panicking() {
    for lag in [1.0e300, f64::MAX] {
        assert_eq!(
            Release::evaluate(Some(EventTime::Recorded(t0())), lag, hours(1)),
            Release::LagOutOfRange { event_at: t0() }
        );
    }
}

#[test]
fn unknown_times_dominate_the_latest_event() {
    let known = EventTime::Recorded(t0());
    assert_eq!(
        known.latest(EventTime::Recorded(hours(2))),
        EventTime::Recorded(hours(2))
    );
    assert_eq!(known.latest(EventTime::Unrecorded), EventTime::Unrecorded);
    assert_eq!(EventTime::Unrecorded.latest(known), EventTime::Unrecorded);
}

#[test]
fn empty_events_are_omitted_and_old_snapshots_load() {
    let events = ExecutionEvents::default();
    assert!(events.is_empty());
    assert_eq!(serde_json::to_string(&events).expect("json"), "{}");
    let loaded: ExecutionEvents =
        serde_json::from_str(r#"{"started_at":"2026-09-01T12:00:00Z"}"#).expect("json");
    assert_eq!(loaded.started_at, Some(t0()));
}