diff --git a/server/src/atlas/cascade.rs b/server/src/atlas/cascade.rs index 2b2d16360..b87d3e3b7 100644 --- a/server/src/atlas/cascade.rs +++ b/server/src/atlas/cascade.rs @@ -506,6 +506,9 @@ mod tests { assert_eq!(p1.river_threshold, p2.river_threshold); assert_eq!(p1.tectonic_class, p2.tectonic_class); assert_eq!(p1.glaciation_grade, p2.glaciation_grade); + // basin_direction threads run_layer1 -> district_basin_dirs -> here; + // guard the full chain's determinism (T-1047). + assert_eq!(p1.basin_direction, p2.basin_direction); } } diff --git a/server/src/atlas/district_profile.rs b/server/src/atlas/district_profile.rs index 7e76ddfdd..9affc8b53 100644 --- a/server/src/atlas/district_profile.rs +++ b/server/src/atlas/district_profile.rs @@ -1288,8 +1288,12 @@ pub fn derive_district( ); // derive_district is the on-demand path (arbitrary DistrictPos, no L1 working - // grid); basin_direction defaults to North here. The batch path - // (derive_all_districts) threads the true D8 thalweg direction from L1. + // grid). basin_direction is an ACCEPTED LIMITATION here: it defaults to North + // (a fallback, not a computed value) because the D8 thalweg is only available + // from the L1 fdir grid the batch path holds. Production voxel generation runs + // through the batch path (derive_all_districts), which threads the true D8 + // direction from L1; this on-demand path is the fallback for districts derived + // outside that pass, where a meaningful basin_direction isn't available. build_district_profile( seed, ¶ms, @@ -1505,6 +1509,40 @@ mod tests { assert_eq!(districts.len(), 32, "district count mismatch"); } + /// T-1047: the `Some(basin_dirs)` threading path — supplied per-district D8 + /// directions propagate to `DistrictProfile.basin_direction`, and districts + /// not in the map fall back to the default (North). Guards the path that the + /// production cascade actually uses (the existing tests only exercise `None`). + #[test] + fn derive_all_districts_threads_supplied_basin_directions() { + use crate::atlas::scale::BasinDirection; + let hm = test_hm(); + let ta = test_ta(&hm); + let params = BodyParams::default(); + + // Real DistrictPos keys from a baseline (None) run. + let baseline = derive_all_districts(test_seed(), ¶ms, &ta, 8, "test_body", None); + let mut keys = baseline.keys().copied(); + let pos_east = keys.next().expect("at least one district"); + let pos_south = keys.next().expect("at least two districts"); + let pos_unmapped = keys.next().expect("at least three districts"); + + let mut basin_dirs: BTreeMap = BTreeMap::new(); + basin_dirs.insert(pos_east, BasinDirection::East); + basin_dirs.insert(pos_south, BasinDirection::South); + + let districts = + derive_all_districts(test_seed(), ¶ms, &ta, 8, "test_body", Some(&basin_dirs)); + + assert_eq!(districts[&pos_east].basin_direction, BasinDirection::East); + assert_eq!(districts[&pos_south].basin_direction, BasinDirection::South); + // Unmapped districts fall back to the default direction (North). + assert_eq!( + districts[&pos_unmapped].basin_direction, + BasinDirection::North + ); + } + // --- derive_district (on-demand 2 km, interpolation + detail-scatter) ----- fn earth_params() -> BodyParams {