use kshana::fom::{FoMScores, Sample};
use kshana::interchange::SCHEMA_VERSION;
use kshana::report::{hash_scenario, ClockRun, RunResult, StudyMeta};
use kshana::scenario::{ClockCfg, GnssState, GnssTimeline, GnssWindow, Scenario, TimeCfg};
use kshana::types::ModelSpec;
fn demo_scenario() -> Scenario {
Scenario {
seed: 1,
threshold_ns: 100.0,
runs: 1,
time: TimeCfg {
step_s: 10.0,
duration_s: 60.0,
},
gnss: GnssTimeline {
windows: vec![
GnssWindow {
t0: 0.0,
t1: 30.0,
state: GnssState::Nominal,
},
GnssWindow {
t0: 30.0,
t1: 60.0,
state: GnssState::Denied,
},
],
},
clock_quantum: ClockCfg {
id: "q".into(),
provenance: "d".into(),
y0: 1e-13,
q_wf: 1e-26,
q_rw: 1e-32,
drift: 0.0,
flicker_floor: 0.0,
},
clock_classical: ClockCfg {
id: "c".into(),
provenance: "d".into(),
y0: 1e-11,
q_wf: 1e-24,
q_rw: 1e-30,
drift: 0.0,
flicker_floor: 0.0,
},
}
}
fn run_of(id: &str) -> ClockRun {
ClockRun {
spec: ModelSpec {
id: id.into(),
kind: "clock".into(),
provenance: "x".into(),
params: serde_json::json!({}),
},
series: vec![Sample {
t: 0.0,
error_ns: 0.0,
gnss: GnssState::Nominal,
}],
fom: FoMScores {
timing_rms_ns: 0.0,
timing_p95_ns: 0.0,
holdover_s: 0.0,
resilience_slope_ns_per_s: 0.0,
availability: 1.0,
integrity: None,
security: None,
},
adev_curve: vec![],
filter_health: None,
}
}
fn result_with_meta(meta: Option<StudyMeta>) -> RunResult {
RunResult {
schema_version: SCHEMA_VERSION.into(),
engine_version: "test".into(),
scenario_hash: "abc".into(),
seed: 1,
threshold_ns: 20.0,
quantum: run_of("optical"),
classical: run_of("csac"),
eci_track: None,
meta,
}
}
#[test]
fn no_meta_means_no_meta_key_in_json() {
let json = serde_json::to_string(&result_with_meta(None)).unwrap();
assert!(
!json.contains("\"meta\""),
"meta=None must not serialize a \"meta\" key (byte back-compat): {json}"
);
}
#[test]
fn empty_study_meta_omits_all_optional_fields() {
let meta = StudyMeta {
study_title: None,
generated_utc: None,
author: None,
disclaimer: None,
};
let json = serde_json::to_string(&meta).unwrap();
assert_eq!(json, "{}", "all-None StudyMeta must serialize to '{{}}'");
}
#[test]
fn present_meta_is_serialized() {
let meta = StudyMeta {
study_title: Some("My Study".into()),
generated_utc: Some("2026-06-23T00:00:00Z".into()),
author: None,
disclaimer: None,
};
let json = serde_json::to_string(&result_with_meta(Some(meta))).unwrap();
assert!(
json.contains("\"meta\""),
"present meta must serialize: {json}"
);
assert!(json.contains("\"study_title\":\"My Study\""));
assert!(json.contains("\"generated_utc\":\"2026-06-23T00:00:00Z\""));
assert!(!json.contains("\"author\""));
assert!(!json.contains("\"disclaimer\""));
}
#[test]
fn scenario_hash_is_independent_of_any_meta() {
let scn = demo_scenario();
let baseline = hash_scenario(&scn);
let meta = StudyMeta {
study_title: Some("Whatever".into()),
generated_utc: Some("2026-06-23T12:00:00Z".into()),
author: Some("Author".into()),
disclaimer: Some("MODELLED".into()),
};
let _withed = result_with_meta(Some(meta));
let again = hash_scenario(&scn);
assert_eq!(baseline, again);
assert_eq!(baseline.len(), 64);
}