test(simulation): Phase-4 hardening — deferred #953/#963 review gaps (T-964)
The verified-still-open coverage list: per-type attractor reachability fixtures (LakeShore via enclosed depression, PassEntrance via crafted saddle, PlainCenter via flat terrain, RiverCrossing via confluence) plus thin_by_spacing behavior (collision, strict-< boundary, equirectangular column wrap); heightmap 8-bit decode, sea_level passthrough, downsample identity and zero-target early-return; drainage area_pct bit-for-bit determinism plus the isolated-basin-fallback divergence comment (Tyre N1, citing the pre-#953 behavior it deliberately departs from); the layer1 mountain-branch pairing test (investigated first — the cascade test supplies a mountain pool but only ever asserted river counts, a genuine gap); an importer idempotency test covering atlas_city_names AND atlas_feature_names plus the Sol exemption, wired into make test-tooling; and the oasis_water dilation radius scaled by GRID_W/512 (Tyre N2, hash-stable). One stale item dropped per the refinement trim (test_sim_determinism wiring — already done). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Idempotency tests for economy_import.atlas's names-only pool populators
|
||||
(T-964 — Phase-4 test hardening).
|
||||
|
||||
`populate_atlas_city_names` and `populate_atlas_feature_names` both clear
|
||||
their target table before reinserting (see each function's own docstring:
|
||||
"there is no UNIQUE(body_id, name[, feature_type]) — without the clear a
|
||||
re-run would accumulate duplicates"). This test proves that contract holds in
|
||||
practice: two back-to-back runs against the same DB must yield an IDENTICAL
|
||||
row count, not a doubled one, and Sol ('GJ 0') must remain permanently
|
||||
exempt (D-223) on both runs.
|
||||
|
||||
Runs against a scratch COPY of the committed `server/data/systems.db` (real
|
||||
schema + real `bodies`/FK data — not a hand-rolled in-memory schema, which
|
||||
would drift from the real FK web this populator depends on). Wiki content
|
||||
(`wiki/star-systems/*/bodies/*/markers.json`) is read directly from the repo
|
||||
— read-only input, safe to reuse as-is; only the DB connection is scratch.
|
||||
Deliberately calls the two populator functions directly rather than the full
|
||||
`import_economics.py` CLI: the CLI's first step shells out to the Rust
|
||||
`generate_brands` binary and overwrites `generated_brands.toml` on disk
|
||||
(`brands.regenerate_brands`), a real side effect on shared repo content that
|
||||
has nothing to do with atlas name-pool idempotency and would make this test
|
||||
depend on a Rust build.
|
||||
|
||||
Stdlib only (unittest) — run directly or via `make test-tooling`:
|
||||
python3 tooling/economy-db/test_atlas_idempotency.py
|
||||
"""
|
||||
|
||||
import shutil
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
from economy_import import atlas # noqa: E402
|
||||
from economy_import.paths import DB_PATH # noqa: E402
|
||||
|
||||
|
||||
class AtlasNamePoolIdempotencyTests(unittest.TestCase):
|
||||
"""Two runs on a scratch DB copy must yield stable, non-doubled counts."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
if not DB_PATH.exists():
|
||||
raise unittest.SkipTest(f"committed systems.db not found at {DB_PATH}")
|
||||
|
||||
def setUp(self):
|
||||
self._tmp = tempfile.TemporaryDirectory()
|
||||
scratch_path = Path(self._tmp.name) / "systems_scratch.db"
|
||||
shutil.copyfile(DB_PATH, scratch_path)
|
||||
self.conn = sqlite3.connect(str(scratch_path))
|
||||
self.conn.execute("PRAGMA foreign_keys=ON")
|
||||
|
||||
def tearDown(self):
|
||||
self.conn.close()
|
||||
self._tmp.cleanup()
|
||||
|
||||
def test_atlas_city_names_count_stable_across_two_runs(self):
|
||||
first = atlas.populate_atlas_city_names(self.conn, dry_run=False)
|
||||
first_count = self.conn.execute(
|
||||
"SELECT COUNT(*) FROM atlas_city_names"
|
||||
).fetchone()[0]
|
||||
self.assertEqual(
|
||||
first, first_count, "populator return value must match the rows it wrote"
|
||||
)
|
||||
|
||||
second = atlas.populate_atlas_city_names(self.conn, dry_run=False)
|
||||
second_count = self.conn.execute(
|
||||
"SELECT COUNT(*) FROM atlas_city_names"
|
||||
).fetchone()[0]
|
||||
|
||||
self.assertEqual(
|
||||
first_count,
|
||||
second_count,
|
||||
"a second run must yield an IDENTICAL atlas_city_names count, not "
|
||||
"a doubled one — the clear-before-reinsert contract must hold",
|
||||
)
|
||||
self.assertEqual(
|
||||
first, second, "the populator's own return value must also be stable"
|
||||
)
|
||||
self.assertGreater(
|
||||
first_count, 0, "fixture sanity: the committed wiki content must yield rows"
|
||||
)
|
||||
|
||||
def test_atlas_feature_names_count_stable_across_two_runs(self):
|
||||
# T-1169: atlas_feature_names landed alongside atlas_city_names but
|
||||
# is a separate table/populator — must be checked independently, not
|
||||
# assumed to share the city-name populator's idempotency by proximity.
|
||||
first = atlas.populate_atlas_feature_names(self.conn, dry_run=False)
|
||||
first_count = self.conn.execute(
|
||||
"SELECT COUNT(*) FROM atlas_feature_names"
|
||||
).fetchone()[0]
|
||||
self.assertEqual(first, first_count)
|
||||
|
||||
second = atlas.populate_atlas_feature_names(self.conn, dry_run=False)
|
||||
second_count = self.conn.execute(
|
||||
"SELECT COUNT(*) FROM atlas_feature_names"
|
||||
).fetchone()[0]
|
||||
|
||||
self.assertEqual(
|
||||
first_count,
|
||||
second_count,
|
||||
"a second run must yield an IDENTICAL atlas_feature_names count, not "
|
||||
"a doubled one — the clear-before-reinsert contract must hold",
|
||||
)
|
||||
self.assertEqual(first, second)
|
||||
self.assertGreater(
|
||||
first_count, 0, "fixture sanity: the committed wiki content must yield rows"
|
||||
)
|
||||
|
||||
def test_sol_system_gj0_gets_zero_rows_in_both_pools_across_two_runs(self):
|
||||
# D-223 permanent exemption: Sol ('GJ 0') keeps authored,
|
||||
# geometry-bearing markers.json via sol_import.py — never the names
|
||||
# pool. Must hold on the FIRST run (not just "never accumulates") and
|
||||
# remain zero on the second.
|
||||
for run in (1, 2):
|
||||
atlas.populate_atlas_city_names(self.conn, dry_run=False)
|
||||
atlas.populate_atlas_feature_names(self.conn, dry_run=False)
|
||||
|
||||
sol_cities = self.conn.execute(
|
||||
"""SELECT COUNT(*) FROM atlas_city_names
|
||||
WHERE body_id IN (SELECT body_id FROM bodies WHERE system_id = 'GJ 0')"""
|
||||
).fetchone()[0]
|
||||
sol_features = self.conn.execute(
|
||||
"""SELECT COUNT(*) FROM atlas_feature_names
|
||||
WHERE body_id IN (SELECT body_id FROM bodies WHERE system_id = 'GJ 0')"""
|
||||
).fetchone()[0]
|
||||
|
||||
self.assertEqual(
|
||||
sol_cities, 0, f"run {run}: Sol (GJ 0) must have zero atlas_city_names rows"
|
||||
)
|
||||
self.assertEqual(
|
||||
sol_features,
|
||||
0,
|
||||
f"run {run}: Sol (GJ 0) must have zero atlas_feature_names rows",
|
||||
)
|
||||
|
||||
# Fixture sanity: GJ 0 must actually have bodies in this DB, or the
|
||||
# zero-rows assertions above would be vacuously true.
|
||||
gj0_body_count = self.conn.execute(
|
||||
"SELECT COUNT(*) FROM bodies WHERE system_id = 'GJ 0'"
|
||||
).fetchone()[0]
|
||||
self.assertGreater(
|
||||
gj0_body_count,
|
||||
0,
|
||||
"fixture sanity: GJ 0 (Sol) must have bodies in the committed DB, or "
|
||||
"the exemption checks above are vacuous",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
@@ -784,8 +784,21 @@ def compute_biome(body_def, elevation, sea_level, surface_water,
|
||||
oasis_water = (biome == 2) & land # scattered lake cells on land
|
||||
if oasis_water.any():
|
||||
from scipy.ndimage import binary_dilation
|
||||
ring1 = binary_dilation(oasis_water, iterations=2) & ~oasis_water & land
|
||||
ring2 = binary_dilation(oasis_water, iterations=4) & ~oasis_water & ~ring1 & land
|
||||
# Pixel-unit iteration counts scale by GRID_W/512 (module
|
||||
# convention, see the GRID_W comment above) so the vegetation
|
||||
# ring covers the same physical distance at any resolution — a
|
||||
# fixed pixel count would shrink the ring (relative to the
|
||||
# planet's real size) as GRID_W grows past the 512 baseline.
|
||||
scale = GRID_W / 512.0
|
||||
ring1_iters = max(1, round(2 * scale))
|
||||
ring2_iters = max(1, round(4 * scale))
|
||||
ring1 = binary_dilation(oasis_water, iterations=ring1_iters) & ~oasis_water & land
|
||||
ring2 = (
|
||||
binary_dilation(oasis_water, iterations=ring2_iters)
|
||||
& ~oasis_water
|
||||
& ~ring1
|
||||
& land
|
||||
)
|
||||
# Inner ring: lush vegetation (coast/lowland green)
|
||||
biome[ring1] = 4 # lowland
|
||||
# Outer ring: transitional (savanna/shrub)
|
||||
|
||||
Reference in New Issue
Block a user