fix(simulation): normalize window_center before cache and coalescing keys (T-1142 server half)
The server clamped window_n but trusted window_center — the letterbox bug's impossible (12276, 3021) was derived at clamped-garbage latitude and CACHED under its raw key. normalize_window_center mirrors the forward map exactly (column rem_euclid against the body's district circumference, row clamped to +/-half-meridian; constants from scale::DISTRICT_M, no new magic numbers; no-radius test bodies identity). Applied in serve_district_window BEFORE the window-cache key and the coalescing key, so an insane request and its sane twin share one entry — proven end-to-end with the literal bug values on a 500km fixture body resolving to canonical (4, 383), byte-identical. Echo contract: DistrictWindowLayer.center now carries the NORMALIZED value. Reconciled by design with the client half in this same branch — the client canonicalizes before sending (bit-identical math, posmod == rem_euclid verified), so request and echo agree from the first round-trip; server normalization is defense-in-depth for buggy or modified clients. 6 new tests incl. the twin-cache proof and the echo pin; atlas:: 570/570, lib 1784/1784, timing-sensitive tests 3x stable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+419
-34
@@ -18,10 +18,11 @@ use crate::atlas::body_params_reader::BodyParamsReader;
|
||||
use crate::atlas::body_world_state::{BodyWorldState, BodyWorldStateCache, SimTick};
|
||||
use crate::atlas::cascade::CascadeLayer;
|
||||
use crate::atlas::city_context_reader::CityContextReader;
|
||||
use crate::atlas::district_profile::DistrictPos;
|
||||
use crate::atlas::district_profile::{BodyParams, DistrictPos};
|
||||
use crate::atlas::gen_queue::{GenPriority, GenWorkItem, GenerationQueue};
|
||||
use crate::atlas::layer1::Layer1Output;
|
||||
use crate::atlas::road_graph::RoadNodeKind;
|
||||
use crate::atlas::scale::DISTRICT_M;
|
||||
use crate::atlas::source_resolver::{BodySourceResolver, SourceResolveError};
|
||||
use crate::bridge::ConnectionId;
|
||||
use crate::seed::{SeedChain, SeedDomain};
|
||||
@@ -745,6 +746,64 @@ pub fn build_settlement_layer(state: &BodyWorldState) -> Option<SettlementLayer>
|
||||
Some(SettlementLayer { settlements })
|
||||
}
|
||||
|
||||
/// Normalize a wire-supplied district-window centre against the body's
|
||||
/// physical geometry (T-1142 — a letterbox-click bug sent `window_center`
|
||||
/// wildly out of a body's valid range; the server accepted it, derived
|
||||
/// clamped garbage inside `derive_district`, and CACHED that garbage under
|
||||
/// the raw un-normalized key). Mirrors `district_profile::derive_district`'s
|
||||
/// own forward mapping (`district_profile.rs:1426-1451`) EXACTLY, so a window
|
||||
/// centre that survives this normalization derives identically to how
|
||||
/// `derive_district` would have resolved it anyway — this function only
|
||||
/// closes the gap between "derive_district silently clamps/wraps internally"
|
||||
/// and "the SERVING path (cache key, coalescing key) saw the raw value".
|
||||
///
|
||||
/// **Column (longitude) wraps** — `rem_euclid` against the body's
|
||||
/// circumference in districts, mirroring the forward map's
|
||||
/// `(wx / circumference_m).rem_euclid(1.0)` (longitude is periodic; a click
|
||||
/// at column 12276 on a body whose circumference is a few hundred districts
|
||||
/// wide is the SAME point as some small in-range column, not garbage).
|
||||
///
|
||||
/// **Row (latitude) clamps** — to `±half_meridian_districts`, mirroring the
|
||||
/// forward map's `(wy / meridian_m).clamp(-0.5, 0.5)` (latitude is NOT
|
||||
/// periodic; it terminates at the poles, so out-of-range rows collapse to the
|
||||
/// nearest pole rather than wrapping — same asymmetry `derive_district`
|
||||
/// itself already encodes).
|
||||
///
|
||||
/// Both bounds are derived from `body_radius_km` via the SAME `DISTRICT_M`
|
||||
/// (2 048 m, D-243) constant the forward map uses — no independent magic
|
||||
/// numbers that could silently drift out of sync with `derive_district`.
|
||||
///
|
||||
/// **No radius (`body_radius_km` absent/non-positive — tiny test bodies
|
||||
/// only, per `BodyParams`'s own doc: "e.g. tiny test bodies"; every real
|
||||
/// `systems.db` body row carries a radius):** identity, no wrap/clamp. The
|
||||
/// forward map's own no-radius branch has no periodicity concept either (it
|
||||
/// clamps the FRACTIONAL PIXEL position directly against the heightmap's
|
||||
/// working-grid dimensions, which aren't known at request-serving time — only
|
||||
/// inside the Rayon work item once the heightmap is loaded); a real letterbox
|
||||
/// click can never hit this branch, so it is out of this fix's scope.
|
||||
fn normalize_window_center(params: &BodyParams, center: DistrictPos) -> DistrictPos {
|
||||
let (dx, dy) = center;
|
||||
match params.body_radius_km {
|
||||
Some(r_km) if r_km > 0.0 => {
|
||||
let circumference_m = std::f64::consts::TAU * r_km * 1000.0;
|
||||
let meridian_m = std::f64::consts::PI * r_km * 1000.0;
|
||||
// Whole districts per full circumference / per half-meridian —
|
||||
// rounded (not truncated) so the bound matches the forward map's
|
||||
// continuous fraction as closely as an integer district grid can.
|
||||
let districts_per_circumference =
|
||||
(circumference_m / DISTRICT_M as f64).round().max(1.0) as i32;
|
||||
let half_meridian_districts = (meridian_m / DISTRICT_M as f64 / 2.0).round() as i32;
|
||||
|
||||
let wrapped_dx = dx.rem_euclid(districts_per_circumference);
|
||||
let clamped_dy = dy.clamp(-half_meridian_districts, half_meridian_districts);
|
||||
(wrapped_dx, clamped_dy)
|
||||
}
|
||||
// No radius: derive_district's own fallback has no wrap/clamp concept
|
||||
// at the DistrictPos level (see the doc above) — identity.
|
||||
_ => center,
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve `req`'s district-window query, if any (D-226 T-1124 amendment,
|
||||
/// T-1137). Returns `None` immediately when `req.window_center` is absent (no
|
||||
/// window requested — the common case, zero cost).
|
||||
@@ -756,15 +815,28 @@ pub fn build_settlement_layer(state: &BodyWorldState) -> Option<SettlementLayer>
|
||||
/// about to take its cache-hit or cache-miss branch, sharing neither's control
|
||||
/// flow.
|
||||
///
|
||||
/// Cache hit (`(body_id, center, n)` already in `window_cache`) → `Some`
|
||||
/// immediately, no queue submission (D-227: a previously-derived window for
|
||||
/// this body+seed is valid forever, no staleness check needed). Cache miss →
|
||||
/// submit a `DeriveWindow` work item (queue-based, per the amendment's binding
|
||||
/// serving model — never inline here) and return `None`; the *next* request
|
||||
/// for this `(body, center, n)` re-checks the cache and finds it populated
|
||||
/// once `drain_generation_completions` has processed the completion (the
|
||||
/// existing D-225 poll-and-recheck-cache pattern every other layer already
|
||||
/// uses, not a push).
|
||||
/// **`window_center` is normalized via [`normalize_window_center`] BEFORE the
|
||||
/// `DistrictWindowCache` key AND the `submit_window` coalescing key are built**
|
||||
/// (T-1142) — this is why `body_params` is read HERE, unconditionally,
|
||||
/// rather than only inside the former miss-branch: normalization needs
|
||||
/// `body_radius_km` to compute the wrap/clamp bounds, and it must happen
|
||||
/// before either key exists, or an insane request and its sane normalized
|
||||
/// twin would land in different cache entries / coalesce independently
|
||||
/// (exactly the bug this fix closes — a garbage `window_center` was cached
|
||||
/// standalone instead of collapsing onto its valid twin). The one-time cost
|
||||
/// (a single indexed `bodies` row read) is paid on every window request now,
|
||||
/// not just on a cache miss — a request whose normalized center hits the
|
||||
/// cache still needed this read to know WHICH key to check.
|
||||
///
|
||||
/// Cache hit (`(body_id, normalized_center, n)` already in `window_cache`) →
|
||||
/// `Some` immediately, no queue submission (D-227: a previously-derived
|
||||
/// window for this body+seed is valid forever, no staleness check needed).
|
||||
/// Cache miss → submit a `DeriveWindow` work item (queue-based, per the
|
||||
/// amendment's binding serving model — never inline here) and return `None`;
|
||||
/// the *next* request for this `(body, normalized_center, n)` re-checks the
|
||||
/// cache and finds it populated once `drain_generation_completions` has
|
||||
/// processed the completion (the existing D-225 poll-and-recheck-cache
|
||||
/// pattern every other layer already uses, not a push).
|
||||
///
|
||||
/// `window_n` is clamped to `[1, DISTRICT_WINDOW_MAX_N]` here — the ONE place
|
||||
/// that clamp is applied; nothing downstream re-checks the wire value.
|
||||
@@ -778,29 +850,15 @@ fn serve_district_window(
|
||||
world_seed: u64,
|
||||
conn_id: ConnectionId,
|
||||
) -> Option<DistrictWindowLayer> {
|
||||
let center = req.window_center?;
|
||||
let raw_center = req.window_center?;
|
||||
let n = req.window_n.clamp(1, DISTRICT_WINDOW_MAX_N);
|
||||
|
||||
let key: DistrictWindowKey = (req.body_id.clone(), center, n);
|
||||
if let Some(layer) = window_cache.get(&key) {
|
||||
return Some(layer.clone());
|
||||
}
|
||||
|
||||
// Miss — resolve heightmap + body params and submit a background derive.
|
||||
// Read/resolve failures are non-fatal for the window (log + skip): the
|
||||
// window simply stays None on this response, same as an unrun whole-body
|
||||
// layer, rather than failing the entire AtlasLayerResponse.
|
||||
let heightmap_path = match resolver.resolve(&req.body_id) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
body_id = %req.body_id,
|
||||
error = %e,
|
||||
"district window request: heightmap resolve failed — window stays None"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
// body_params is needed to normalize the centre BEFORE either cache key
|
||||
// exists (T-1142) — read it first, unconditionally (not gated on a cache
|
||||
// miss like the former structure). A read failure here can't distinguish
|
||||
// "insane vs. sane center" for the key, so it's a hard skip for the whole
|
||||
// window (window stays None on this response), same failure posture the
|
||||
// former miss-only read already had.
|
||||
let Some(reader) = body_params_reader else {
|
||||
tracing::warn!(
|
||||
body_id = %req.body_id,
|
||||
@@ -820,6 +878,37 @@ fn serve_district_window(
|
||||
}
|
||||
};
|
||||
|
||||
let center = normalize_window_center(&body_params, raw_center);
|
||||
if center != raw_center {
|
||||
tracing::debug!(
|
||||
body_id = %req.body_id,
|
||||
raw = ?raw_center,
|
||||
normalized = ?center,
|
||||
"district window request: out-of-range window_center normalized (T-1142)"
|
||||
);
|
||||
}
|
||||
|
||||
let key: DistrictWindowKey = (req.body_id.clone(), center, n);
|
||||
if let Some(layer) = window_cache.get(&key) {
|
||||
return Some(layer.clone());
|
||||
}
|
||||
|
||||
// Miss — resolve the heightmap and submit a background derive.
|
||||
// Read/resolve failures are non-fatal for the window (log + skip): the
|
||||
// window simply stays None on this response, same as an unrun whole-body
|
||||
// layer, rather than failing the entire AtlasLayerResponse.
|
||||
let heightmap_path = match resolver.resolve(&req.body_id) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
body_id = %req.body_id,
|
||||
error = %e,
|
||||
"district window request: heightmap resolve failed — window stays None"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
queue.submit_window(
|
||||
GenWorkItem::DeriveWindow {
|
||||
body_id: req.body_id.clone(),
|
||||
@@ -1434,6 +1523,284 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// normalize_window_center (T-1142 — letterbox-click out-of-range bug)
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
/// A small-body fixture (500 km radius) whose bounds are hand-checkable:
|
||||
/// `districts_per_circumference = round(TAU*500_000/2048) = 1534`,
|
||||
/// `half_meridian_districts = round(PI*500_000/2048/2) = 383`. The
|
||||
/// literal column/row this fixture uses (`12276`, `3021`) are the exact
|
||||
/// values the reported T-1142 letterbox-click bug sent — at THIS radius
|
||||
/// they genuinely overflow both bounds (at Earth radius, coincidentally,
|
||||
/// they wouldn't — the bounds are tens of thousands of districts wide),
|
||||
/// so this is a faithful small-body reproduction, not just an
|
||||
/// arbitrarily-chosen out-of-range pair.
|
||||
fn small_body_params() -> BodyParams {
|
||||
BodyParams {
|
||||
hydrosphere: Some("ocean".into()),
|
||||
atmosphere: Some("breathable".into()),
|
||||
planet_class: Some("temperate".into()),
|
||||
body_radius_km: Some(500.0),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Out-of-range COLUMN wraps (longitude is periodic) to its in-range
|
||||
/// canonical twin via `rem_euclid` — mirroring `derive_district`'s own
|
||||
/// `(wx / circumference_m).rem_euclid(1.0)` forward map.
|
||||
#[test]
|
||||
fn normalize_window_center_wraps_out_of_range_column() {
|
||||
let params = small_body_params();
|
||||
// districts_per_circumference = 1534 (hand-computed above).
|
||||
// 12276 rem_euclid 1534 = 4 (12276 = 8*1534 + 4).
|
||||
assert_eq!(
|
||||
12276_i32.rem_euclid(1534),
|
||||
4,
|
||||
"sanity: the hand-computed wrap"
|
||||
);
|
||||
let (nx, ny) = normalize_window_center(¶ms, (12276, 0));
|
||||
assert_eq!(
|
||||
nx, 4,
|
||||
"out-of-range column wraps to its canonical in-range twin"
|
||||
);
|
||||
assert_eq!(ny, 0, "an in-range row is untouched");
|
||||
|
||||
// The canonical twin normalizes to itself (idempotent).
|
||||
let (nx2, _) = normalize_window_center(¶ms, (4, 0));
|
||||
assert_eq!(nx2, 4);
|
||||
|
||||
// Negative columns wrap too (rem_euclid, not truncating rem) —
|
||||
// longitude has no sign discontinuity.
|
||||
let (nx3, _) = normalize_window_center(¶ms, (-1, 0));
|
||||
assert_eq!(
|
||||
nx3, 1533,
|
||||
"negative column wraps to the top of the range, not a negative remainder"
|
||||
);
|
||||
}
|
||||
|
||||
/// Beyond-pole ROW clamps (latitude terminates, does not wrap) to
|
||||
/// `±half_meridian_districts` — mirroring `derive_district`'s own
|
||||
/// `(wy / meridian_m).clamp(-0.5, 0.5)` forward map. This is the
|
||||
/// asymmetry the coordinator's fix explicitly calls out: columns wrap,
|
||||
/// rows clamp — never the other way around.
|
||||
#[test]
|
||||
fn normalize_window_center_clamps_beyond_pole_row() {
|
||||
let params = small_body_params();
|
||||
// half_meridian_districts = 383 (hand-computed above).
|
||||
let (_, ny) = normalize_window_center(¶ms, (0, 3021));
|
||||
assert_eq!(
|
||||
ny, 383,
|
||||
"beyond-pole row clamps to the pole boundary, not wraps"
|
||||
);
|
||||
|
||||
let (_, ny_neg) = normalize_window_center(¶ms, (0, -9000));
|
||||
assert_eq!(ny_neg, -383, "clamping is symmetric at both poles");
|
||||
|
||||
// A row exactly at the boundary is untouched.
|
||||
let (_, ny_boundary) = normalize_window_center(¶ms, (0, 383));
|
||||
assert_eq!(ny_boundary, 383);
|
||||
}
|
||||
|
||||
/// An in-range center (well inside both bounds) is returned UNCHANGED —
|
||||
/// normalization must be a no-op for the overwhelming common case (every
|
||||
/// legitimate click), not just a defensive clamp that happens to also
|
||||
/// preserve valid input.
|
||||
#[test]
|
||||
fn normalize_window_center_leaves_in_range_center_unchanged() {
|
||||
let params = small_body_params();
|
||||
let center = (100, -50);
|
||||
assert_eq!(normalize_window_center(¶ms, center), center);
|
||||
|
||||
// (0, 0) — the origin — is always in range regardless of body size.
|
||||
assert_eq!(normalize_window_center(¶ms, (0, 0)), (0, 0));
|
||||
}
|
||||
|
||||
/// No-radius bodies (tiny test-body fallback, `body_radius_km: None`) get
|
||||
/// IDENTITY — `derive_district`'s own no-radius branch has no
|
||||
/// wrap/clamp-in-district-space concept (see the function doc); an
|
||||
/// extreme center here is out of this fix's scope by design, not an
|
||||
/// oversight.
|
||||
#[test]
|
||||
fn normalize_window_center_no_radius_is_identity() {
|
||||
let params = BodyParams::default(); // body_radius_km: None
|
||||
let extreme = (999_999, -999_999);
|
||||
assert_eq!(normalize_window_center(¶ms, extreme), extreme);
|
||||
}
|
||||
|
||||
/// End-to-end (the coordinator's core ask): an out-of-range
|
||||
/// `window_center` and its already-normalized twin, requested through the
|
||||
/// REAL `handle_atlas_request` path, land in the SAME `DistrictWindowCache`
|
||||
/// entry and produce a byte-identical `DistrictWindowLayer` — normalization
|
||||
/// happens BEFORE the cache key is built, so an insane request and its
|
||||
/// sane twin never diverge into separate cache entries (the bug this fix
|
||||
/// closes: the insane request was cached STANDALONE).
|
||||
#[test]
|
||||
fn insane_and_sane_twin_requests_share_one_cache_entry() {
|
||||
let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY);
|
||||
let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY);
|
||||
let (_db, resolver, params_reader, _root) =
|
||||
resolver_and_params_reader_with_radius("SmallMoon", 500.0);
|
||||
let queue = GenerationQueue::with_threads(1);
|
||||
|
||||
// The insane request — the reported T-1142 letterbox-click values.
|
||||
let insane_req = AtlasLayerRequest {
|
||||
body_id: "SmallMoon".to_string(),
|
||||
up_to: CascadeLayer::Topography,
|
||||
window_center: Some((12276, 3021)),
|
||||
window_n: 4,
|
||||
};
|
||||
let resp1 = handle_atlas_request(
|
||||
&insane_req,
|
||||
&mut cache,
|
||||
&mut window_cache,
|
||||
&queue,
|
||||
&resolver,
|
||||
None,
|
||||
Some(¶ms_reader),
|
||||
42,
|
||||
1,
|
||||
test_conn_id(),
|
||||
);
|
||||
assert!(
|
||||
resp1.district_window.is_none(),
|
||||
"first request — cache miss, DeriveWindow submitted"
|
||||
);
|
||||
|
||||
// Wait for the background derive to complete and drain it into the cache
|
||||
// (mirrors handle_atlas_request_clamps_oversized_window_n's pattern).
|
||||
std::thread::sleep(Duration::from_millis(300));
|
||||
let completions = queue.drain_completions();
|
||||
for c in completions {
|
||||
if let GenCompletion::WindowDerived { body_id, layer } = c {
|
||||
if body_id == "SmallMoon" {
|
||||
window_cache.insert((body_id, layer.center, layer.n), *layer);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Exactly ONE entry must exist in the window cache after the insane
|
||||
// request's derive completes — normalization means it was keyed on
|
||||
// the canonical (4, 383), not the raw (12276, 3021).
|
||||
assert_eq!(
|
||||
window_cache.len(),
|
||||
1,
|
||||
"the insane request's derive must be cached under its NORMALIZED key"
|
||||
);
|
||||
|
||||
// The "sane twin" — the already-normalized canonical center — hits
|
||||
// the SAME cache entry the insane request just populated.
|
||||
let sane_twin_req = AtlasLayerRequest {
|
||||
body_id: "SmallMoon".to_string(),
|
||||
up_to: CascadeLayer::Topography,
|
||||
window_center: Some((4, 383)), // the hand-computed canonical twin
|
||||
window_n: 4,
|
||||
};
|
||||
let resp2 = handle_atlas_request(
|
||||
&sane_twin_req,
|
||||
&mut cache,
|
||||
&mut window_cache,
|
||||
&queue,
|
||||
&resolver,
|
||||
None,
|
||||
Some(¶ms_reader),
|
||||
42,
|
||||
2,
|
||||
test_conn_id(),
|
||||
);
|
||||
let twin_layer = resp2.district_window.expect(
|
||||
"the sane twin must hit the cache the insane request populated — no new derive needed",
|
||||
);
|
||||
assert_eq!(
|
||||
window_cache.len(),
|
||||
1,
|
||||
"the sane twin must NOT create a second cache entry"
|
||||
);
|
||||
|
||||
// Re-request the ORIGINAL insane center too — it must ALSO now hit
|
||||
// the same populated cache entry (both requests normalize to the
|
||||
// same key).
|
||||
let resp3 = handle_atlas_request(
|
||||
&insane_req,
|
||||
&mut cache,
|
||||
&mut window_cache,
|
||||
&queue,
|
||||
&resolver,
|
||||
None,
|
||||
Some(¶ms_reader),
|
||||
42,
|
||||
3,
|
||||
test_conn_id(),
|
||||
);
|
||||
let insane_layer_second_try = resp3
|
||||
.district_window
|
||||
.expect("the insane request, re-requested, must ALSO hit the shared cache entry");
|
||||
|
||||
assert_eq!(
|
||||
twin_layer, insane_layer_second_try,
|
||||
"the insane request and its sane twin must resolve to a BYTE-IDENTICAL layer"
|
||||
);
|
||||
assert_eq!(
|
||||
window_cache.len(),
|
||||
1,
|
||||
"still exactly one entry — neither re-request created a second one"
|
||||
);
|
||||
}
|
||||
|
||||
/// The echoed `center` on `DistrictWindowLayer` is the NORMALIZED value,
|
||||
/// not the raw wire value — the client's D-227 staleness guard (D-226
|
||||
/// T-1124 amendment §2) must see what was ACTUALLY derived, so it can
|
||||
/// correctly match this response against its own (now also normalized,
|
||||
/// per the T-1142 fix note to the client team) cache key.
|
||||
#[test]
|
||||
fn echoed_center_is_the_normalized_value_not_the_raw_wire_value() {
|
||||
let mut cache = BodyWorldStateCache::new(CACHE_CAPACITY);
|
||||
let mut window_cache = DistrictWindowCache::new(DISTRICT_WINDOW_CACHE_CAPACITY);
|
||||
let (_db, resolver, params_reader, _root) =
|
||||
resolver_and_params_reader_with_radius("SmallMoon2", 500.0);
|
||||
let queue = GenerationQueue::with_threads(1);
|
||||
|
||||
let insane_req = AtlasLayerRequest {
|
||||
body_id: "SmallMoon2".to_string(),
|
||||
up_to: CascadeLayer::Topography,
|
||||
window_center: Some((12276, 3021)), // raw, out-of-range
|
||||
window_n: 4,
|
||||
};
|
||||
handle_atlas_request(
|
||||
&insane_req,
|
||||
&mut cache,
|
||||
&mut window_cache,
|
||||
&queue,
|
||||
&resolver,
|
||||
None,
|
||||
Some(¶ms_reader),
|
||||
42,
|
||||
1,
|
||||
test_conn_id(),
|
||||
);
|
||||
|
||||
std::thread::sleep(Duration::from_millis(300));
|
||||
let completions = queue.drain_completions();
|
||||
let window_completion = completions.into_iter().find_map(|c| {
|
||||
if let GenCompletion::WindowDerived { body_id, layer } = c {
|
||||
if body_id == "SmallMoon2" {
|
||||
return Some(layer);
|
||||
}
|
||||
}
|
||||
None
|
||||
});
|
||||
let layer = window_completion.expect("DeriveWindow must complete");
|
||||
assert_eq!(
|
||||
layer.center,
|
||||
(4, 383),
|
||||
"the completed/echoed layer.center is the NORMALIZED value, not the raw (12276, 3021)"
|
||||
);
|
||||
assert_ne!(
|
||||
layer.center,
|
||||
(12276, 3021),
|
||||
"the raw out-of-range wire value must never be echoed back"
|
||||
);
|
||||
}
|
||||
|
||||
/// `serve_district_window` returns `None` (no window requested) when the
|
||||
/// request carries no `window_center` — the common case, and the ONLY
|
||||
/// path every pre-T-1137 caller takes (wire back-compat: an old client's
|
||||
@@ -2302,7 +2669,8 @@ mod tests {
|
||||
}
|
||||
|
||||
/// Build a DB with the columns needed by both `BodySourceResolver` and
|
||||
/// `BodyParamsReader` for the same body, plus a tiny heightmap root.
|
||||
/// `BodyParamsReader` for the same body (Earth radius, 6371 km), plus a
|
||||
/// tiny heightmap root.
|
||||
///
|
||||
/// Returns (db_path, resolver, body_params_reader, _root_kept_alive).
|
||||
fn resolver_and_params_reader(
|
||||
@@ -2312,6 +2680,23 @@ mod tests {
|
||||
BodySourceResolver,
|
||||
crate::atlas::body_params_reader::BodyParamsReader,
|
||||
PathBuf, // root dir — must stay alive for the test duration
|
||||
) {
|
||||
resolver_and_params_reader_with_radius(body_id, 6371.0)
|
||||
}
|
||||
|
||||
/// Same as [`resolver_and_params_reader`] with a caller-chosen
|
||||
/// `body_radius_km` (T-1142: the window-centre normalization tests need a
|
||||
/// SMALL body — at Earth radius the district-circumference/half-meridian
|
||||
/// bounds are tens of thousands of districts wide, too large for a
|
||||
/// hand-checkable out-of-range test value).
|
||||
fn resolver_and_params_reader_with_radius(
|
||||
body_id: &str,
|
||||
r_km: f64,
|
||||
) -> (
|
||||
PathBuf,
|
||||
BodySourceResolver,
|
||||
crate::atlas::body_params_reader::BodyParamsReader,
|
||||
PathBuf, // root dir — must stay alive for the test duration
|
||||
) {
|
||||
let n = SEQ.fetch_add(1, Ordering::Relaxed);
|
||||
let db = std::env::temp_dir().join(format!("sr_proxybp_{}_{n}.db", std::process::id()));
|
||||
@@ -2345,8 +2730,8 @@ mod tests {
|
||||
.unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO bodies (body_id, system_id, terrain_reference, hydrosphere, atmosphere, planet_class, body_radius_km, orbital_period_days, axial_tilt_deg)
|
||||
VALUES (?1, 'GJ-1', ?2, 'ocean', 'breathable', 'temperate', 6371.0, 365.25, 23.5)",
|
||||
rusqlite::params![body_id, REL],
|
||||
VALUES (?1, 'GJ-1', ?2, 'ocean', 'breathable', 'temperate', ?3, 365.25, 23.5)",
|
||||
rusqlite::params![body_id, REL, r_km],
|
||||
)
|
||||
.unwrap();
|
||||
drop(conn);
|
||||
|
||||
Reference in New Issue
Block a user