Skip to content

Commit 5cdb242

Browse files
tests(ahbg): prove UCNS geometry identity and strict presentation boundary
1 parent 2b5884a commit 5cdb242

1 file changed

Lines changed: 88 additions & 25 deletions

File tree

ahbg/presentation/tests/test_presentation.py

Lines changed: 88 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1+
"""Regression tests for the AHBG presentation-only boundary.
2+
3+
Usage guidance:
4+
Focused: ``python -m unittest ahbg.presentation.tests.test_presentation``
5+
Repo discovery: ``python -m unittest discover -s ahbg/presentation/tests -p 'test*.py'``
6+
"""
7+
18
from __future__ import annotations
29

310
import copy
411
import json
12+
import math
513
import sys
614
import unittest
715
from pathlib import Path
@@ -10,38 +18,65 @@
1018
if str(ROOT) not in sys.path:
1119
sys.path.insert(0, str(ROOT))
1220

13-
from ahbg.presentation.geometry import axial_to_xy, center_distance
21+
from ahbg.presentation.geometry import center_distance, source_to_display
1422
from ahbg.presentation.project import snapshot_from_observation
1523
from ahbg.presentation.snapshot import PresentationSnapshotError, load_snapshot, validate_snapshot
1624

1725

26+
UCNS_COMMIT = "1975fe70cf4e0826a8020c2da3047569e277af64"
27+
GEOMETRY_SOURCE = {
28+
"repository": "The-Interdependency/ucns",
29+
"commit": UCNS_COMMIT,
30+
"module": "src/ucns/mobius_seed.py",
31+
"schema_id": "ucns.mobius-seed-of-life",
32+
"schema_version": "0.1.0",
33+
"projection_id": "seed-of-life-seven-equal-circles",
34+
"selection_effect": "none",
35+
}
36+
37+
1838
class PresentationTests(unittest.TestCase):
1939
def setUp(self) -> None:
2040
self.sample = dict(load_snapshot())
2141

22-
def test_sample_is_valid(self) -> None:
42+
def test_sample_is_valid_and_pins_ucns_source(self) -> None:
2343
self.assertEqual(self.sample["standing"], "not-mechanics")
44+
self.assertEqual(self.sample["geometry_source"], GEOMETRY_SOURCE)
2445
self.assertEqual(self.sample["motions"][0]["to"], self.sample["units"][0]["tile"])
46+
self.assertEqual({tile["source_slot"] for tile in self.sample["tiles"]}, {"CENTER", *(f"RING_{i}" for i in range(6))})
2547

26-
def test_adjacent_visual_centers_are_one_radius_apart(self) -> None:
48+
def test_source_centers_are_scaled_not_reconstructed(self) -> None:
2749
radius = 64.0
28-
self.assertAlmostEqual(
29-
center_distance(axial_to_xy(0, 0, radius), axial_to_xy(1, 0, radius)),
30-
radius,
31-
)
32-
33-
def test_duplicate_tile_id_and_coordinate_fail_closed(self) -> None:
34-
for mutation in ("id", "coord"):
50+
center = source_to_display(0.0, 0.0, radius)
51+
ring0 = source_to_display(1.0, 0.0, radius)
52+
self.assertAlmostEqual(center_distance(center, ring0), radius)
53+
ring1 = source_to_display(0.5, math.sqrt(3) / 2, radius)
54+
self.assertAlmostEqual(center_distance(center, ring1), radius)
55+
56+
def test_duplicate_tile_id_source_slot_and_position_fail_closed(self) -> None:
57+
for mutation in ("id", "slot", "position"):
3558
payload = copy.deepcopy(self.sample)
3659
if mutation == "id":
3760
payload["tiles"][1]["id"] = payload["tiles"][0]["id"]
61+
elif mutation == "slot":
62+
payload["tiles"][1]["source_slot"] = payload["tiles"][0]["source_slot"]
3863
else:
39-
payload["tiles"][1]["q"] = payload["tiles"][0]["q"]
40-
payload["tiles"][1]["r"] = payload["tiles"][0]["r"]
64+
payload["tiles"][1]["x"] = payload["tiles"][0]["x"]
65+
payload["tiles"][1]["y"] = payload["tiles"][0]["y"]
4166
with self.assertRaises(PresentationSnapshotError):
4267
validate_snapshot(payload)
4368

44-
def test_bad_unit_tile_and_label_fail_closed(self) -> None:
69+
def test_unknown_fields_and_bad_unit_fields_fail_closed(self) -> None:
70+
payload = copy.deepcopy(self.sample)
71+
payload["private_prompt"] = "must not cross boundary"
72+
with self.assertRaisesRegex(PresentationSnapshotError, "undeclared fields"):
73+
validate_snapshot(payload)
74+
75+
payload = copy.deepcopy(self.sample)
76+
payload["feed"][0]["dm_state"] = "secret"
77+
with self.assertRaisesRegex(PresentationSnapshotError, "undeclared fields"):
78+
validate_snapshot(payload)
79+
4580
payload = copy.deepcopy(self.sample)
4681
payload["units"][0]["tile"] = []
4782
with self.assertRaises(PresentationSnapshotError):
@@ -54,42 +89,70 @@ def test_bad_unit_tile_and_label_fail_closed(self) -> None:
5489

5590
def test_motion_destination_must_equal_presented_unit_tile(self) -> None:
5691
payload = copy.deepcopy(self.sample)
57-
payload["motions"][0]["to"] = "e"
92+
payload["motions"][0]["to"] = "RING_1"
5893
with self.assertRaisesRegex(PresentationSnapshotError, "does not match"):
5994
validate_snapshot(payload)
6095

61-
def test_projector_uses_package_relative_snapshot_and_validates(self) -> None:
96+
payload = copy.deepcopy(self.sample)
97+
payload["motions"] = False
98+
with self.assertRaisesRegex(PresentationSnapshotError, "must be a list"):
99+
validate_snapshot(payload)
100+
101+
def test_projector_drops_internal_observation_and_feed_fields(self) -> None:
62102
observation = {
63103
"turn": 2,
104+
"geometry_source": {**GEOMETRY_SOURCE, "internal_note": "drop me"},
64105
"tiles": [
65-
{"tile_id": "c", "q": 0, "r": 0},
66-
{"tile_id": "ne", "q": 1, "r": -1},
106+
{"tile_id": "CENTER", "ucns_slot": "CENTER", "x": 0.0, "y": 0.0, "private": "drop"},
107+
{"tile_id": "RING_0", "ucns_slot": "RING_0", "x": 1.0, "y": 0.0},
67108
],
68-
"units": [{"unit_id": "A0", "tile_id": "ne", "label": "A0"}],
109+
"units": [{"unit_id": "A0", "tile_id": "RING_0", "label": "A0", "private": "drop"}],
69110
"seed": "must-not-leak",
70111
}
71112
move = {
72113
"kind": "move",
73-
"data": {"unit_id": "A0", "from_tile_id": "c", "to_tile_id": "ne"},
114+
"data": {"unit_id": "A0", "from_tile_id": "CENTER", "to_tile_id": "RING_0", "dm": "drop"},
74115
}
75116
projected = snapshot_from_observation(
76-
observation, plane_id="plane-0", move_events=[move]
117+
observation,
118+
plane_id="plane-0",
119+
move_events=[move],
120+
feed=[{"turn": 2, "text": "public", "private_prompt": "must-not-leak"}],
77121
)
78122
self.assertNotIn("seed", projected)
79-
self.assertEqual(projected["motions"], [{"unit": "A0", "from": "c", "to": "ne"}])
123+
self.assertNotIn("internal_note", projected["geometry_source"])
124+
self.assertEqual(projected["feed"], [{"turn": 2, "text": "public"}])
125+
self.assertEqual(projected["motions"], [{"unit": "A0", "from": "CENTER", "to": "RING_0"}])
80126

81-
def test_browser_source_contains_accessibility_and_layer_guards(self) -> None:
127+
def test_browser_source_contains_accessibility_and_validation_guards(self) -> None:
82128
source = (ROOT / "ahbg" / "presentation" / "board.js").read_text(encoding="utf-8")
129+
html = (ROOT / "ahbg" / "presentation" / "board.html").read_text(encoding="utf-8")
83130
for phrase in (
84131
'hit.setAttribute("tabindex", "0")',
85132
'event.key === "Enter" || event.key === " "',
86-
"tile id repeats",
87-
"q,r must be integers",
133+
"UCNS source slot repeats",
134+
"motions must be a list when present",
88135
"motion destination for",
89136
'selectionRing.setAttribute("class", "selection-ring")',
90-
"tileGroups",
137+
"minimumChord",
138+
"sourceToPixel",
91139
):
92140
self.assertIn(phrase, source)
141+
self.assertIn('role="group"', html)
142+
self.assertNotIn('role="img"', html)
143+
self.assertNotIn("axialToPixel", source)
144+
145+
def test_behavior_modules_declare_module_build(self) -> None:
146+
for relative in (
147+
"ahbg/presentation/__init__.py",
148+
"ahbg/presentation/snapshot.py",
149+
"ahbg/presentation/project.py",
150+
"ahbg/presentation/geometry.py",
151+
"ahbg/presentation/board.js",
152+
):
153+
text = (ROOT / relative).read_text(encoding="utf-8")
154+
self.assertIn("=== MODULE_BUILD ===", text, relative)
155+
self.assertIn("=== END MODULE_BUILD ===", text, relative)
93156

94157
def test_sample_json_is_plain_data(self) -> None:
95158
path = ROOT / "ahbg" / "presentation" / "sample_snapshot.json"

0 commit comments

Comments
 (0)