File size: 4,931 Bytes
7596726
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
//! Unit tests for the retained-job SSE payload contract.

use super::*;
use crate::domain::{Employee, Shift};
use serde_json::Value;
use solverforge::{
    SolverLifecycleState, SolverSnapshot, SolverStatus, SolverTelemetry, SolverTerminalReason,
};

#[test]
fn failed_events_use_stock_payload_fields() {
    let payload: Value = serde_json::from_str(&event_payload(
        7,
        "failed",
        &SolverEventMetadata {
            job_id: 7,
            event_sequence: 4,
            lifecycle_state: SolverLifecycleState::Failed,
            terminal_reason: Some(SolverTerminalReason::Failed),
            snapshot_revision: Some(3),
            current_score: None,
            best_score: None,
            telemetry: SolverTelemetry::default(),
        },
        None,
        Some("boom"),
    ))
    .unwrap();

    assert_eq!(payload["id"], "7");
    assert_eq!(payload["jobId"], "7");
    assert_eq!(payload["eventType"], "failed");
    assert_eq!(payload["lifecycleState"], "FAILED");
    assert_eq!(payload["terminalReason"], "failed");
    assert_eq!(payload["error"], "boom");
}

#[test]
fn event_payload_derives_stock_telemetry_fields_from_exact_runtime_telemetry() {
    let payload: Value = serde_json::from_str(&event_payload(
        5,
        "progress",
        &SolverEventMetadata {
            job_id: 5,
            event_sequence: 2,
            lifecycle_state: SolverLifecycleState::Solving,
            terminal_reason: None,
            snapshot_revision: Some(1),
            current_score: Some(HardSoftDecimalScore::ZERO),
            best_score: Some(HardSoftDecimalScore::ZERO),
            telemetry: SolverTelemetry {
                elapsed: std::time::Duration::from_millis(2_500),
                step_count: 9,
                moves_generated: 300,
                moves_evaluated: 200,
                moves_accepted: 50,
                score_calculations: 80,
                generation_time: std::time::Duration::from_millis(400),
                evaluation_time: std::time::Duration::from_millis(900),
                ..SolverTelemetry::default()
            },
        },
        None,
        None,
    ))
    .unwrap();

    assert_eq!(payload["telemetry"]["elapsedMs"], 2500);
    assert_eq!(payload["telemetry"]["stepCount"], 9);
    assert_eq!(payload["telemetry"]["movesGenerated"], 300);
    assert_eq!(payload["telemetry"]["movesEvaluated"], 200);
    assert_eq!(payload["telemetry"]["movesAccepted"], 50);
    assert_eq!(payload["telemetry"]["scoreCalculations"], 80);
    assert_eq!(payload["telemetry"]["generationMs"], 400);
    assert_eq!(payload["telemetry"]["evaluationMs"], 900);
    assert_eq!(payload["telemetry"]["movesPerSecond"], 80);
    assert_eq!(payload["telemetry"]["acceptanceRate"], 0.25);
}

#[test]
fn snapshot_bootstrap_payload_exposes_live_solution_and_ui_score_fields() {
    let mut solution = Plan::new(
        vec![Employee::new(0, "Alex").with_skill("Doctor")],
        vec![{
            let mut shift = Shift::new(
                "shift-1",
                chrono::NaiveDate::from_ymd_opt(2024, 1, 1)
                    .unwrap()
                    .and_hms_opt(8, 0, 0)
                    .unwrap(),
                chrono::NaiveDate::from_ymd_opt(2024, 1, 1)
                    .unwrap()
                    .and_hms_opt(16, 0, 0)
                    .unwrap(),
                "ER",
                "Doctor",
            );
            shift.employee_idx = Some(0);
            shift
        }],
    );
    solution.score = Some(HardSoftDecimalScore::ZERO);

    let status = SolverStatus {
        job_id: 11,
        lifecycle_state: SolverLifecycleState::Solving,
        terminal_reason: None,
        checkpoint_available: true,
        event_sequence: 9,
        latest_snapshot_revision: Some(4),
        current_score: None,
        best_score: None,
        telemetry: SolverTelemetry::default(),
    };
    let snapshot = SolverSnapshot {
        job_id: 11,
        snapshot_revision: 4,
        lifecycle_state: SolverLifecycleState::Solving,
        terminal_reason: None,
        current_score: Some(HardSoftDecimalScore::ZERO),
        best_score: Some(HardSoftDecimalScore::ZERO),
        telemetry: SolverTelemetry::default(),
        solution,
    };

    let payload: Value = serde_json::from_str(&snapshot_status_event_payload(
        11,
        bootstrap_snapshot_event_type(status.lifecycle_state),
        &status,
        &snapshot,
    ))
    .unwrap();

    assert_eq!(payload["eventType"], "best_solution");
    assert_eq!(payload["lifecycleState"], "SOLVING");
    assert_eq!(payload["snapshotRevision"], 4);
    assert_eq!(payload["currentScore"], "0hard/0soft");
    assert_eq!(payload["bestScore"], "0hard/0soft");
    assert!(payload["solution"].is_object());
    assert_eq!(payload["solution"]["shifts"][0]["employeeIdx"], 0);
    assert_eq!(payload["solution"]["score"], "0hard/0soft");
}