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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
// SPDX-License-Identifier: AGPL-3.0-only
//! Pin every number the tutorials quote, so a tutorial can never silently lie.
//!
//! Each annotated teaching scenario under `docs/tutorials/scenarios/` is pulled in
//! with `include_str!`, run through the public API (`run_toml` / `export_sp3` /
//! `list_scenario_kinds`), and asserted against the tutorial's headline numbers.
//! Every assertion is anchored to an EXTERNAL, non-circular oracle named in the
//! tutorial and in this file's comments — never a self-comparison or an unsourced
//! magic number. This makes each documented figure a CI-enforced contract: the
//! moment the engine drifts from a tutorial's quoted output, the build goes red.
//!
//! Mirrors the `dispatches_each_kind_*` pattern in `src/api.rs` and the structure
//! of `tests/sgp4_verification.rs`.
/// Every tutorial scenario kind is a real dispatch kind (guards against
/// documenting a kind that does not exist). Oracle: the self-describing
/// `list_scenario_kinds` API in `src/api.rs`.
#[test]
fn tutorial_scenarios_use_real_kinds() {
let kinds: std::collections::HashSet<_> = kshana::api::list_scenario_kinds()
.iter()
.map(|m| m.name)
.collect();
for k in [
"clock",
"orbit",
"integrity",
"spoof",
"hybrid",
"inertial",
"timetransfer",
"gnss-sim",
] {
assert!(
kinds.contains(k),
"tutorial kind {k} missing from list_scenario_kinds"
);
}
}
/// Every teaching scenario, its source, and the canonical parent under
/// `scenarios/` whose field values `docs/tutorials/README.md` promises it
/// reproduces. One table drives the run, the expected-summary and the
/// parent-identity checks so the three cannot drift apart.
const TEACHING: &[(&str, &str, &str)] = &[
(
"clock.toml",
include_str!("../docs/tutorials/scenarios/clock.toml"),
include_str!("../scenarios/clock-holdover.toml"),
),
(
"orbit.toml",
include_str!("../docs/tutorials/scenarios/orbit.toml"),
include_str!("../scenarios/orbit-sgp4-gps.toml"),
),
(
"integrity.toml",
include_str!("../docs/tutorials/scenarios/integrity.toml"),
include_str!("../scenarios/integrity-raim.toml"),
),
(
"security.toml",
include_str!("../docs/tutorials/scenarios/security.toml"),
include_str!("../scenarios/spoof-attack.toml"),
),
(
"hybrid.toml",
include_str!("../docs/tutorials/scenarios/hybrid.toml"),
include_str!("../scenarios/hybrid-pnt.toml"),
),
(
"inertial.toml",
include_str!("../docs/tutorials/scenarios/inertial.toml"),
include_str!("../scenarios/imu-deadreckoning.toml"),
),
(
"timetransfer.toml",
include_str!("../docs/tutorials/scenarios/timetransfer.toml"),
include_str!("../scenarios/timetransfer.toml"),
),
(
"gnss-sim.toml",
include_str!("../docs/tutorials/scenarios/gnss-sim.toml"),
include_str!("../scenarios/gnss-sim-raim.toml"),
),
];
/// Every annotated teaching scenario runs end-to-end and emits the unified output
/// envelope (JSON + SVG + summary). Mirrors `api.rs::dispatches_each_kind`.
#[test]
fn annotated_tutorial_scenarios_run() {
for (name, src, _) in TEACHING {
let out = kshana::api::run_toml(src).expect("tutorial scenario runs");
assert!(out.json.starts_with('{'), "{name}");
assert!(out.svg.starts_with("<svg"), "{name}");
assert!(!out.summary.is_empty(), "{name}");
}
}
/// The `TEACHING` table must name every file in the tutorials directory. Without
/// this, a ninth teaching scenario could be added with no entry and every check
/// below would grade a set that silently excludes it.
#[test]
fn the_teaching_table_covers_the_whole_tutorials_directory() {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("docs/tutorials/scenarios");
let mut on_disk: Vec<String> = std::fs::read_dir(&dir)
.expect("docs/tutorials/scenarios must exist")
.map(|e| {
e.expect("dir entry")
.file_name()
.to_string_lossy()
.into_owned()
})
.filter(|n| n.ends_with(".toml"))
.collect();
on_disk.sort();
let mut tabled: Vec<String> = TEACHING.iter().map(|(n, _, _)| n.to_string()).collect();
tabled.sort();
assert_eq!(
on_disk, tabled,
"the tutorials directory and the TEACHING table disagree"
);
assert!(!on_disk.is_empty(), "read_dir matched no teaching scenario");
}
/// Pull the `# expected:` one-line summary a teaching scenario records for itself.
fn recorded_summary(src: &str) -> &str {
src.lines()
.find_map(|l| l.trim().strip_prefix("# expected:"))
.map(str::trim)
.expect("every teaching scenario records an `# expected:` summary")
}
/// Compare one `|`-separated segment, treating the `<hash>` placeholder some
/// recorded summaries carry as a wildcard for the content-addressed scenario id.
fn segment_matches(expected: &str, actual: &str) -> bool {
match expected.split_once("<hash>") {
None => expected == actual,
Some((pre, post)) => {
actual.len() > pre.len() + post.len()
&& actual.starts_with(pre)
&& actual.ends_with(post)
}
}
}
/// The `# expected:` line each teaching scenario records is the output a reader is
/// promised, so it must be the output the engine produces. Nothing else in the
/// repo reads these lines: without this test they are prose, and an edit to a
/// teaching copy moves the real summary while the recorded one stays stale.
#[test]
fn teaching_scenarios_match_their_recorded_expected_summary() {
for (name, src, _) in TEACHING {
let expected = recorded_summary(src);
let actual = kshana::api::run_toml(src)
.unwrap_or_else(|e| panic!("{name} runs: {e}"))
.summary;
let want: Vec<&str> = expected.split(" | ").collect();
let got: Vec<&str> = actual.split(" | ").collect();
assert_eq!(
want.len(),
got.len(),
"{name}: recorded summary has {} segments, the engine emits {}\n recorded: {expected}\n actual: {actual}",
want.len(),
got.len()
);
for (w, g) in want.iter().zip(&got) {
assert!(
segment_matches(w, g),
"{name}: recorded `{w}` but the engine emits `{g}`\n recorded: {expected}\n actual: {actual}"
);
}
}
}
/// `docs/tutorials/README.md` promises the teaching copies carry the parent
/// scenario's field values, with only the commentary added. Comparing parsed TOML
/// (not text) is exactly that claim: comments and layout are free to differ, every
/// value must not. Without it, a parent edit plus a golden regen can leave the
/// teaching copy behind with a green build.
#[test]
fn teaching_copies_carry_their_parents_field_values() {
for (name, teaching, parent) in TEACHING {
let t: toml::Value =
toml::from_str(teaching).unwrap_or_else(|e| panic!("{name} parses: {e}"));
let p: toml::Value =
toml::from_str(parent).unwrap_or_else(|e| panic!("parent of {name} parses: {e}"));
assert_eq!(
t, p,
"{name} has drifted from its parent in scenarios/; the tutorials README \
promises the field values are identical"
);
}
}
/// Tutorial 1 — orbit / GPS headline numbers. Oracle: the GPS shell geometry and
/// the PDOP·σ_UERE position-sigma identity (Misra & Enge, *Global Positioning
/// System*, 2nd ed.).
#[test]
fn tutorial1_orbit_headline_holds() {
let out =
kshana::api::run_toml(include_str!("../docs/tutorials/scenarios/orbit.toml")).unwrap();
let v: serde_json::Value = serde_json::from_str(&out.json).unwrap();
let g = &v["geometry"];
// best PDOP ~1.07; position sigma = PDOP * sigma_uere, and sigma_uere = 1.0
// here, so the two are numerically equal (the identity, not "sigma == PDOP").
let pdop = g["best_pdop"].as_f64().unwrap();
assert!((pdop - 1.071).abs() < 0.01, "best PDOP {pdop}");
assert!((g["best_position_sigma_m"].as_f64().unwrap() - pdop).abs() < 1e-6);
assert_eq!(g["samples_total"].as_u64().unwrap(), 361);
// 345/361 GNSS-nominal -> > 90 % (a real GPS shell seen from a LEO user).
assert!(out.summary.contains("345/361"));
}
/// Tutorial 1 — the "where are the satellites" claim: PG01 must lie on the real GPS
/// MEO shell. Oracle: GPS semi-major axis a ≈ 26,560 km (IS-GPS-200 / GPS SPS
/// Performance Standard), EXTERNAL to Kshana.
#[test]
fn tutorial1_satellites_are_in_the_gps_meo_shell() {
let sp3 =
kshana::api::export_sp3(include_str!("../docs/tutorials/scenarios/orbit.toml")).unwrap();
// First-epoch PG01 line: parse the 3 ECEF km, geocentric radius must be the
// GPS shell radius.
let line = sp3.lines().find(|l| l.starts_with("PG01")).unwrap();
let xs: Vec<f64> = line[4..]
.split_whitespace()
.take(3)
.map(|s| s.parse().unwrap())
.collect();
let r = (xs[0] * xs[0] + xs[1] * xs[1] + xs[2] * xs[2]).sqrt();
// GPS a ~ 26,560 km; accept the eccentricity band around it.
assert!(
(26000.0..27200.0).contains(&r),
"PG01 geocentric radius {r} km not in the GPS MEO shell"
);
}
/// Tutorial 2 — clock holdover ordering + the CSAC value. Oracle: the NIST SP 1065
/// white-FM phase-error law σ_x(T) = √(q_wf·T); for the CSAC the k-σ/p95 spec-cross
/// sits in the SP-1065 band (a band, not an exact magic number).
#[test]
fn tutorial2_clock_holdover_holds() {
let out =
kshana::api::run_toml(include_str!("../docs/tutorials/scenarios/clock.toml")).unwrap();
let v: serde_json::Value = serde_json::from_str(&out.json).unwrap();
let q = v["quantum"]["fom"]["holdover_s"].as_f64().unwrap();
let c = v["classical"]["fom"]["holdover_s"].as_f64().unwrap();
assert!(q >= c, "optical must hold >= CSAC");
assert!((q - 6600.0).abs() < 1.0, "optical holds the full outage");
// CSAC breaches 20 ns near the white-FM crossing time (NIST SP 1065).
assert!(
(2000.0..3200.0).contains(&c),
"CSAC holdover {c}s off the SP-1065 prediction band"
);
}
/// Tutorial 3 (Part A) — spoof detector: the closed-form χ²₁ P_md and the
/// Monte-Carlo P_md must agree. Oracle: two INDEPENDENT computations of the same
/// probability (Kay, *Detection Theory*) — non-circular by construction.
#[test]
fn tutorial3_spoof_analytic_matches_montecarlo() {
let out =
kshana::api::run_toml(include_str!("../docs/tutorials/scenarios/security.toml")).unwrap();
let v: serde_json::Value = serde_json::from_str(&out.json).unwrap();
for side in ["quantum", "classical"] {
let a = v[side]["detection"]["analytic_pmd"].as_f64().unwrap();
let m = v[side]["detection"]["mc_pmd"].as_f64().unwrap();
assert!(
(a - m).abs() < 0.05,
"{side} analytic {a} vs MC {m} disagree"
);
}
// Quantum security ~ 1.0 (catches the spec-sized spoof) >> classical.
assert!(v["quantum"]["security_fom"].as_f64().unwrap() > 0.9);
}
/// The reproducibility lesson (Tier-3 exercise claim) is true: the same source
/// yields an identical result on two runs. Oracle: the repo's own determinism
/// guarantee (`scripts/check-reproducible.sh`).
#[test]
fn tutorial_reproducibility_is_bit_stable() {
let s = include_str!("../docs/tutorials/scenarios/clock.toml");
let a = kshana::api::run_toml(s).unwrap().json;
let b = kshana::api::run_toml(s).unwrap().json;
assert_eq!(a, b, "deterministic engine");
}