//! T-1181 acceptance gate — cache-hit derive path == cache-miss derive path, //! byte-identical, for EVERY D-255(a) rung. //! //! This is the mandatory gate the ticket names as its most important //! deliverable (D-227 amendment (3), D-255(f)): a step canvas is //! `derive(seed, position)`, a pure function — evicting a cache entry and //! re-deriving it must produce byte-identical output, never merely //! "close enough." The same shape as T-1170's window-independence invariant. //! //! **What "cache-hit" vs "cache-miss" means here, precisely:** the SERVER //! derive core (`step_canvas::build_step_canvas`) never itself reads a //! cache — `StepCanvasCache`/`GlobalTierCache` are populated OUTSIDE the //! derive core, by the gen_queue completion handler (`plugin.rs`). So //! "cache-hit == cache-miss" is proven by construction (there is only ONE //! code path that produces a canvas — `build_step_canvas` — and every //! caller, whether serving a first request or a Nth repeat, calls it //! identically) UNLESS a future change introduces a genuine second path //! (e.g. D-255(f) mechanism-B acceleration reading a resident coarser //! canvas as an input). This suite pins the invariant directly at the //! derive-core level (call `build_step_canvas` twice, independently, same //! inputs, assert byte-identical) AND at the cache level (insert once, //! `get` it back via `StepCanvasCache`/`GlobalTierCache`, assert the //! round-tripped bytes match a fresh independent derive) — so this test //! remains the correctness gate even after a future acceleration path //! lands, per D-227 amendment (3)'s "mandatory determinism test." //! //! Run: `cargo test --test step_canvas_acceptance_gate` use settled_reach_server::atlas::body_world_state::RiverNetwork; use settled_reach_server::atlas::district_profile::{BodyParams, ClimateConstants}; use settled_reach_server::atlas::drainage; use settled_reach_server::atlas::features::TerrainAnalysis; use settled_reach_server::atlas::heightmap::BodyHeightmap; use settled_reach_server::atlas::step_canvas::{ build_step_canvas, decode_step_canvas, encode_step_canvas, BodyDrivingClockClass, GlobalTierCache, StepCanvasCache, StepCanvasRung, }; use settled_reach_server::seed::{SeedChain, SeedDomain}; /// Same fixture-building convention `bmv_gridunit_bench.rs`/ /// `window_derivation_golden.rs` already establish — a deterministic /// gradient heightmap, small enough to run every rung's canvas at a modest /// (non-benchmark) extent in a plain `cargo test` run. fn fixture_hm() -> BodyHeightmap { let (w, h) = (128u32, 64u32); let n = (w * h) as usize; let data = (0..n) .map(|i| { let r = (i / w as usize) as f32 / h as f32; let c = (i % w as usize) as f32 / w as f32; let ripple = (c * std::f32::consts::TAU * 3.0).sin() * 0.08; (r * 0.6 + c * 0.4 + ripple).clamp(0.0, 1.0) }) .collect(); BodyHeightmap { body_id: "gate-body".into(), width: w, height: h, data, sea_level: 0.3, } } fn fixture_ta(hm: &BodyHeightmap) -> TerrainAnalysis { let dr = drainage::analyze(&hm.data, hm.width, hm.height, hm.sea_level); TerrainAnalysis::analyze(hm, &dr) } fn fixture_river_network(hm: &BodyHeightmap) -> RiverNetwork { drainage::analyze(&hm.data, hm.width, hm.height, hm.sea_level).river_network } fn fixture_params() -> BodyParams { BodyParams { hydrosphere: Some("ocean".into()), atmosphere: Some("breathable".into()), planet_class: Some("temperate".into()), body_radius_km: Some(6371.0), ..Default::default() } } /// A small, fast canvas extent for the gate suite — the invariant being /// tested (byte-identical repeat derivation) does not require production /// canvas sizes; those are already covered by the workshop's dedicated /// bench files (`bmv_gridunit_bench.rs`, `bmv_global_tier_bench.rs`). const GATE_EXTENT: (u32, u32) = (24, 18); /// Every D-255(a) rung, in ladder order — the gate's "for EVERY rung" /// requirement, checked exhaustively rather than sampled. const ALL_RUNGS: [StepCanvasRung; 6] = [ StepCanvasRung::Global, StepCanvasRung::Region, StepCanvasRung::District, StepCanvasRung::Quarter, StepCanvasRung::Block, StepCanvasRung::Chunk, ]; /// Core gate: call `build_step_canvas` twice, independently (two fresh /// `TerrainAnalysis`/`RiverNetwork` builds, not two reads of one shared /// value — the strongest form of "cache-miss twice"), same /// `(seed, body, position)` inputs, and assert byte-identical `RawStepCanvas` /// output for every rung. #[test] fn cache_hit_equals_cache_miss_every_rung() { let seed = SeedChain::root(0xACCE97_u64).derive(SeedDomain::Body, 1); let climate = ClimateConstants::default(); let center = (12_288_i64, -8_192_i64); // an arbitrary non-origin centre for rung in ALL_RUNGS { // Independent build #1 ("cache-miss" run A) — fresh heightmap load, // fresh drainage analysis, fresh TerrainAnalysis. let hm_a = fixture_hm(); let ta_a = fixture_ta(&hm_a); let rn_a = fixture_river_network(&hm_a); let params_a = fixture_params(); let canvas_a = build_step_canvas( seed, "gate-body", ¶ms_a, &ta_a, &rn_a, &[], rung, center, GATE_EXTENT, &climate, 0, ); // Independent build #2 ("cache-miss" run B — simulating what a // second, later request after eviction would recompute) — every // input rebuilt from scratch again, not reused from run A. let hm_b = fixture_hm(); let ta_b = fixture_ta(&hm_b); let rn_b = fixture_river_network(&hm_b); let params_b = fixture_params(); let canvas_b = build_step_canvas( seed, "gate-body", ¶ms_b, &ta_b, &rn_b, &[], rung, center, GATE_EXTENT, &climate, 0, ); assert_eq!( canvas_a, canvas_b, "rung {rung:?}: two independent derive_step_canvas builds diverged — \ D-227 purity violated (cache-hit/cache-miss byte-identity gate)" ); } } /// The same gate at the ENCODED wire level (PNG-per-field round-trip) — a /// separate property from the raw-derive gate above: even if the derive /// core is pure, a non-deterministic or lossy encoder would still break the /// "same request, same bytes on the wire" guarantee a client's persistent /// cache (D-227 amendment (2)) depends on. #[test] fn encoded_canvas_round_trip_is_lossless_every_rung() { let seed = SeedChain::root(0xACCE97_u64).derive(SeedDomain::Body, 2); let climate = ClimateConstants::default(); let center = (2_048_i64, 4_096_i64); let hm = fixture_hm(); let ta = fixture_ta(&hm); let rn = fixture_river_network(&hm); let params = fixture_params(); for rung in ALL_RUNGS { let raw = build_step_canvas( seed, "gate-body", ¶ms, &ta, &rn, &[], rung, center, GATE_EXTENT, &climate, 0, ); let encoded = encode_step_canvas(&raw); let decoded = decode_step_canvas(&encoded); assert_eq!( raw, decoded, "rung {rung:?}: PNG-per-field encode/decode round-trip lost data" ); // Encoding itself is deterministic — encode the SAME raw canvas // twice and expect byte-identical PNG output (never "usually // matches"). This is the property a persistent client cache // (D-227 amendment (2)) needs: identical input bytes -> identical // stored bytes, so a schema/version-tag comparison is even // meaningful. let encoded_again = encode_step_canvas(&raw); assert_eq!( encoded.morphology.png_bytes, encoded_again.morphology.png_bytes, "rung {rung:?}: PNG encoder is non-deterministic on identical input" ); } } /// The gate at the CACHE-TIER level: insert a derived canvas into /// `GlobalTierCache`/`StepCanvasCache`, read it back, and confirm the /// round-tripped bytes match a fresh independent derive — proving the cache /// layer is a transparent accelerator (D-227 amendment (3): "an /// optimization, not a semantic dependency") rather than a second source of /// truth that could silently diverge from the derive core. #[test] fn global_tier_cache_round_trip_matches_fresh_derive() { let seed = SeedChain::root(0xACCE97_u64).derive(SeedDomain::Body, 3); let climate = ClimateConstants::default(); let hm = fixture_hm(); let ta = fixture_ta(&hm); let rn = fixture_river_network(&hm); let params = fixture_params(); let fresh_raw = build_step_canvas( seed, "gate-body", ¶ms, &ta, &rn, &[], StepCanvasRung::Global, (0, 0), GATE_EXTENT, // ignored for Global — extent is the body's region grid &climate, 0, ); let fresh_encoded = encode_step_canvas(&fresh_raw); let mut cache = GlobalTierCache::new(); assert!(cache.get("gate-body").is_none()); cache.insert("gate-body".to_string(), fresh_encoded.clone()); assert!(cache.contains("gate-body")); let cached = cache .get("gate-body") .expect("global tier cache hit after insert"); assert_eq!( cached, &fresh_encoded, "GlobalTierCache round-trip diverged from the freshly-derived canvas" ); // Re-derive fresh AGAIN (simulating a cold-cache request that never // touched this cache instance at all) and confirm it still matches the // cached bytes — this is the eviction->recompute->byte-identical // property D-227 requires. let hm2 = fixture_hm(); let ta2 = fixture_ta(&hm2); let rn2 = fixture_river_network(&hm2); let params2 = fixture_params(); let recomputed_raw = build_step_canvas( seed, "gate-body", ¶ms2, &ta2, &rn2, &[], StepCanvasRung::Global, (0, 0), GATE_EXTENT, &climate, 0, ); let recomputed_encoded = encode_step_canvas(&recomputed_raw); assert_eq!( cached, &recomputed_encoded, "eviction->recompute path diverged from the cached global-tier canvas" ); } /// Same cache-tier gate for `StepCanvasCache` (fixed rungs 1-5), across /// every fixed rung. #[test] fn step_canvas_cache_round_trip_matches_fresh_derive_every_fixed_rung() { let seed = SeedChain::root(0xACCE97_u64).derive(SeedDomain::Body, 4); let climate = ClimateConstants::default(); let center = (512_i64, -1_024_i64); let extent = GATE_EXTENT; let min_wl_m = 0u32; let body_class = BodyDrivingClockClass::Moonless; for rung in ALL_RUNGS { if rung.is_global() { continue; // covered by global_tier_cache_round_trip_matches_fresh_derive } let hm = fixture_hm(); let ta = fixture_ta(&hm); let rn = fixture_river_network(&hm); let params = fixture_params(); let fresh_raw = build_step_canvas( seed, "gate-body", ¶ms, &ta, &rn, &[], rung, center, extent, &climate, min_wl_m, ); let fresh_encoded = encode_step_canvas(&fresh_raw); let mut cache = StepCanvasCache::new(8); let key = ("gate-body".to_string(), rung, center, extent, min_wl_m); assert!(cache.get(&key, 0, body_class).is_none()); cache.insert(key.clone(), fresh_encoded.clone(), 0); let cached = cache .get(&key, 1, body_class) .unwrap_or_else(|| panic!("rung {rung:?}: cache hit expected after insert")); assert_eq!( cached, fresh_encoded, "rung {rung:?}: StepCanvasCache round-trip diverged from the freshly-derived canvas" ); // Independent re-derive (simulating eviction) still matches. let hm2 = fixture_hm(); let ta2 = fixture_ta(&hm2); let rn2 = fixture_river_network(&hm2); let params2 = fixture_params(); let recomputed_raw = build_step_canvas( seed, "gate-body", ¶ms2, &ta2, &rn2, &[], rung, center, extent, &climate, min_wl_m, ); let recomputed_encoded = encode_step_canvas(&recomputed_raw); assert_eq!( cached, recomputed_encoded, "rung {rung:?}: eviction->recompute path diverged from the cached canvas" ); } } /// Determinism across DIFFERENT positions is not the gate's job (that's /// what makes derive believable, not what makes cache/derive agree) — but a /// sanity check that two DIFFERENT centres do NOT produce identical output /// is worth pinning here too, so this suite can't be satisfied by an /// accidentally-constant derive function. #[test] fn different_centers_produce_different_canvases_sanity_check() { let seed = SeedChain::root(0xACCE97_u64).derive(SeedDomain::Body, 5); let climate = ClimateConstants::default(); let hm = fixture_hm(); let ta = fixture_ta(&hm); let rn = fixture_river_network(&hm); let params = fixture_params(); // Global ignores `center` by construction (whole-body canvas), so this // sanity check only applies to fixed rungs. for rung in [ StepCanvasRung::Region, StepCanvasRung::District, StepCanvasRung::Quarter, StepCanvasRung::Block, StepCanvasRung::Chunk, ] { let canvas_a = build_step_canvas( seed, "gate-body", ¶ms, &ta, &rn, &[], rung, (0, 0), GATE_EXTENT, &climate, 0, ); let canvas_b = build_step_canvas( seed, "gate-body", ¶ms, &ta, &rn, &[], rung, (200_000, 100_000), GATE_EXTENT, &climate, 0, ); assert_ne!( canvas_a.elev_q, canvas_b.elev_q, "rung {rung:?}: two far-apart centres produced identical elev_q — \ suspiciously constant derive (this suite should not pass on a stub)" ); } }