Files
settled-reach/tooling/economy-db/test_traits.py
T
jpmschweitzerandClaude Fable 5 ebe8742469 fix(simulation): PR #173 review round — all 9 findings addressed
T1: heritage corridor_pool excluded from the ordinary phase-1 lottery and
coverage repair (D-232 reserves the heritage sub-pool for the remoteness
dial); reachable via hero pin, necessity swerve, and the T-1003 heritage
pool only. 3 new tests.
T2+H4: coverage repair no longer grows trait_selection past K when all
slots are pinned (phase-2 necessity swerve serves the type instead);
runtime warn when authored pins exceed K; V-TT-05 importer guardrail
bounds pins per body at 5 (max ComplexityTier K). 2 new tests + 2 python
tests.
H1: body-level dispatch aggregation extracted to pure
aggregate_body_dispatch_inputs + tested directly (union mix, MAX
prosperity/K); threading test asserts identical vocabulary/pools across
co-body settlements with per-settlement swerve rates. 2 new tests.
H2: tooling/economy-db/test_traits.py — 14 stdlib unittest cases over
V-TT-03/04/05 failure branches, wired into make test-tooling.
H3: hard-gate JSON parsers now tracing::warn on malformed blobs (silent
gate-widening) matching the sibling map parsers.
H5: catalog read memoized (OnceLock) — SQL+parse once per server run,
bias stays per-body. 1 new test.
T3: TraitDistrict seed-domain doc aligned with the two-level derive chain.
T4: D-225 misattribution dropped from the reader module doc.

systems.db regenerated + stamped (traits.py is a stamped source).
Gates: full cargo test 1638 green (goldens intact), clippy -D warnings,
ruff, make test-tooling (now incl. the traits units).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 12:18:14 +02:00

314 lines
10 KiB
Python

#!/usr/bin/env python3
"""
Unit tests for economy_import.traits validation (T-995, PR #173 review H2).
Covers the failure branches of the ObjectTag registry loader/validator
(V-TT-03 existence/axis, V-TT-04 fallback-graph) and the V-TT-05 pin bound —
the `make test-tooling` dry-run only exercises the happy path against the
committed, already-valid registry.
Stdlib only (unittest) — run directly or via `make test-tooling`:
python3 tooling/economy-db/test_traits.py
"""
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 traits # noqa: E402
from economy_import.errors import ImportAborted # noqa: E402
def write_registry(tmp: Path, body: str) -> Path:
path = tmp / "object_tag_vocabulary.toml"
path.write_text(body, encoding="utf-8")
return path
VALID_REGISTRY = """
[tags.wall.generic_wall]
description = "generic wall placeholder"
generic = true
[tags.wall.brick_wall]
description = "fired brick"
fallback = "generic_wall"
[tags.roof.generic_roof]
description = "generic roof placeholder"
generic = true
[tags.roof.flat_roof]
description = "flat roof"
fallback = "generic_roof"
[tags.facade.generic_facade]
description = "generic facade placeholder"
generic = true
[tags.street.generic_street]
description = "generic street placeholder"
generic = true
"""
class RegistryValidationTests(unittest.TestCase):
"""_load_object_tag_vocabulary: V-TT-03 shape + V-TT-04 fallback graph."""
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.tmp = Path(self._tmp.name)
self._orig = traits.OBJECT_TAG_VOCABULARY_TOML
def tearDown(self):
traits.OBJECT_TAG_VOCABULARY_TOML = self._orig
self._tmp.cleanup()
def load(self, registry_toml: str | None):
if registry_toml is None:
traits.OBJECT_TAG_VOCABULARY_TOML = self.tmp / "missing.toml"
else:
traits.OBJECT_TAG_VOCABULARY_TOML = write_registry(self.tmp, registry_toml)
errors: list[str] = []
registry = traits._load_object_tag_vocabulary(errors)
return registry, errors
def test_valid_registry_loads_without_errors(self):
registry, errors = self.load(VALID_REGISTRY)
self.assertEqual(errors, [])
self.assertEqual(registry["brick_wall"]["axis"], "wall")
self.assertEqual(registry["brick_wall"]["fallback"], "generic_wall")
self.assertTrue(registry["generic_wall"]["generic"])
def test_missing_registry_is_a_vtt03_error(self):
registry, errors = self.load(None)
self.assertEqual(registry, {})
self.assertTrue(any("V-TT-03" in e and "not found" in e for e in errors))
def test_unknown_axis_is_flagged(self):
_, errors = self.load(
VALID_REGISTRY
+ """
[tags.chimney.smoke_stack]
description = "not a real axis"
fallback = "generic_wall"
"""
)
self.assertTrue(any("V-TT-03" in e and "axis 'chimney'" in e for e in errors))
def test_duplicate_tag_across_axes_is_flagged(self):
_, errors = self.load(
VALID_REGISTRY
+ """
[tags.roof.brick_wall]
description = "duplicate of a wall tag"
fallback = "generic_roof"
"""
)
self.assertTrue(any("V-TT-03" in e and "declared in both" in e for e in errors))
def test_missing_description_is_flagged(self):
_, errors = self.load(
VALID_REGISTRY
+ """
[tags.wall.bare_wall]
fallback = "generic_wall"
"""
)
self.assertTrue(any("V-TT-03" in e and "missing 'description'" in e for e in errors))
def test_generic_with_fallback_is_flagged(self):
_, errors = self.load(
"""
[tags.wall.generic_wall]
description = "generic wall"
generic = true
fallback = "generic_wall"
"""
)
self.assertTrue(any("V-TT-04" in e and "fallback-terminal" in e for e in errors))
def test_non_generic_without_fallback_is_flagged(self):
_, errors = self.load(
VALID_REGISTRY
+ """
[tags.wall.orphan_wall]
description = "no parent"
"""
)
self.assertTrue(any("V-TT-04" in e and "must declare" in e for e in errors))
def test_fallback_to_unknown_tag_is_flagged(self):
_, errors = self.load(
VALID_REGISTRY
+ """
[tags.wall.floating_wall]
description = "points nowhere"
fallback = "no_such_tag"
"""
)
self.assertTrue(any("V-TT-04" in e and "unknown tag 'no_such_tag'" in e for e in errors))
def test_fallback_cycle_is_flagged(self):
_, errors = self.load(
VALID_REGISTRY
+ """
[tags.wall.wall_a]
description = "cycles to b"
fallback = "wall_b"
[tags.wall.wall_b]
description = "cycles to a"
fallback = "wall_a"
"""
)
self.assertTrue(any("V-TT-04" in e and "cycles" in e for e in errors))
def test_wrong_generic_set_is_flagged(self):
# Drop generic_street entirely — the 4-placeholder contract breaks.
registry_toml = VALID_REGISTRY.replace(
"""
[tags.street.generic_street]
description = "generic street placeholder"
generic = true
""",
"",
)
_, errors = self.load(registry_toml)
self.assertTrue(any("V-TT-04" in e and "generic placeholders" in e for e in errors))
CATALOG_WITH_BAD_TAG = """
[templates.test_template]
label = "Test Template"
corridor_pool = "baseline"
base_weight = 10000
allow_tags = ["no_such_tag"]
zone_affinity = { MixedUse = 10000 }
"""
CATALOG_WITH_AXIS_MISMATCH = """
[templates.test_template]
label = "Test Template"
corridor_pool = "baseline"
base_weight = 10000
zone_affinity = { MixedUse = 10000 }
[templates.test_template.visual_bundle]
wall = ["flat_roof"]
"""
class CatalogCrossCheckTests(unittest.TestCase):
"""populate_trait_templates: V-TT-03 catalog→registry cross-checks."""
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.tmp = Path(self._tmp.name)
self._orig_registry = traits.OBJECT_TAG_VOCABULARY_TOML
self._orig_catalog = traits.ARCHITECTURE_TRAIT_CATALOG_TOML
traits.OBJECT_TAG_VOCABULARY_TOML = write_registry(self.tmp, VALID_REGISTRY)
self.conn = sqlite3.connect(":memory:")
self.conn.execute(
"""CREATE TABLE trait_templates (
tag TEXT PRIMARY KEY, label TEXT NOT NULL, cultural_description TEXT,
corridor_pool TEXT NOT NULL DEFAULT 'baseline', geographic_sector TEXT,
bulk_class_gate TEXT, production_ubiquity_gate TEXT,
min_prosperity_bps INTEGER NOT NULL DEFAULT 0,
base_weight INTEGER NOT NULL DEFAULT 10000,
weight_mods TEXT, zone_affinity TEXT,
allow_tags TEXT, block_tags TEXT, era_scope TEXT, visual_bundle TEXT)"""
)
def tearDown(self):
traits.OBJECT_TAG_VOCABULARY_TOML = self._orig_registry
traits.ARCHITECTURE_TRAIT_CATALOG_TOML = self._orig_catalog
self.conn.close()
self._tmp.cleanup()
def populate_errors(self, catalog_toml: str) -> str:
"""Run populate_trait_templates; return its printed error report.
A tiny test catalog also trips the V-TT-01 pool-size guardrail, so the
abort alone proves nothing — assertions must target the specific
V-TT-03 line in the captured output.
"""
import contextlib
import io
catalog = self.tmp / "architecture_trait_catalog.toml"
catalog.write_text(catalog_toml, encoding="utf-8")
traits.ARCHITECTURE_TRAIT_CATALOG_TOML = catalog
out = io.StringIO()
with contextlib.redirect_stdout(out):
with self.assertRaises(ImportAborted):
traits.populate_trait_templates(self.conn, dry_run=True)
return out.getvalue()
def test_unknown_allow_tag_reports_vtt03(self):
out = self.populate_errors(CATALOG_WITH_BAD_TAG)
self.assertIn("V-TT-03", out)
self.assertIn("no_such_tag", out)
def test_axis_mismatch_in_visual_bundle_reports_vtt03(self):
# flat_roof is a valid tag, but registered under 'roof', used as 'wall'.
out = self.populate_errors(CATALOG_WITH_AXIS_MISMATCH)
self.assertIn("V-TT-03", out)
self.assertIn("axis 'roof'", out)
class PinBoundTests(unittest.TestCase):
"""populate_atlas_body_trait_bias: V-TT-05 pins-per-body bound (H4)."""
def setUp(self):
self._tmp = tempfile.TemporaryDirectory()
self.tmp = Path(self._tmp.name)
self._orig_bias = traits.ARCHITECTURE_TRAIT_BIAS_TOML
self.conn = sqlite3.connect(":memory:")
self.conn.execute("CREATE TABLE bodies (body_id TEXT PRIMARY KEY)")
self.conn.execute("CREATE TABLE trait_templates (tag TEXT PRIMARY KEY)")
self.conn.execute(
"""CREATE TABLE atlas_body_trait_bias (
id INTEGER PRIMARY KEY AUTOINCREMENT, body_id TEXT NOT NULL,
template_tag TEXT NOT NULL, bias_kind TEXT NOT NULL,
weight_multiplier_bps INTEGER, note TEXT)"""
)
self.conn.execute("INSERT INTO bodies VALUES ('HeroBody')")
for i in range(6):
self.conn.execute("INSERT INTO trait_templates VALUES (?)", (f"tmpl_{i}",))
def tearDown(self):
traits.ARCHITECTURE_TRAIT_BIAS_TOML = self._orig_bias
self.conn.close()
self._tmp.cleanup()
def bias_toml(self, pin_count: int) -> str:
blocks = []
for i in range(pin_count):
blocks.append(
f'[[bias]]\nbody_id = "HeroBody"\ntemplate_tag = "tmpl_{i}"\nbias_kind = "pin"\n'
)
return "\n".join(blocks)
def populate(self, body: str):
path = self.tmp / "architecture_trait_bias.toml"
path.write_text(body, encoding="utf-8")
traits.ARCHITECTURE_TRAIT_BIAS_TOML = path
return traits.populate_atlas_body_trait_bias(self.conn, dry_run=True)
def test_five_pins_on_one_body_pass(self):
self.assertEqual(self.populate(self.bias_toml(5)), 5)
def test_six_pins_on_one_body_abort_with_vtt05(self):
with self.assertRaises(ImportAborted):
self.populate(self.bias_toml(6))
if __name__ == "__main__":
unittest.main(verbosity=2)