blackopsrepl's picture
chore(deps): update SolverForge runtime stack
8aa703c
//! DTO tests that keep transport JSON and generated UI metadata aligned.
use super::*;
use serde::Deserialize;
use solverforge::ConstraintSet;
use std::fs;
use std::time::Duration;
#[derive(Deserialize)]
struct UiModel {
entities: Vec<UiNamedEntry>,
facts: Vec<UiNamedEntry>,
constraints: Vec<UiConstraint>,
views: Vec<UiView>,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
struct UiConstraint {
name: String,
#[serde(rename = "type")]
constraint_type: String,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
struct UiNamedEntry {
name: String,
plural: String,
label: String,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
struct UiView {
id: String,
kind: String,
label: String,
entity: String,
entity_plural: String,
source_plural: String,
variable_field: String,
allows_unassigned: bool,
}
#[test]
fn plan_dto_returns_decode_errors_for_semantically_invalid_payloads() {
let dto = PlanDto {
fields: Map::new(),
score: None,
};
assert!(dto.to_domain().is_err());
}
#[test]
fn runtime_telemetry_derives_stock_transport_fields() {
let telemetry = SolverTelemetry {
elapsed: Duration::from_millis(2_500),
step_count: 9,
moves_generated: 300,
moves_evaluated: 200,
moves_accepted: 50,
score_calculations: 80,
generation_time: Duration::from_millis(400),
evaluation_time: Duration::from_millis(900),
..SolverTelemetry::default()
};
let dto = TelemetryDto::from_runtime(&telemetry);
assert_eq!(dto.elapsed_ms, 2_500);
assert_eq!(dto.step_count, 9);
assert_eq!(dto.moves_generated, 300);
assert_eq!(dto.moves_evaluated, 200);
assert_eq!(dto.moves_accepted, 50);
assert_eq!(dto.score_calculations, 80);
assert_eq!(dto.generation_ms, 400);
assert_eq!(dto.evaluation_ms, 900);
assert_eq!(dto.moves_per_second, 80);
assert!((dto.acceptance_rate - 0.25).abs() < f64::EPSILON);
}
#[test]
fn analyzed_constraint_names_match_ui_model() {
let ui_model_path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/static/generated/ui-model.json"
);
let ui_model: UiModel =
serde_json::from_str(&fs::read_to_string(ui_model_path).unwrap()).unwrap();
let plan = Plan::new(Vec::new(), Vec::new());
let constraints = crate::constraints::create_constraints();
let analysis = constraints.evaluate_detailed(&plan);
let analyzed_constraints: Vec<String> = analysis
.iter()
.map(|analysis| analysis.constraint_ref.name.to_string())
.collect();
let ui_constraints: Vec<String> = ui_model
.constraints
.iter()
.map(|constraint| constraint.name.clone())
.collect();
assert_eq!(analyzed_constraints, ui_constraints);
assert_eq!(
ui_model.entities,
vec![UiNamedEntry {
name: "shift".to_string(),
plural: "shifts".to_string(),
label: "Shifts".to_string(),
}]
);
assert_eq!(
ui_model.facts,
vec![UiNamedEntry {
name: "employee".to_string(),
plural: "employees".to_string(),
label: "Employees".to_string(),
}]
);
assert_eq!(
ui_model.views,
vec![
UiView {
id: "by-location".to_string(),
kind: "schedule-by-location".to_string(),
label: "By location".to_string(),
entity: "shift".to_string(),
entity_plural: "shifts".to_string(),
source_plural: "employees".to_string(),
variable_field: "employeeIdx".to_string(),
allows_unassigned: true,
},
UiView {
id: "by-employee".to_string(),
kind: "schedule-by-employee".to_string(),
label: "By employee".to_string(),
entity: "shift".to_string(),
entity_plural: "shifts".to_string(),
source_plural: "employees".to_string(),
variable_field: "employeeIdx".to_string(),
allows_unassigned: true,
}
]
);
}