//! Golden-seed regression for the Layer 0→1 generation cascade (#952, D-200, D-224). //! //! Pins two artifacts for one fixed input heightmap, in a single diffable JSON //! golden (matching the `golden_suite.rs` convention): //! - **Layer 0** — SHA-256 of the source `heightmap.png` bytes. Flips if the //! Python heightmap generator (or the committed file) changes. //! - **Layer 1** — the serialized `Layer1Output` of the cascade run on a //! downsampled copy. Flips if the Rust drainage / feature / sub-biome code //! changes. //! //! The input is a real committed body heightmap (the #963 bake output), so this //! is an end-to-end determinism guard. The cascade is downsampled to keep the //! golden small while still exercising the full Layer-1 pipeline. //! //! Regenerate after an intended change: //! UPDATE_GOLDEN=1 cargo test --test cascade_golden //! //! Golden captured on x86_64. The downsample and sub-biome cost use f32, so a //! different architecture could in principle round differently — regenerate //! per-arch if CI ever moves off x86_64. use std::path::PathBuf; use serde_json::{json, Value}; use settled_reach_server::atlas::cascade::{run_cascade_from_heightmap, CascadeLayer}; use settled_reach_server::atlas::heightmap::load_heightmap_png; use settled_reach_server::seed::SeedChain; use sha2::{Digest, Sha256}; /// Source heightmap — a real committed body, relative to the server manifest dir. const SOURCE_HEIGHTMAP: &str = "../wiki/star-systems/GJ-1/bodies/GJ1c/heightmap.png"; /// Downsample target: large enough that GJ1c's drainage produces a real river /// network (not just attractors), small enough for a compact golden. const DOWNSAMPLE: (u32, u32) = (256, 128); /// World seed for the run. Cosmetic here — Layers 0–1 are RNG-free, so changing /// it does not change the golden; it is carried only to exercise the SeedChain /// contract end-to-end (D-224). Seed-sensitivity gets pinned once Layer 3+ lands. const WORLD_SEED: u64 = 42; const GOLDEN: &str = "tests/golden/cascade_layer1.json"; #[test] fn cascade_layer0_to_1_matches_golden() { let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let src = manifest.join(SOURCE_HEIGHTMAP); // ── Layer 0 — load + hash the source heightmap.png. ───────────────────── let png_bytes = std::fs::read(&src) .unwrap_or_else(|e| panic!("read source heightmap {}: {e}", src.display())); let mut hasher = Sha256::new(); hasher.update(&png_bytes); let sha = format!("{:x}", hasher.finalize()); let heightmap = load_heightmap_png(&src, "GJ1c", 0.3).expect("decode source heightmap"); // ── Layer 1 — downsample, then run the cascade to topography. ─────────── let small = heightmap.downsample(DOWNSAMPLE.0, DOWNSAMPLE.1); let body_seed = SeedChain::for_body(WORLD_SEED, "GJ1c"); let snapshot = run_cascade_from_heightmap(body_seed, small, &[], None, None, CascadeLayer::Topography); let layer1 = snapshot.layer1.expect("Layer 1 ran"); let actual = json!({ "source_heightmap": SOURCE_HEIGHTMAP, "source_sha256": sha, "downsample": [DOWNSAMPLE.0, DOWNSAMPLE.1], "layer1": serde_json::to_value(&layer1).expect("serialize Layer1Output"), }); let actual_json = serde_json::to_string_pretty(&actual).expect("format JSON") + "\n"; let golden_path = manifest.join(GOLDEN); if std::env::var("UPDATE_GOLDEN").is_ok() { std::fs::create_dir_all(golden_path.parent().unwrap()).expect("mkdir golden"); std::fs::write(&golden_path, &actual_json).expect("write golden"); eprintln!( "Golden written: {} ({} bytes)", golden_path.display(), actual_json.len() ); return; } let golden_json = std::fs::read_to_string(&golden_path).unwrap_or_else(|e| { panic!( "Golden not found: {}. Run: UPDATE_GOLDEN=1 cargo test --test cascade_golden\n{e}", golden_path.display() ) }); // Compare as parsed JSON so whitespace/formatting never causes spurious diffs. let actual_v: Value = serde_json::from_str(&actual_json).expect("reparse actual JSON"); let golden_v: Value = serde_json::from_str(&golden_json).expect("parse golden JSON"); if actual_v != golden_v { let count = |v: &Value, ptr: &str| v.pointer(ptr).and_then(Value::as_array).map_or(0, Vec::len); panic!( "Cascade golden mismatch.\n \ source_sha256: golden={} actual={}\n \ river_cells: golden={} actual={}\n \ attractors: golden={} actual={}\n\ To update: UPDATE_GOLDEN=1 cargo test --test cascade_golden\n Golden: {}", golden_v["source_sha256"], actual_v["source_sha256"], count(&golden_v, "/layer1/river_network/river_cells"), count(&actual_v, "/layer1/river_network/river_cells"), count(&golden_v, "/layer1/attractors"), count(&actual_v, "/layer1/attractors"), golden_path.display(), ); } }