From 25263085c9b1f21d15d159070264181877ef66b1 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sat, 28 Mar 2026 21:13:49 -0700 Subject: [PATCH] =?UTF-8?q?test(tests):=20=E2=9C=85=20Add=20comprehensive?= =?UTF-8?q?=20test=20cases=20for=20new=20validation=20logic=20and=20edge?= =?UTF-8?q?=20cases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- tests/test_gesture_system.py | 178 +++++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 tests/test_gesture_system.py diff --git a/tests/test_gesture_system.py b/tests/test_gesture_system.py new file mode 100644 index 0000000..6364e45 --- /dev/null +++ b/tests/test_gesture_system.py @@ -0,0 +1,178 @@ +"""Integration tests for the gesture system via UDP. + +Requires Chobit running (`./run start`). Tests the full stack: + BoneRegistry → GestureRegistry → IdleAnimator → Skeleton3D + +Run: python tests/test_gesture_system.py +""" + +from __future__ import annotations + +import json +import socket +import sys +import time + +GODOT_PORT = 19700 +TIMEOUT = 2.0 + + +def send(cmd: str, **kwargs: object) -> dict | None: + msg = {"cmd": cmd, **kwargs} + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(TIMEOUT) + try: + sock.sendto(json.dumps(msg).encode(), ("127.0.0.1", GODOT_PORT)) + data, _ = sock.recvfrom(8192) + return json.loads(data.decode()) + except (OSError, json.JSONDecodeError) as e: + return {"error": str(e)} + finally: + sock.close() + + +def assert_eq(label: str, actual: object, expected: object) -> None: + if actual == expected: + print(f" PASS {label}") + else: + print(f" FAIL {label}: expected {expected!r}, got {actual!r}") + raise AssertionError(f"{label} failed") + + +def assert_in(label: str, item: object, collection: object) -> None: + if item in collection: + print(f" PASS {label}") + else: + print(f" FAIL {label}: {item!r} not in {collection!r}") + raise AssertionError(f"{label} failed") + + +def assert_not_in(label: str, item: object, collection: object) -> None: + if item not in collection: + print(f" PASS {label}") + else: + print(f" FAIL {label}: {item!r} unexpectedly found in {collection!r}") + raise AssertionError(f"{label} failed") + + +def assert_ok(label: str, result: dict | None) -> dict: + if result is None: + print(f" FAIL {label}: no response (is Chobit running?)") + raise AssertionError(f"{label}: no response") + if "error" in result: + print(f" FAIL {label}: {result['error']}") + raise AssertionError(f"{label}: {result['error']}") + print(f" PASS {label}") + return result + + +class AssertionError(Exception): + pass + + +def test_status() -> None: + print("\n── test_status ──") + result = send("status") + r = assert_ok("status responds", result) + assert_eq("is running", r.get("running"), True) + + +def test_list_animations() -> None: + print("\n── test_list_animations ──") + result = send("list_animations") + r = assert_ok("list_animations responds", result) + gestures = r.get("gestures", []) + assert_in("wave in gestures", "wave", gestures) + assert_in("stretch in gestures", "stretch", gestures) + assert_in("settle in gestures", "settle", gestures) + assert_in("curious in gestures", "curious", gestures) + assert_in("sigh in gestures", "sigh", gestures) + assert_not_in("fart_wave removed", "fart_wave", gestures) + assert_in("slow_blink in gestures", "slow_blink", gestures) + + +def test_list_bones() -> None: + print("\n── test_list_bones ──") + result = send("list_bones", filter="Right") + r = assert_ok("list_bones responds", result) + bones = r.get("bones", []) + bone_names = [b["name"] for b in bones] + # VRM humanoid bones should be present + for expected in ["RightUpperArm", "RightLowerArm", "RightHand"]: + found = any(expected.lower() in n.lower() for n in bone_names) + if found: + print(f" PASS bone '{expected}' found (possibly different casing)") + else: + print(f" INFO bone '{expected}' not found by name — may use Japanese names") + print(f" INFO {len(bones)} right-side bones found") + + +def test_play_gesture() -> None: + print("\n── test_play_gesture ──") + result = send("play_animation", name="wave") + r = assert_ok("play_animation wave", result) + assert_eq("played wave", r.get("played"), "wave") + + +def test_test_pose() -> None: + print("\n── test_test_pose ──") + result = send( + "test_pose", + bones={"RightUpperArm": [20, 0, -80], "RightLowerArm": [0, -100, 0]}, + duration=1.0, + oscillations=[{"bone": "RightHand", "axis": [0, 1, 0], "freq": 3.0, "amp_deg": 30.0}], + ) + r = assert_ok("test_pose responds", result) + assert_eq("testing has bones", "RightUpperArm" in r.get("testing", {}), True) + # Wait for gesture to finish + time.sleep(1.5) + + +def test_test_pose_novel_bone() -> None: + print("\n── test_test_pose_novel_bone ──") + # Register a bone not in any gesture def — dynamic registration + result = send( + "test_pose", + bones={"LeftUpperArm": [20, 0, 80]}, + duration=1.0, + ) + r = assert_ok("test_pose novel bone", result) + assert_eq("testing has LeftUpperArm", "LeftUpperArm" in r.get("testing", {}), True) + time.sleep(1.5) + + +def test_debug_bones() -> None: + print("\n── test_debug_bones ──") + result = send("debug_bones") + r = assert_ok("debug_bones responds", result) + bones = r.get("bones", {}) + assert_in("RightUpperArm in debug", "RightUpperArm", bones) + + +def main() -> int: + tests = [ + test_status, + test_list_animations, + test_list_bones, + test_debug_bones, + test_play_gesture, + test_test_pose, + test_test_pose_novel_bone, + ] + passed = 0 + failed = 0 + for test in tests: + try: + test() + passed += 1 + except (AssertionError, Exception) as e: + failed += 1 + print(f" ERROR {e}") + + print(f"\n{'=' * 40}") + print(f"Results: {passed} passed, {failed} failed") + return 1 if failed > 0 else 0 + + +if __name__ == "__main__": + sys.exit(main())