← Files Build 3D Game RoomsARCHIVED FILE

skills/build-3d-game-rooms/scripts/test_room_system.py

8.42 KB · Sep 30, 2026 · 23:15 UTC

↓ Download file

#!/usr/bin/env python3
"""Dependency-light regression tests for room contracts, masks, and lessons."""

from __future__ import annotations

import json
import shutil
import subprocess
import sys
import tempfile
import unittest
from datetime import datetime, timezone
from pathlib import Path

import numpy as np
from PIL import Image


ROOT = Path(__file__).resolve().parent.parent
SCRIPTS = ROOT / "scripts"


class RoomSystemTests(unittest.TestCase):
    def setUp(self):
        self.tmp = Path(tempfile.mkdtemp(prefix="room-skill-test-"))

    def tearDown(self):
        shutil.rmtree(self.tmp, ignore_errors=True)

    def run_script(self, name, *args, success=True):
        result = subprocess.run([sys.executable, str(SCRIPTS / name), *map(str, args)], text=True, capture_output=True)
        if success and result.returncode:
            self.fail(result.stdout + result.stderr)
        if not success and result.returncode == 0:
            self.fail("expected failure")
        return result

    def mask(self, name, array):
        path = self.tmp / name
        Image.fromarray(array.astype(np.uint8), "L").save(path)
        return path

    def trace(self, mask, kind="door", success=True, extra=()):
        out = self.tmp / f"{mask.stem}.json"
        preview = self.tmp / f"{mask.stem}.preview.png"
        result = self.run_script("trace_opening_mask.py", mask, "--id", mask.stem, "--kind", kind, "--width-m", "1.2", "--height-m", "2.4", "--out", out, "--preview", preview, *extra, success=success)
        return result, out, preview

    def test_rectangular_and_arched_doors(self):
        rect = np.zeros((64, 64), np.uint8); rect[12:, 20:44] = 255
        _, out, preview = self.trace(self.mask("rect.png", rect))
        self.assertTrue(out.is_file() and preview.is_file())
        arch = np.zeros((64, 64), np.uint8); arch[26:, 18:46] = 255
        yy, xx = np.ogrid[:64, :64]; arch[((xx - 32) ** 2 + (yy - 26) ** 2 <= 14 ** 2) & (yy <= 26)] = 255
        self.trace(self.mask("arch.png", arch))

    def test_window_and_alcove(self):
        inner = np.zeros((64, 64), np.uint8); inner[16:48, 18:46] = 255
        self.trace(self.mask("window.png", inner), "window", extra=("--sill-m", "0.8"))
        self.trace(self.mask("alcove.png", inner), "deep_alcove", extra=("--depth-m", "0.5"))

    def test_holes_islands_grayscale_and_invalid_door_fail(self):
        island = np.zeros((64, 64), np.uint8); island[20:32, 12:24] = 255; island[20:32, 40:52] = 255
        self.trace(self.mask("islands.png", island), "window", success=False, extra=("--sill-m", "1"))
        hole = np.zeros((64, 64), np.uint8); hole[12:52, 12:52] = 255; hole[24:40, 24:40] = 0
        self.trace(self.mask("hole.png", hole), "window", success=False, extra=("--sill-m", "1"))
        gray = np.zeros((64, 64), np.uint8); gray[16:48, 16:48] = 240
        self.trace(self.mask("gray.png", gray), "window", success=False, extra=("--sill-m", "1"))
        closed = np.zeros((64, 64), np.uint8); closed[16:48, 16:48] = 255
        self.trace(self.mask("door.png", closed), "door", success=False)

    def test_package_starts_blocked(self):
        package = self.tmp / "room"
        self.run_script("create_room.py", "test-room", "--family", "display-gallery", "--output", package)
        brief = json.loads((package / "room-brief.json").read_text())
        self.assertEqual(brief["symmetry"], {"mode": "none", "order": 1, "appliesTo": [], "exceptions": []})
        result = self.run_script("validate_room.py", package, "--gate", "function", success=False)
        self.assertIn("explicit approval", result.stdout)

    def test_declared_symmetry_requires_complete_scope(self):
        package = self.tmp / "room"
        self.run_script("create_room.py", "test-room", "--family", "display-gallery", "--output", package)
        brief_path = package / "room-brief.json"
        brief = json.loads(brief_path.read_text())
        brief["symmetry"] = {"mode": "radial", "order": 8, "appliesTo": ["architecture"], "exceptions": []}
        brief_path.write_text(json.dumps(brief), encoding="utf-8")
        result = self.run_script("validate_room.py", package, "--gate", "function", success=False)
        self.assertIn("declared symmetry must apply", result.stdout)

    def test_plan_contract_defaults_to_engineering_layers(self):
        package = self.tmp / "room"
        self.run_script("create_room.py", "test-room", "--family", "display-gallery", "--output", package)
        plan = json.loads((package / "plan-metadata.json").read_text())
        self.assertEqual(plan["style"], "monochrome-engineering")
        self.assertEqual(plan["projectionSource"], "actual-meshes")
        self.assertEqual(plan["layers"], ["lower-room", "reflected-ceiling"])
        self.assertFalse(plan["aiInspirationIncluded"])
        props = json.loads((package / "props.json").read_text())
        self.assertEqual(props["generation"]["apiModel"], "meshy-5")
        layout = json.loads((package / "room-layout.json").read_text())
        self.assertEqual(layout["units"], "meters")
        self.assertTrue((package / "images").is_dir())
        self.assertTrue((package / "meshes").is_dir())

    def test_meshy_dry_run_is_pinned_and_needs_no_key(self):
        package = self.tmp / "room"
        self.run_script("create_room.py", "test-room", "--family", "display-gallery", "--output", package)
        image_path = package / "images" / "column.png"
        Image.new("RGB", (8, 8), (200, 180, 140)).save(image_path)
        reviews_path = package / "milestone-reviews.json"
        reviews = json.loads(reviews_path.read_text())
        reviews["function"]["status"] = "approved"
        reviews_path.write_text(json.dumps(reviews), encoding="utf-8")
        brief_path = package / "room-brief.json"
        brief = json.loads(brief_path.read_text())
        brief["creditBudget"]["maximumCredits"] = 30
        brief_path.write_text(json.dumps(brief), encoding="utf-8")
        props_path = package / "props.json"
        props = json.loads(props_path.read_text())
        props["generation"]["pricingVerifiedAt"] = datetime.now(timezone.utc).isoformat()
        props["generation"]["hardCreditCeiling"] = 30
        props["assets"] = [{
            "id": "column", "function": "support rhythm", "class": "reusable",
            "description": "stone column", "dimensionsMeters": [0.5, 0.5, 3],
            "attachmentFace": "floor", "semanticFront": "positive-y", "intendedInstances": 1,
            "image": {"prompt": "isolated stone column", "generator": "test", "model": "", "source": "images/column.png", "approved": True, "approvedBy": "reviewer"},
            "targetPolycount": 10000, "texturePrompt": "warm stone", "rejectionCriteria": ["missing back surface"],
            "attempts": [], "acceptedMesh": None,
        }]
        props_path.write_text(json.dumps(props), encoding="utf-8")
        result = self.run_script("meshy_image_to_3d.py", "submit", package, "--asset", "column", "--dry-run")
        payload = json.loads(result.stdout)
        self.assertEqual(payload["ai_model"], "meshy-5")
        self.assertEqual(payload["texture_resolution"], "2k")
        self.assertTrue(payload["enable_pbr"])
        self.assertNotIn("image_url", payload)

    def test_primary_arrival_requires_opening_or_exception(self):
        package = self.tmp / "room"
        self.run_script("create_room.py", "test-room", "--family", "custom", "--output", package)
        result = self.run_script("validate_room.py", package, "--gate", "function", success=False)
        self.assertIn("primary arrival opening or functional exception", result.stdout)

    def test_lessons_require_pending_decision(self):
        ledger = self.tmp / "lessons.json"
        ledger.write_text(json.dumps({"schema": "game-room.room-lessons.v1", "lessons": []}), encoding="utf-8")
        self.run_script("manage_lessons.py", "--ledger", ledger, "propose", "--id", "test", "--source-room", "room", "--observation", "obs", "--rule", "rule", "--scope", "all", "--pipeline-stages", "form")
        data = json.loads(ledger.read_text()); self.assertEqual(data["lessons"][0]["status"], "pending")
        self.run_script("manage_lessons.py", "--ledger", ledger, "decide", "--id", "test", "--decision", "approved", "--approved-by", "user")
        data = json.loads(ledger.read_text()); self.assertEqual(data["lessons"][0]["status"], "approved")
        self.run_script("manage_lessons.py", "--ledger", ledger, "decide", "--id", "test", "--decision", "rejected", "--approved-by", "user", success=False)


if __name__ == "__main__":
    unittest.main()

SHA-256: ca46d871630c0a061888bceb1593cbada969c27667272a0f279c1a3d17f21449