unity-mcp/Server/tests/integration/test_gameobject_resources.py

255 lines
6.7 KiB
Python
Raw Normal View History

🎮 GameObject Toolset Redesign and Streamlining (#518) * feat: Redesign GameObject API for better LLM ergonomics ## New Tools - find_gameobjects: Search GameObjects, returns paginated instance IDs only - manage_components: Component lifecycle (add, remove, set_property) ## New Resources - unity://scene/gameobject/{id}: Single GameObject data (no component serialization) - unity://scene/gameobject/{id}/components: All components (paginated) - unity://scene/gameobject/{id}/component/{name}: Single component by type ## Updated - manage_scene get_hierarchy: Now includes componentTypes array - manage_gameobject: Slimmed to lifecycle only (create, modify, delete) - Legacy actions (find, get_components, etc.) log deprecation warnings ## Extracted Utilities - ParamCoercion: Centralized int/bool/float/string coercion - VectorParsing: Vector3/Vector2/Quaternion/Color parsing - GameObjectLookup: Centralized GameObject search logic ## Test Coverage - 76 new Unity EditMode tests for ManageGameObject actions - 21 new pytest tests for Python tools/resources - New NL/T CI suite for GameObject API (GO-0 to GO-5) Addresses LLM confusion with parameter overload by splitting into focused tools and read-only resources. * feat: Add static gameobject_api helper resource for UI discoverability Adds unity://scene/gameobject-api resource that: - Shows in Cursor's resource list UI (no parameters needed) - Documents the parameterized gameobject resources - Explains the workflow: find_gameobjects → read resource - Lists examples and related tools * feat: Add GO tests to main NL/T CI workflow - Adds GO pass (GO-0 to GO-5) after T pass in claude-nl-suite.yml - Includes retry logic for incomplete GO tests - Updates all regex patterns to recognize GO-* test IDs - Updates DESIRED lists to include all 21 tests (NL-0..4, T-A..J, GO-0..5) - Updates default_titles for GO tests in markdown summary - Keeps separate claude-gameobject-suite.yml for standalone runs * feat: Add GameObject API stress tests and NL/T suite updates Stress Tests (12 new tests): - BulkCreate small/medium batches - FindGameObjects pagination with by_component search - AddComponents to single object - GetComponents with full serialization - SetComponentProperties (complex Rigidbody) - Deep hierarchy creation and path lookup - GetHierarchy with large scenes - Resource read performance tests - RapidFire create-modify-delete cycles NL/T Suite Updates: - Added GO-0..GO-10 tests in nl-gameobject-suite.md - Fixed tool naming: mcp__unity__ → mcp__UnityMCP__ Other: - Fixed LongUnityScriptClaudeTest.cs compilation errors - Added reports/, .claude/local/, scripts/local-test/ to .gitignore All 254 EditMode tests pass (250 run, 4 explicit skips) * fix: Address code review feedback - ParamCoercion: Use CultureInfo.InvariantCulture for float parsing - ManageComponents: Move Transform removal check before GetComponent - ManageGameObjectFindTests: Use try-finally for LogAssert.ignoreFailingMessages - VectorParsing: Document that quaternions are not auto-normalized - gameobject.py: Prefix unused ctx parameter with underscore * fix: Address additional code review feedback - ManageComponents: Reuse GameObjectLookup.FindComponentType instead of duplicate - ManageComponents: Log warnings when SetPropertiesOnComponent fails - GameObjectLookup: Make FindComponentType public for reuse - gameobject.py: Extract _normalize_response helper to reduce duplication - gameobject.py: Add TODO comment for unused typed response classes * fix: Address more code review feedback NL/T Prompt Fixes: - nl-gameobject-suite.md: Remove non-existent list_resources/read_resource from AllowedTools - nl-gameobject-suite.md: Fix parameter names (component_type, properties) - nl-unity-suite-nl.md: Remove unused manage_editor from AllowedTools Test Fixes: - GameObjectAPIStressTests: Add null check to ToJObject helper - GameObjectAPIStressTests: Clarify AudioSource usage comment - ManageGameObjectFindTests: Use built-in 'UI' layer instead of 'Water' - LongUnityScriptClaudeTest: Clean up NL/T test artifacts (Counte42 typo, HasTarget) * docs: Add documentation for API limitations and behaviors - GameObjectLookup.SearchByPath: Document and warn that includeInactive has no effect (Unity API limitation) - ManageComponents.TrySetProperty: Document case-insensitive lookup behavior * More test fixes and tighten parameters on python tools * fix: Align test expectation with implementation error message case * docs: update README tools and resources lists - Add missing tools: manage_components, batch_execute, find_gameobjects, refresh_unity - Add missing resources: gameobject_api, editor_state_v2 - Make descriptions more concise across all tools and resources - Ensure documentation matches current MCP server functionality * fix: Address code review feedback - ParamCoercion: Use InvariantCulture for int/double parsing consistency - ManageComponents: Remove redundant Undo.RecordObject (AddComponent handles undo) - ManageScene: Replace deprecated FindObjectsOfType with FindObjectsByType - GameObjectLookup: Add explanatory comment to empty catch block - gameobject.py: Extract _validate_instance_id helper to reduce duplication - Tests: Fix assertion for instanceID (Unity IDs can be negative) * chore: Remove accidentally committed test artifacts - Remove Materials folder (40 .mat files from interactive testing) - Remove Shaders folder (5 noise shaders from testing) - Remove test scripts (Bounce*, CylinderBounce* from testing) - Remove Temp.meta and commit.sh * test: Improve delete tests to verify actual deletion - Delete_ByTag_DeletesMatchingObjects: Verify objects are actually destroyed - Delete_ByLayer_DeletesMatchingObjects: Assert deletion using Unity null check - Delete_MultipleObjectsSameName_DeletesCorrectly: Document first-match behavior - Delete_Success_ReturnsDeletedCount: Verify count value if present All tests now verify deletion occurred rather than just checking for a result. * refactor: remove deprecated manage_gameobject actions - Remove deprecated switch cases: find, get_components, get_component, add_component, remove_component, set_component_property - Remove deprecated wrapper methods (423 lines deleted from ManageGameObject.cs) - Delete ManageGameObjectFindTests.cs (tests deprecated 'find' action) - Remove deprecated test methods from ManageGameObjectTests.cs - Add GameObject resource URIs to README documentation - Add batch_execute performance tips to README, tool description, and gameobject_api resource - Enhance batch_execute description to emphasize 10-100x performance gains Total: ~1200 lines removed. New API (find_gameobjects, manage_components, resources) is the recommended path forward. * fix: Remove starlette stubs from conftest.py Starlette is now a proper dependency via the mcp package, so we don't need to stub it anymore. The real package handles all HTTP transport needs.
2026-01-07 02:13:45 +08:00
"""
Tests for the GameObject resources.
Resources:
- unity://scene/gameobject/{instance_id}
- unity://scene/gameobject/{instance_id}/components
- unity://scene/gameobject/{instance_id}/component/{component_name}
"""
import pytest
from .test_helpers import DummyContext
import services.resources.gameobject as gameobject_res_mod
@pytest.mark.asyncio
async def test_get_gameobject_data(monkeypatch):
"""Test reading a single GameObject resource."""
captured = {}
async def fake_send(cmd, params, **kwargs):
captured["cmd"] = cmd
captured["params"] = params
return {
"success": True,
"data": {
"instanceID": 12345,
"name": "Player",
"tag": "Player",
"layer": 0,
"activeSelf": True,
"activeInHierarchy": True,
"isStatic": False,
"path": "/Player",
"componentTypes": ["Transform", "PlayerController", "Rigidbody"],
},
}
monkeypatch.setattr(
gameobject_res_mod,
"async_send_command_with_retry",
fake_send,
)
resp = await gameobject_res_mod.get_gameobject(
ctx=DummyContext(),
instance_id="12345",
)
assert resp.success is True
assert captured["params"]["instanceID"] == 12345
@pytest.mark.asyncio
async def test_get_gameobject_components(monkeypatch):
"""Test reading all components for a GameObject."""
captured = {}
async def fake_send(cmd, params, **kwargs):
captured["cmd"] = cmd
captured["params"] = params
return {
"success": True,
"data": {
"cursor": 0,
"pageSize": 25,
"next_cursor": None,
"truncated": False,
"total": 3,
"items": [
{"typeName": "UnityEngine.Transform", "instanceID": 1, "enabled": True},
{"typeName": "UnityEngine.MeshRenderer", "instanceID": 2, "enabled": True},
{"typeName": "UnityEngine.BoxCollider", "instanceID": 3, "enabled": True},
],
},
}
monkeypatch.setattr(
gameobject_res_mod,
"async_send_command_with_retry",
fake_send,
)
resp = await gameobject_res_mod.get_gameobject_components(
ctx=DummyContext(),
instance_id="12345",
)
assert resp.success is True
assert captured["params"]["instanceID"] == 12345
@pytest.mark.asyncio
async def test_get_gameobject_components_pagination(monkeypatch):
"""Test pagination parameters for components resource."""
captured = {}
async def fake_send(cmd, params, **kwargs):
captured["params"] = params
return {
"success": True,
"data": {
"cursor": 10,
"pageSize": 5,
"next_cursor": "15",
"truncated": True,
"total": 20,
"items": [],
},
}
monkeypatch.setattr(
gameobject_res_mod,
"async_send_command_with_retry",
fake_send,
)
resp = await gameobject_res_mod.get_gameobject_components(
ctx=DummyContext(),
instance_id="12345",
page_size=5,
cursor=10,
)
assert resp.success is True
p = captured["params"]
assert p["pageSize"] == 5
assert p["cursor"] == 10
@pytest.mark.asyncio
async def test_get_gameobject_components_include_properties(monkeypatch):
"""Test include_properties flag for components resource."""
captured = {}
async def fake_send(cmd, params, **kwargs):
captured["params"] = params
return {
"success": True,
"data": {
"items": [
{
"typeName": "UnityEngine.Rigidbody",
"instanceID": 123,
"mass": 1.0,
"drag": 0.0,
"useGravity": True,
}
]
},
}
monkeypatch.setattr(
gameobject_res_mod,
"async_send_command_with_retry",
fake_send,
)
resp = await gameobject_res_mod.get_gameobject_components(
ctx=DummyContext(),
instance_id="12345",
include_properties=True,
)
assert resp.success is True
assert captured["params"]["includeProperties"] is True
@pytest.mark.asyncio
async def test_get_gameobject_component_single(monkeypatch):
"""Test reading a single component by name."""
captured = {}
async def fake_send(cmd, params, **kwargs):
captured["cmd"] = cmd
captured["params"] = params
return {
"success": True,
"data": {
"typeName": "UnityEngine.Rigidbody",
"instanceID": 67890,
"mass": 5.0,
"drag": 0.1,
"angularDrag": 0.05,
"useGravity": True,
"isKinematic": False,
},
}
monkeypatch.setattr(
gameobject_res_mod,
"async_send_command_with_retry",
fake_send,
)
resp = await gameobject_res_mod.get_gameobject_component(
ctx=DummyContext(),
instance_id="12345",
component_name="Rigidbody",
)
assert resp.success is True
p = captured["params"]
assert p["instanceID"] == 12345
assert p["componentName"] == "Rigidbody"
@pytest.mark.asyncio
async def test_get_gameobject_component_not_found(monkeypatch):
"""Test error when component is not found."""
async def fake_send(cmd, params, **kwargs):
return {
"success": False,
"message": "GameObject '12345' does not have a 'NonExistent' component.",
}
monkeypatch.setattr(
gameobject_res_mod,
"async_send_command_with_retry",
fake_send,
)
resp = await gameobject_res_mod.get_gameobject_component(
ctx=DummyContext(),
instance_id="12345",
component_name="NonExistent",
)
assert resp.success is False
assert "NonExistent" in (resp.message or "")
@pytest.mark.asyncio
async def test_get_gameobject_not_found(monkeypatch):
"""Test error when GameObject is not found."""
async def fake_send(cmd, params, **kwargs):
return {
"success": False,
"message": "GameObject with instanceID '99999' not found.",
}
monkeypatch.setattr(
gameobject_res_mod,
"async_send_command_with_retry",
fake_send,
)
resp = await gameobject_res_mod.get_gameobject(
ctx=DummyContext(),
instance_id="99999",
)
assert resp.success is False
assert "99999" in (resp.message or "")