Files
settled-reach/tooling/test_ledger_atlas_idempotency.py
T
jpmschweitzerandClaude Opus 5.5 23538d640f refactor(tooling): T-1289 — economy-db becomes reach ledger
The sole generator of systems.db moves to tooling/domains/ledger/ and is
now `reach ledger import`. economy_import/ keeps its name (Rust comments in
server/src cite it); the entrypoint becomes service.py; schema_version.py
moves with the importer, which is where the version is defined.

The stamp survived the move, which is the thing that had to hold:

- generated_brands.toml is byte-identical (sha256 e748531…) before and after
- `reach check systems-db-stamp` reported STALE after the move (the registry
  saw it) and OK after the regen
- the dry-run carries every count and warning of the baseline transcript,
  and exit 2 — imported and stamped, coverage gate unmet — still reaches the
  caller through @command

`make regen-db` survives as a one-line delegate, per D-263's muscle-memory
clause: about fifty files name it, including the headers of generated wiki
TOMLs and the remedies the push gate prints. `make economy-db` is retired;
it ran `reach generate brands` before the import, which the import already
does as its first step.

economy_import.errors is reconciled as DOMAINS.md asked. ImportAborted stays
as internal rollback control flow and never reaches a caller; the service
converts it to a ReachError carrying the remedy.

regenerate_brands caught cargo_binary's ReachError, printed it and raised
ImportAborted, dropping the remedy. It runs before the import transaction
opens, so there is nothing to roll back — it now propagates.

Both sys.path bootstraps are gone; they existed only because the directory
was hyphenated. The step labels ran [1/10]…[10/13]…[17/19]; one 24-step
counter now drives the event phase and progress.

Stale pointers fixed on the way: MIGRATION_SQL has lived in
economy_import/migration.py since T-1067, but the asset-pipeline rule,
DEVOPS and the schema comments still sent readers to import_economics.py;
the rule and DEVOPS also still named the check scripts T-1281 retired.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 16:18:59 +02:00

157 lines
6.4 KiB
Python

#!/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
`reach ledger import`: its 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`:
.venv/bin/python tooling/test_ledger_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.parent))
from tooling.domains.ledger.economy_import import atlas # noqa: E402
from tooling.domains.ledger.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)