216 lines
6.7 KiB
Python
216 lines
6.7 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Tests for tower_visual_mapper.py — verifies map construction and formatting."""
|
||
|
|
|
||
|
|
import json
|
||
|
|
import sys
|
||
|
|
import tempfile
|
||
|
|
from pathlib import Path
|
||
|
|
from unittest.mock import patch
|
||
|
|
|
||
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
|
||
|
|
|
||
|
|
from tower_visual_mapper import (
|
||
|
|
TowerRoom, TowerNPC, TowerFloor, TowerMap,
|
||
|
|
scan_gallery_index, scan_memory_architecture, scan_wizard_configs,
|
||
|
|
build_tower_map, to_json, to_ascii, _gallery_image_to_room,
|
||
|
|
_parse_json_response
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
# === Unit Tests ===
|
||
|
|
|
||
|
|
def test_gallery_image_to_room_known():
|
||
|
|
room = _gallery_image_to_room("01-wizard-tower-bitcoin.jpg", "The Tower", "The Origin")
|
||
|
|
assert room is not None
|
||
|
|
assert room.name == "The Tower — Exterior"
|
||
|
|
assert room.floor == 0
|
||
|
|
assert "bitcoin" in room.description.lower() or "sovereign" in room.description.lower()
|
||
|
|
print(" PASS: test_gallery_image_to_room_known")
|
||
|
|
|
||
|
|
|
||
|
|
def test_gallery_image_to_room_unknown():
|
||
|
|
room = _gallery_image_to_room("random-image.jpg", "Something", "The Origin")
|
||
|
|
assert room is None
|
||
|
|
print(" PASS: test_gallery_image_to_room_unknown")
|
||
|
|
|
||
|
|
|
||
|
|
def test_gallery_image_to_room_philosophy():
|
||
|
|
room = _gallery_image_to_room("06-the-paperclip-moment.jpg", "A paperclip", "The Philosophy")
|
||
|
|
assert room is not None
|
||
|
|
assert room.category == "philosophy"
|
||
|
|
print(" PASS: test_gallery_image_to_room_philosophy")
|
||
|
|
|
||
|
|
|
||
|
|
def test_parse_json_response_clean():
|
||
|
|
text = '{"floors": 5, "rooms": [{"name": "Test"}]}'
|
||
|
|
result = _parse_json_response(text)
|
||
|
|
assert result["floors"] == 5
|
||
|
|
assert result["rooms"][0]["name"] == "Test"
|
||
|
|
print(" PASS: test_parse_json_response_clean")
|
||
|
|
|
||
|
|
|
||
|
|
def test_parse_json_response_fenced():
|
||
|
|
text = '```json\n{"floors": 3}\n```'
|
||
|
|
result = _parse_json_response(text)
|
||
|
|
assert result["floors"] == 3
|
||
|
|
print(" PASS: test_parse_json_response_fenced")
|
||
|
|
|
||
|
|
|
||
|
|
def test_parse_json_response_garbage():
|
||
|
|
result = _parse_json_response("no json here at all")
|
||
|
|
assert result == {}
|
||
|
|
print(" PASS: test_parse_json_response_garbage")
|
||
|
|
|
||
|
|
|
||
|
|
def test_tower_map_structure():
|
||
|
|
tower = TowerMap()
|
||
|
|
tower.rooms = [
|
||
|
|
TowerRoom(name="Room A", floor=0, category="test"),
|
||
|
|
TowerRoom(name="Room B", floor=0, category="test"),
|
||
|
|
TowerRoom(name="Room C", floor=1, category="other"),
|
||
|
|
]
|
||
|
|
tower.npcs = [
|
||
|
|
TowerNPC(name="NPC1", role="guard", location="Room A"),
|
||
|
|
]
|
||
|
|
|
||
|
|
output = json.loads(to_json(tower))
|
||
|
|
assert output["name"] == "The Tower"
|
||
|
|
assert output["stats"]["total_rooms"] == 3
|
||
|
|
assert output["stats"]["total_npcs"] == 1
|
||
|
|
print(" PASS: test_tower_map_structure")
|
||
|
|
|
||
|
|
|
||
|
|
def test_to_json():
|
||
|
|
tower = TowerMap()
|
||
|
|
tower.rooms = [TowerRoom(name="Test Room", floor=1)]
|
||
|
|
output = json.loads(to_json(tower))
|
||
|
|
assert output["rooms"][0]["name"] == "Test Room"
|
||
|
|
assert output["rooms"][0]["floor"] == 1
|
||
|
|
print(" PASS: test_to_json")
|
||
|
|
|
||
|
|
|
||
|
|
def test_to_ascii():
|
||
|
|
tower = TowerMap()
|
||
|
|
tower.floors = [TowerFloor(number=0, name="Ground", rooms=["Test Room"])]
|
||
|
|
tower.rooms = [TowerRoom(name="Test Room", floor=0, description="A test")]
|
||
|
|
tower.npcs = []
|
||
|
|
tower.connections = []
|
||
|
|
|
||
|
|
output = to_ascii(tower)
|
||
|
|
assert "THE TOWER" in output
|
||
|
|
assert "Test Room" in output
|
||
|
|
assert "FLOOR 0" in output
|
||
|
|
print(" PASS: test_to_ascii")
|
||
|
|
|
||
|
|
|
||
|
|
def test_to_ascii_with_npcs():
|
||
|
|
tower = TowerMap()
|
||
|
|
tower.floors = [TowerFloor(number=0, name="Ground", rooms=["The Forge"])]
|
||
|
|
tower.rooms = [TowerRoom(name="The Forge", floor=0, occupants=["Bezalel"])]
|
||
|
|
tower.npcs = [TowerNPC(name="Bezalel", role="Builder", location="The Forge")]
|
||
|
|
|
||
|
|
output = to_ascii(tower)
|
||
|
|
assert "Bezalel" in output
|
||
|
|
print(" PASS: test_to_ascii_with_npcs")
|
||
|
|
|
||
|
|
|
||
|
|
def test_scan_gallery_index(tmp_path):
|
||
|
|
# Create mock gallery
|
||
|
|
gallery = tmp_path / "grok-imagine-gallery"
|
||
|
|
gallery.mkdir()
|
||
|
|
index = gallery / "INDEX.md"
|
||
|
|
index.write_text("""# Gallery
|
||
|
|
### The Origin
|
||
|
|
| 01 | wizard-tower-bitcoin.jpg | The Tower, sovereign |
|
||
|
|
| 02 | soul-inscription.jpg | SOUL.md glowing |
|
||
|
|
### The Philosophy
|
||
|
|
| 05 | value-drift-battle.jpg | Blue vs red ships |
|
||
|
|
""")
|
||
|
|
rooms = scan_gallery_index(tmp_path)
|
||
|
|
assert len(rooms) >= 2
|
||
|
|
names = [r.name for r in rooms]
|
||
|
|
assert any("Tower" in n for n in names)
|
||
|
|
assert any("Inscription" in n for n in names)
|
||
|
|
print(" PASS: test_scan_gallery_index")
|
||
|
|
|
||
|
|
|
||
|
|
def test_scan_wizard_configs(tmp_path):
|
||
|
|
wizards = tmp_path / "wizards"
|
||
|
|
for name in ["timmy", "bezalel", "ezra"]:
|
||
|
|
wdir = wizards / name
|
||
|
|
wdir.mkdir(parents=True)
|
||
|
|
(wdir / "config.yaml").write_text("model: test\n")
|
||
|
|
|
||
|
|
npcs = scan_wizard_configs(tmp_path)
|
||
|
|
assert len(npcs) >= 3
|
||
|
|
names = [n.name for n in npcs]
|
||
|
|
assert any("Timmy" in n for n in names)
|
||
|
|
assert any("Bezalel" in n for n in names)
|
||
|
|
print(" PASS: test_scan_wizard_configs")
|
||
|
|
|
||
|
|
|
||
|
|
def test_build_tower_map_empty(tmp_path):
|
||
|
|
tower = build_tower_map(tmp_path, include_vision=False)
|
||
|
|
assert tower.name == "The Tower"
|
||
|
|
# Should still have palace rooms from MEMORY_ARCHITECTURE (won't exist in tmp, but that's fine)
|
||
|
|
assert isinstance(tower.rooms, list)
|
||
|
|
print(" PASS: test_build_tower_map_empty")
|
||
|
|
|
||
|
|
|
||
|
|
def test_room_deduplication():
|
||
|
|
tower = TowerMap()
|
||
|
|
tower.rooms = [
|
||
|
|
TowerRoom(name="Dup Room", floor=0),
|
||
|
|
TowerRoom(name="Dup Room", floor=1), # same name, different floor
|
||
|
|
TowerRoom(name="Unique Room", floor=0),
|
||
|
|
]
|
||
|
|
# Deduplicate in build_tower_map — simulate
|
||
|
|
seen = {}
|
||
|
|
deduped = []
|
||
|
|
for room in tower.rooms:
|
||
|
|
if room.name not in seen:
|
||
|
|
seen[room.name] = True
|
||
|
|
deduped.append(room)
|
||
|
|
assert len(deduped) == 2
|
||
|
|
print(" PASS: test_room_deduplication")
|
||
|
|
|
||
|
|
|
||
|
|
def run_all():
|
||
|
|
print("=== tower_visual_mapper tests ===")
|
||
|
|
tests = [
|
||
|
|
test_gallery_image_to_room_known,
|
||
|
|
test_gallery_image_to_room_unknown,
|
||
|
|
test_gallery_image_to_room_philosophy,
|
||
|
|
test_parse_json_response_clean,
|
||
|
|
test_parse_json_response_fenced,
|
||
|
|
test_parse_json_response_garbage,
|
||
|
|
test_tower_map_structure,
|
||
|
|
test_to_json,
|
||
|
|
test_to_ascii,
|
||
|
|
test_to_ascii_with_npcs,
|
||
|
|
test_scan_gallery_index,
|
||
|
|
test_scan_wizard_configs,
|
||
|
|
test_build_tower_map_empty,
|
||
|
|
test_room_deduplication,
|
||
|
|
]
|
||
|
|
passed = 0
|
||
|
|
failed = 0
|
||
|
|
for test in tests:
|
||
|
|
try:
|
||
|
|
if "tmp_path" in test.__code__.co_varnames:
|
||
|
|
with tempfile.TemporaryDirectory() as td:
|
||
|
|
test(Path(td))
|
||
|
|
else:
|
||
|
|
test()
|
||
|
|
passed += 1
|
||
|
|
except Exception as e:
|
||
|
|
print(f" FAIL: {test.__name__} — {e}")
|
||
|
|
failed += 1
|
||
|
|
|
||
|
|
print(f"\n{'ALL PASSED' if failed == 0 else f'{failed} FAILED'}: {passed}/{len(tests)}")
|
||
|
|
return failed == 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
sys.exit(0 if run_all() else 1)
|