2025-12-29 12:15:50 +08:00
|
|
|
import asyncio
|
|
|
|
|
|
|
|
|
|
from .test_helpers import DummyContext
|
|
|
|
|
import services.tools.manage_scriptable_object as mod
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_manage_scriptable_object_forwards_create_params(monkeypatch):
|
|
|
|
|
captured = {}
|
|
|
|
|
|
|
|
|
|
async def fake_async_send(cmd, params, **kwargs):
|
|
|
|
|
captured["cmd"] = cmd
|
|
|
|
|
captured["params"] = params
|
|
|
|
|
return {"success": True, "data": {"ok": True}}
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(mod, "async_send_command_with_retry", fake_async_send)
|
|
|
|
|
|
|
|
|
|
ctx = DummyContext()
|
|
|
|
|
ctx.set_state("unity_instance", "UnityMCPTests@dummy")
|
|
|
|
|
|
|
|
|
|
result = asyncio.run(
|
|
|
|
|
mod.manage_scriptable_object(
|
|
|
|
|
ctx=ctx,
|
|
|
|
|
action="create",
|
|
|
|
|
type_name="My.Namespace.TestDefinition",
|
|
|
|
|
folder_path="Assets/Temp/Foo",
|
|
|
|
|
asset_name="Bar",
|
|
|
|
|
overwrite="true",
|
|
|
|
|
patches='[{"propertyPath":"displayName","op":"set","value":"Hello"}]',
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert result["success"] is True
|
|
|
|
|
assert captured["cmd"] == "manage_scriptable_object"
|
|
|
|
|
assert captured["params"]["action"] == "create"
|
|
|
|
|
assert captured["params"]["typeName"] == "My.Namespace.TestDefinition"
|
|
|
|
|
assert captured["params"]["folderPath"] == "Assets/Temp/Foo"
|
|
|
|
|
assert captured["params"]["assetName"] == "Bar"
|
|
|
|
|
assert captured["params"]["overwrite"] is True
|
|
|
|
|
assert isinstance(captured["params"]["patches"], list)
|
|
|
|
|
assert captured["params"]["patches"][0]["propertyPath"] == "displayName"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_manage_scriptable_object_forwards_modify_params(monkeypatch):
|
|
|
|
|
captured = {}
|
|
|
|
|
|
|
|
|
|
async def fake_async_send(cmd, params, **kwargs):
|
|
|
|
|
captured["cmd"] = cmd
|
|
|
|
|
captured["params"] = params
|
|
|
|
|
return {"success": True, "data": {"ok": True}}
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(mod, "async_send_command_with_retry", fake_async_send)
|
|
|
|
|
|
|
|
|
|
ctx = DummyContext()
|
|
|
|
|
ctx.set_state("unity_instance", "UnityMCPTests@dummy")
|
|
|
|
|
|
|
|
|
|
result = asyncio.run(
|
|
|
|
|
mod.manage_scriptable_object(
|
|
|
|
|
ctx=ctx,
|
|
|
|
|
action="modify",
|
|
|
|
|
target='{"guid":"abc"}',
|
|
|
|
|
patches=[{"propertyPath": "materials.Array.size", "op": "array_resize", "value": 2}],
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert result["success"] is True
|
|
|
|
|
assert captured["cmd"] == "manage_scriptable_object"
|
|
|
|
|
assert captured["params"]["action"] == "modify"
|
|
|
|
|
assert captured["params"]["target"] == {"guid": "abc"}
|
|
|
|
|
assert captured["params"]["patches"][0]["op"] == "array_resize"
|
|
|
|
|
|
|
|
|
|
|
Harden `manage_scriptable_object` Tool (#522)
* feat(manage_scriptable_object): harden tool with path normalization, auto-resize, bulk mapping
Phase 1: Path Syntax & Auto-Resizing
- Add NormalizePropertyPath() to convert field[index] to Array.data format
- Add EnsureArrayCapacity() to auto-grow arrays when targeting out-of-bounds indices
Phase 2: Consolidation
- Replace duplicate TryGet* helpers with ParamCoercion/VectorParsing shared utilities
- Add Vector4 parsing support to VectorParsing.cs
Phase 3: Bulk Data Mapping
- Handle JArray values for list/array properties (recursive element setting)
- Handle JObject values for nested struct/class properties
Phase 4: Enhanced Reference Resolution
- Support plain 32-char GUID strings for ObjectReference fields
Phase 5: Validation & Dry-Run
- Add ValidatePatches() for pre-validation of all patches
- Add dry_run parameter to validate without mutating
Includes comprehensive stress test suite covering:
- Big Bang (large nested arrays), Out of Bounds, Friendly Path Syntax
- Deep Nesting, Mixed References, Rapid Fire, Type Mismatch
- Bulk Array Mapping, GUID Shorthand, Dry Run validation
* feat: Add AnimationCurve and Quaternion support to manage_scriptable_object tool
- Implement TrySetAnimationCurve() supporting both {'keys': [...]} and direct [...] formats
* Support keyframe properties: time, value, inSlope, outSlope, weightedMode, inWeight, outWeight
* Gracefully default missing optional fields to 0
* Clear error messages for malformed structures
- Implement TrySetQuaternion() with 4 input formats:
* Euler array [x, y, z] - 3 elements interpreted as degrees
* Raw array [x, y, z, w] - 4 components
* Object format {x, y, z, w} - explicit components
* Explicit euler {euler: [x, y, z]} - labeled format
- Improve error handling:
* Null values: AnimationCurve→empty, Quaternion→identity
* Invalid inputs rejected with specific, actionable error messages
* Validate keyframe objects and array sizes
- Add comprehensive test coverage in ManageScriptableObjectStressTests.cs:
* AnimationCurve with keyframe array format
* AnimationCurve with direct array (no wrapper)
* Quaternion via Euler angles
* Quaternion via raw components
* Quaternion via object format
* Quaternion via explicit euler property
- Fix test file compilation issues:
* Replace undefined TestFolder with _runRoot
* Add System.IO using statement
* refactor: consolidate test utilities to eliminate duplication
- Add TestUtilities.cs with shared helpers:
- ToJObject() - consolidates 11 duplicates across test files
- EnsureFolder() - consolidates 2 duplicates
- WaitForUnityReady() - consolidates 2 duplicates
- FindFallbackShader() - consolidates shader chain duplicates
- SafeDeleteAsset() - helper for asset cleanup
- CleanupEmptyParentFolders() - standardizes TearDown cleanup
- Update 11 test files to use shared TestUtilities via 'using static'
- Standardize TearDown cleanup patterns across all test files
- Net reduction of ~40 lines while improving maintainability
* fix: add missing animCurve and rotation fields to ComplexStressSO
Add AnimationCurve and Quaternion fields required by Phase 6 stress tests.
2026-01-07 22:46:35 +08:00
|
|
|
def test_manage_scriptable_object_forwards_dry_run_param(monkeypatch):
|
|
|
|
|
captured = {}
|
|
|
|
|
|
|
|
|
|
async def fake_async_send(cmd, params, **kwargs):
|
|
|
|
|
captured["cmd"] = cmd
|
|
|
|
|
captured["params"] = params
|
|
|
|
|
return {"success": True, "data": {"dryRun": True, "validationResults": []}}
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(mod, "async_send_command_with_retry", fake_async_send)
|
|
|
|
|
|
|
|
|
|
ctx = DummyContext()
|
|
|
|
|
ctx.set_state("unity_instance", "UnityMCPTests@dummy")
|
|
|
|
|
|
|
|
|
|
result = asyncio.run(
|
|
|
|
|
mod.manage_scriptable_object(
|
|
|
|
|
ctx=ctx,
|
|
|
|
|
action="modify",
|
|
|
|
|
target='{"guid":"abc123"}',
|
|
|
|
|
patches=[{"propertyPath": "intValue", "op": "set", "value": 42}],
|
|
|
|
|
dry_run=True,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert result["success"] is True
|
|
|
|
|
assert captured["cmd"] == "manage_scriptable_object"
|
|
|
|
|
assert captured["params"]["action"] == "modify"
|
|
|
|
|
assert captured["params"]["dryRun"] is True
|
|
|
|
|
assert captured["params"]["target"] == {"guid": "abc123"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_manage_scriptable_object_dry_run_string_coercion(monkeypatch):
|
|
|
|
|
"""Test that dry_run accepts string 'true' and coerces to boolean."""
|
|
|
|
|
captured = {}
|
|
|
|
|
|
|
|
|
|
async def fake_async_send(cmd, params, **kwargs):
|
|
|
|
|
captured["cmd"] = cmd
|
|
|
|
|
captured["params"] = params
|
|
|
|
|
return {"success": True, "data": {"dryRun": True}}
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(mod, "async_send_command_with_retry", fake_async_send)
|
|
|
|
|
|
|
|
|
|
ctx = DummyContext()
|
|
|
|
|
ctx.set_state("unity_instance", "UnityMCPTests@dummy")
|
|
|
|
|
|
|
|
|
|
result = asyncio.run(
|
|
|
|
|
mod.manage_scriptable_object(
|
|
|
|
|
ctx=ctx,
|
|
|
|
|
action="modify",
|
|
|
|
|
target={"guid": "xyz"},
|
|
|
|
|
patches=[],
|
|
|
|
|
dry_run="true", # String instead of bool
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert result["success"] is True
|
|
|
|
|
assert captured["params"]["dryRun"] is True
|
|
|
|
|
|
|
|
|
|
|
2025-12-29 12:15:50 +08:00
|
|
|
|