#!/usr/bin/env python3 """`reach character` decisions, exercised without launching Godot or Blender (T-1290). The garment-QA capture step is a guarded exec (D-263); what is testable is the plan — which config, which Godot, whether a virtual display is needed — and the bash script's exit codes (2 for a missing config, 3 for no Godot). The GLB strip is pure bytes, so it gets a synthetic GLB with real utility nodes. Run: .venv/bin/python tooling/test_character.py """ from __future__ import annotations import contextlib import io import os import sys import tempfile from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from tooling.core.errors import ReachError # noqa: E402 from tooling.domains.character import glb_strip, qa # noqa: E402 def check(failures: list[str], cond: bool, message: str) -> None: if not cond: failures.append(message) def expect_error(failures: list[str], fn, code: int, label: str) -> None: try: fn() failures.append(f"{label}: did not fail") except ReachError as exc: check(failures, exc.exit_code == code, f"{label}: exit {exc.exit_code}, wanted {code}") check(failures, bool(exc.fix), f"{label}: no remedy") def test_capture_plan(failures: list[str]) -> None: with tempfile.TemporaryDirectory() as tmp: home = Path(tmp) godot = home / "bin" / "godot4" godot.parent.mkdir() godot.write_text("#!/bin/sh\n") godot.chmod(0o755) never = lambda _name: None # noqa: E731 plan = qa.capture_plan("peasant", {"DISPLAY": ":0"}, home, which=never) check(failures, plan.config.name == "peasant.json", f"config name not resolved: {plan.config}") check(failures, plan.argv[0] == str(godot), f"~/bin/godot4 not preferred: {plan.argv}") check(failures, plan.argv[-1] == qa.SCENE and "--rendering-driver" in plan.argv, "scene argv changed") headless = qa.capture_plan(None, {}, home, which=never) check(failures, headless.argv[:2] == ["xvfb-run", "-a"], f"no $DISPLAY did not add xvfb-run: {headless.argv}") check(failures, headless.config.name == "peasant.json", "default config is no longer peasant") explicit = qa.capture_plan(None, {"GODOT": "/nope/godot", "DISPLAY": ":0"}, Path("/nonexistent"), which=lambda n: "/usr/bin/godot" if n == "godot" else None) check(failures, explicit.argv[0] == "/usr/bin/godot", f"PATH fallback order wrong: {explicit.argv}") expect_error(failures, lambda: qa.resolve_config("no_such_garment"), 2, "missing config") expect_error(failures, lambda: qa.resolve_godot({}, Path("/nonexistent"), which=never), 3, "missing godot") def test_glb_strip(failures: list[str]) -> None: gltf = { "asset": {"version": "2.0"}, "scene": 0, "scenes": [{"nodes": [0, 1, 2]}], "nodes": [ {"name": "Body", "mesh": 0, "skin": 0}, {"name": "Icosphere", "mesh": 1}, {"name": "WGT-root", "mesh": 1, "children": [3]}, {"name": "Child", "mesh": 0, "skin": 0}, ], "meshes": [ {"name": "Body", "primitives": [{"attributes": {"POSITION": 0, "JOINTS_0": 1}}]}, {"name": "Icosphere", "primitives": [{"attributes": {"POSITION": 0}}]}, ], "skins": [{"joints": [0]}], "buffers": [{"byteLength": 8}], } with tempfile.TemporaryDirectory() as tmp, contextlib.redirect_stderr(io.StringIO()): path = os.path.join(tmp, "x.glb") glb_strip.write_glb(gltf, b"\x00" * 8, path) result = glb_strip.process_file(path, path) check(failures, result["stripped"] == 2, f"expected 2 utility nodes stripped, got {result}") stripped, _bin = glb_strip.read_glb(path) kept = [stripped["nodes"][i]["name"] for i in stripped["scenes"][0]["nodes"]] check(failures, kept == ["Body"], f"scene roots after strip: {kept}") again = glb_strip.process_file(path, path) check(failures, again["stripped"] == 0, "a second strip was not a no-op") def main() -> int: failures: list[str] = [] test_capture_plan(failures) test_glb_strip(failures) if failures: print("test_character: FAIL", file=sys.stderr) for failure in failures: print(f" - {failure}", file=sys.stderr) return 1 print("test_character: OK — QA capture plan (config, godot order, xvfb, exit codes 2/3) " "and GLB strip decided without launching Godot") return 0 if __name__ == "__main__": sys.exit(main())