415 lines
14 KiB
Rust
415 lines
14 KiB
Rust
//! 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)"
|
|
);
|
|
}
|
|
}
|