#!/usr/bin/env bash # tests/run-atlas-determinism: Determinism smoke test for generate_atlas.py (#847) # # Imports generate_atlas as a Python module, calls process_body() twice with # seed=42 and dry_run=True, compares the returned markers dicts as JSON. # No wiki files are written or modified. # # Purpose: cheap guardrail against determinism regressions in terrain analysis, # city placement, A* road routing, infrastructure MST, and gate terminal # placement. GJ892f is a domed body (population=300, 1 city) — the smallest # well-exercised case in the atlas pipeline. # # Spec ref: #847 # Exit: 0 = deterministic (pass), non-zero = failure # Stdout: {"suite":"atlas-determinism","total":1,"passed":N,"failed":N,"duration_ms":N} set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" START_MS=$(date +%s%3N) # Write the comparison script to a real file so that the generate_atlas venv # bootstrap (os.execv) can re-exec it from the venv Python when needed. # A heredoc (python3 - <<'EOF') does not work after os.execv because stdin # has already been consumed. HELPER=$(mktemp /tmp/atlas_det_helper.XXXXXX.py) trap "rm -f '$HELPER'" EXIT cat > "$HELPER" << 'PYEOF' """Atlas determinism helper — called by tests/run-atlas-determinism (#847). Imports generate_atlas as a module and calls process_body() twice with dry_run=True. Compares the returned markers dicts as JSON. Exits 0 if identical, 1 if they differ, 2 on setup/import failure. """ import json import os import sqlite3 import sys from pathlib import Path REPO_ROOT = Path(os.environ["SR_REPO_ROOT"]) sys.path.insert(0, str(REPO_ROOT / "tooling" / "planet-gen")) try: import generate_atlas except ImportError as e: print(f"SKIP: generate_atlas import failed (missing deps?): {e}", file=sys.stderr) sys.exit(2) DB_PATH = REPO_ROOT / "server" / "data" / "systems.db" BODY_ID = "GJ892f" # domed, pop=300 → exactly 1 city; minimal and fast SEED = 42 if not DB_PATH.exists(): print(f"SKIP: systems.db not found: {DB_PATH}", file=sys.stderr) sys.exit(2) conn = sqlite3.connect(str(DB_PATH)) conn.execute("PRAGMA foreign_keys=ON") row = conn.execute(""" SELECT b.body_id, b.system_id, b.terrain_reference, b.population, b.settlement_pattern, b.planet_class, b.economic_role, COALESCE(b.cultural_corridor, s.cultural_corridor) FROM bodies b JOIN star_systems s ON b.system_id = s.system_id WHERE b.body_id = ? """, (BODY_ID,)).fetchone() conn.close() if not row: print(f"SKIP: {BODY_ID} not found in systems.db", file=sys.stderr) sys.exit(2) body_info = dict(zip( ("body_id", "system_id", "terrain_reference", "population", "settlement_pattern", "planet_class", "economic_role", "cultural_corridor"), row, )) body_info["population"] = body_info["population"] or 0 kwargs = dict( body_info=body_info, seed=SEED, noise_factor=0.25, dry_run=True, # no files written force=True, # skip the already-populated check verbose=False, ) a = generate_atlas.process_body(**kwargs) b = generate_atlas.process_body(**kwargs) if a["status"] != "generated": print(f"FAIL: run 1 status={a['status']} — {a.get('message', '')}", file=sys.stderr) sys.exit(1) if b["status"] != "generated": print(f"FAIL: run 2 status={b['status']} — {b.get('message', '')}", file=sys.stderr) sys.exit(1) json_a = json.dumps(a["markers"], indent=2) json_b = json.dumps(b["markers"], indent=2) if json_a == json_b: print(f"PASS: {BODY_ID} markers identical on two runs (seed={SEED})", file=sys.stderr) sys.exit(0) else: import difflib diff = "\n".join(list(difflib.unified_diff( json_a.splitlines(), json_b.splitlines(), lineterm="", fromfile="run1", tofile="run2", ))[:40]) print(f"FAIL: {BODY_ID} markers differ between run 1 and run 2 (seed={SEED})", file=sys.stderr) print(diff, file=sys.stderr) sys.exit(1) PYEOF set +e SR_REPO_ROOT="$REPO_ROOT" python3 "$HELPER" 2>&1 >&2 EXIT_CODE=$? set -e END_MS=$(date +%s%3N) DURATION_MS=$((END_MS - START_MS)) # Exit code 2 = setup failure / missing deps → count as 0 tests (skip, not fail) if [[ $EXIT_CODE -eq 0 ]]; then PASSED=1; FAILED=0; TOTAL=1 elif [[ $EXIT_CODE -eq 2 ]]; then PASSED=0; FAILED=0; TOTAL=0 echo " [atlas-determinism] SKIPPED (import failure or missing DB)" >&2 else PASSED=0; FAILED=1; TOTAL=1 fi printf '{"suite":"atlas-determinism","total":%d,"passed":%d,"failed":%d,"duration_ms":%d}\n' \ "$TOTAL" "$PASSED" "$FAILED" "$DURATION_MS" # Exit code 2 = venv/DB missing → treated as skip, not failure (exit 0 for run-all). # Exit code 1 = determinism failure → exit 1 to fail CI. [[ $EXIT_CODE -eq 2 ]] && exit 0 || exit $EXIT_CODE