using System; using System.IO; using System.Linq; using Newtonsoft.Json.Linq; using NUnit.Framework; using UnityEditor; using UnityEditor.Animations; using UnityEngine; using MCPForUnity.Editor.Tools.Animation; using static MCPForUnityTests.Editor.TestUtilities; namespace MCPForUnityTests.Editor.Tools { public class ManageAnimationTests { private const string TempRoot = "Assets/Temp/ManageAnimationTests"; [SetUp] public void SetUp() { EnsureFolder(TempRoot); } [TearDown] public void TearDown() { // Clean up scene objects foreach (var go in UnityEngine.Object.FindObjectsOfType()) { if (go.name.StartsWith("AnimTest_")) { UnityEngine.Object.DestroyImmediate(go); } } if (AssetDatabase.IsValidFolder(TempRoot)) { AssetDatabase.DeleteAsset(TempRoot); } CleanupEmptyParentFolders(TempRoot); } // ============================================================================= // Dispatch / Error Handling // ============================================================================= [Test] public void HandleCommand_MissingAction_ReturnsError() { var paramsObj = new JObject(); var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); Assert.That(result["message"].ToString(), Does.Contain("Action is required")); } [Test] public void HandleCommand_UnknownAction_ReturnsError() { var paramsObj = new JObject { ["action"] = "bogus_action" }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); Assert.That(result["message"].ToString(), Does.Contain("Unknown action")); } [Test] public void HandleCommand_UnknownAnimatorAction_ReturnsError() { var paramsObj = new JObject { ["action"] = "animator_nonexistent" }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); Assert.That(result["message"].ToString(), Does.Contain("Unknown animator action")); } [Test] public void HandleCommand_UnknownClipAction_ReturnsError() { var paramsObj = new JObject { ["action"] = "clip_nonexistent" }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); Assert.That(result["message"].ToString(), Does.Contain("Unknown clip action")); } // ============================================================================= // Animator: Get Info // ============================================================================= [Test] public void AnimatorGetInfo_NoTarget_ReturnsError() { var paramsObj = new JObject { ["action"] = "animator_get_info" }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); } [Test] public void AnimatorGetInfo_NoAnimator_ReturnsError() { var go = new GameObject("AnimTest_NoAnimator"); try { var paramsObj = new JObject { ["action"] = "animator_get_info", ["target"] = "AnimTest_NoAnimator" }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); Assert.That(result["message"].ToString(), Does.Contain("No Animator")); } finally { UnityEngine.Object.DestroyImmediate(go); } } [Test] public void AnimatorGetInfo_WithAnimator_ReturnsData() { var go = new GameObject("AnimTest_WithAnimator"); go.AddComponent(); try { var paramsObj = new JObject { ["action"] = "animator_get_info", ["target"] = "AnimTest_WithAnimator" }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), result.ToString()); var data = result["data"] as JObject; Assert.IsNotNull(data); Assert.AreEqual("AnimTest_WithAnimator", data["gameObject"].ToString()); Assert.IsNotNull(data["enabled"]); Assert.IsNotNull(data["speed"]); } finally { UnityEngine.Object.DestroyImmediate(go); } } // ============================================================================= // Animator: Set Speed / Set Enabled // ============================================================================= [Test] public void AnimatorSetSpeed_ChangesSpeed() { var go = new GameObject("AnimTest_Speed"); var animator = go.AddComponent(); try { var paramsObj = new JObject { ["action"] = "animator_set_speed", ["target"] = "AnimTest_Speed", ["speed"] = 2.5f }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), result.ToString()); Assert.AreEqual(2.5f, animator.speed, 0.001f); } finally { UnityEngine.Object.DestroyImmediate(go); } } [Test] public void AnimatorSetEnabled_DisablesAnimator() { var go = new GameObject("AnimTest_Enabled"); var animator = go.AddComponent(); Assert.IsTrue(animator.enabled); try { var paramsObj = new JObject { ["action"] = "animator_set_enabled", ["target"] = "AnimTest_Enabled", ["enabled"] = false }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), result.ToString()); Assert.IsFalse(animator.enabled); } finally { UnityEngine.Object.DestroyImmediate(go); } } // ============================================================================= // Clip: Create // ============================================================================= [Test] public void ClipCreate_CreatesAsset() { string clipPath = $"{TempRoot}/TestClip_{Guid.NewGuid():N}.anim"; var paramsObj = new JObject { ["action"] = "clip_create", ["clipPath"] = clipPath, ["length"] = 2.0f, ["loop"] = true }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), result.ToString()); var clip = AssetDatabase.LoadAssetAtPath(clipPath); Assert.IsNotNull(clip, "Clip asset should exist"); var settings = AnimationUtility.GetAnimationClipSettings(clip); Assert.IsTrue(settings.loopTime, "Clip should be looping"); Assert.AreEqual(2.0f, settings.stopTime, 0.01f); } [Test] public void ClipCreate_DuplicatePath_ReturnsError() { string clipPath = $"{TempRoot}/DuplicateClip.anim"; // Create first var clip = new AnimationClip { name = "DuplicateClip" }; AssetDatabase.CreateAsset(clip, clipPath); AssetDatabase.SaveAssets(); // Try to create again var paramsObj = new JObject { ["action"] = "clip_create", ["clipPath"] = clipPath, }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); Assert.That(result["message"].ToString(), Does.Contain("already exists")); } [Test] public void ClipCreate_MissingPath_ReturnsError() { var paramsObj = new JObject { ["action"] = "clip_create" }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); Assert.That(result["message"].ToString(), Does.Contain("clipPath")); } // ============================================================================= // Clip: Get Info // ============================================================================= [Test] public void ClipGetInfo_ReturnsClipData() { string clipName = $"InfoClip_{Guid.NewGuid():N}"; string clipPath = $"{TempRoot}/{clipName}.anim"; var clip = new AnimationClip { name = clipName, frameRate = 30f }; var settings = AnimationUtility.GetAnimationClipSettings(clip); settings.loopTime = true; settings.stopTime = 1.5f; AnimationUtility.SetAnimationClipSettings(clip, settings); AssetDatabase.CreateAsset(clip, clipPath); AssetDatabase.SaveAssets(); var paramsObj = new JObject { ["action"] = "clip_get_info", ["clipPath"] = clipPath }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), result.ToString()); var data = result["data"] as JObject; Assert.IsNotNull(data); Assert.AreEqual(clipName, data["name"].ToString()); Assert.AreEqual(30f, data.Value("frameRate"), 0.01f); Assert.IsTrue(data.Value("isLooping")); } [Test] public void ClipGetInfo_NotFound_ReturnsError() { var paramsObj = new JObject { ["action"] = "clip_get_info", ["clipPath"] = "Assets/Nonexistent.anim" }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); Assert.That(result["message"].ToString(), Does.Contain("not found")); } // ============================================================================= // Clip: Add / Set Curve // ============================================================================= [Test] public void ClipAddCurve_AddsKeyframes() { string clipPath = $"{TempRoot}/CurveClip_{Guid.NewGuid():N}.anim"; var clip = new AnimationClip { name = "CurveClip" }; AssetDatabase.CreateAsset(clip, clipPath); AssetDatabase.SaveAssets(); var paramsObj = new JObject { ["action"] = "clip_add_curve", ["clipPath"] = clipPath, ["propertyPath"] = "localPosition.y", ["type"] = "Transform", ["keys"] = new JArray( new JArray(0f, 0f), new JArray(0.5f, 2f), new JArray(1f, 0f) ) }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), result.ToString()); // Verify curve was added clip = AssetDatabase.LoadAssetAtPath(clipPath); var bindings = AnimationUtility.GetCurveBindings(clip); Assert.AreEqual(1, bindings.Length); Assert.AreEqual("localPosition.y", bindings[0].propertyName); var curve = AnimationUtility.GetEditorCurve(clip, bindings[0]); Assert.AreEqual(3, curve.length); } [Test] public void ClipSetCurve_ReplacesKeyframes() { string clipPath = $"{TempRoot}/SetCurveClip_{Guid.NewGuid():N}.anim"; var clip = new AnimationClip { name = "SetCurveClip" }; // Add initial curve var initialCurve = new AnimationCurve(new Keyframe(0, 0), new Keyframe(1, 1)); var binding = EditorCurveBinding.FloatCurve("", typeof(Transform), "localPosition.x"); AnimationUtility.SetEditorCurve(clip, binding, initialCurve); AssetDatabase.CreateAsset(clip, clipPath); AssetDatabase.SaveAssets(); // Replace with new keyframes var paramsObj = new JObject { ["action"] = "clip_set_curve", ["clipPath"] = clipPath, ["propertyPath"] = "localPosition.x", ["type"] = "Transform", ["keys"] = new JArray( new JArray(0f, 5f), new JArray(2f, 10f) ) }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), result.ToString()); clip = AssetDatabase.LoadAssetAtPath(clipPath); var curve = AnimationUtility.GetEditorCurve(clip, binding); Assert.AreEqual(2, curve.length); Assert.AreEqual(5f, curve.keys[0].value, 0.01f); Assert.AreEqual(10f, curve.keys[1].value, 0.01f); } [Test] public void ClipAddCurve_WithObjectFormat_ParsesKeyframes() { string clipPath = $"{TempRoot}/ObjFormatClip_{Guid.NewGuid():N}.anim"; var clip = new AnimationClip { name = "ObjFormatClip" }; AssetDatabase.CreateAsset(clip, clipPath); AssetDatabase.SaveAssets(); var paramsObj = new JObject { ["action"] = "clip_add_curve", ["clipPath"] = clipPath, ["propertyPath"] = "localPosition.z", ["type"] = "Transform", ["keys"] = new JArray( new JObject { ["time"] = 0f, ["value"] = 0f, ["inTangent"] = 0f, ["outTangent"] = 1f }, new JObject { ["time"] = 1f, ["value"] = 5f } ) }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), result.ToString()); clip = AssetDatabase.LoadAssetAtPath(clipPath); var bindings = AnimationUtility.GetCurveBindings(clip); Assert.AreEqual(1, bindings.Length); var curve = AnimationUtility.GetEditorCurve(clip, bindings[0]); Assert.AreEqual(2, curve.length); Assert.AreEqual(1f, curve.keys[0].outTangent, 0.01f); } [Test] public void ClipAddCurve_MissingKeys_ReturnsError() { string clipPath = $"{TempRoot}/NoKeysClip_{Guid.NewGuid():N}.anim"; var clip = new AnimationClip { name = "NoKeysClip" }; AssetDatabase.CreateAsset(clip, clipPath); AssetDatabase.SaveAssets(); var paramsObj = new JObject { ["action"] = "clip_add_curve", ["clipPath"] = clipPath, ["propertyPath"] = "localPosition.y", ["type"] = "Transform", }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); Assert.That(result["message"].ToString(), Does.Contain("keys")); } // ============================================================================= // Clip: Assign // ============================================================================= [Test] public void ClipAssign_AddsAnimationComponent() { string clipPath = $"{TempRoot}/AssignClip_{Guid.NewGuid():N}.anim"; var clip = new AnimationClip { name = "AssignClip" }; clip.legacy = true; AssetDatabase.CreateAsset(clip, clipPath); AssetDatabase.SaveAssets(); var go = new GameObject("AnimTest_Assign"); try { Assert.IsNull(go.GetComponent()); Assert.IsNull(go.GetComponent()); var paramsObj = new JObject { ["action"] = "clip_assign", ["target"] = "AnimTest_Assign", ["clipPath"] = clipPath }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), result.ToString()); var anim = go.GetComponent(); Assert.IsNotNull(anim, "Should have added Animation component"); Assert.IsNotNull(anim.clip, "Should have assigned clip"); } finally { UnityEngine.Object.DestroyImmediate(go); } } [Test] public void ClipAssign_MissingClip_ReturnsError() { var go = new GameObject("AnimTest_AssignMissing"); try { var paramsObj = new JObject { ["action"] = "clip_assign", ["target"] = "AnimTest_AssignMissing", ["clipPath"] = "Assets/Nonexistent.anim" }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); Assert.That(result["message"].ToString(), Does.Contain("not found")); } finally { UnityEngine.Object.DestroyImmediate(go); } } // ============================================================================= // Parameter Normalization // ============================================================================= [Test] public void HandleCommand_SnakeCaseParams_Normalized() { // Test that snake_case parameters like clip_path get normalized to clipPath string clipPath = $"{TempRoot}/SnakeCase_{Guid.NewGuid():N}.anim"; var paramsObj = new JObject { ["action"] = "clip_create", ["clip_path"] = clipPath, ["length"] = 1.0f }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), result.ToString()); var clip = AssetDatabase.LoadAssetAtPath(clipPath); Assert.IsNotNull(clip); } [Test] public void HandleCommand_PropertiesDict_Flattened() { // Test that properties dict is flattened into top-level params string clipPath = $"{TempRoot}/PropsFlat_{Guid.NewGuid():N}.anim"; var paramsObj = new JObject { ["action"] = "clip_create", ["properties"] = new JObject { ["clipPath"] = clipPath, ["length"] = 1.5f, ["loop"] = true } }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), result.ToString()); var clip = AssetDatabase.LoadAssetAtPath(clipPath); Assert.IsNotNull(clip); var settings = AnimationUtility.GetAnimationClipSettings(clip); Assert.IsTrue(settings.loopTime); } // ============================================================================= // Controller: Dispatch // ============================================================================= [Test] public void HandleCommand_UnknownControllerAction_ReturnsError() { var paramsObj = new JObject { ["action"] = "controller_nonexistent" }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); Assert.That(result["message"].ToString(), Does.Contain("Unknown controller action")); } // ============================================================================= // Controller: Create // ============================================================================= [Test] public void ControllerCreate_CreatesAsset() { string controllerPath = $"{TempRoot}/TestController_{Guid.NewGuid():N}.controller"; var paramsObj = new JObject { ["action"] = "controller_create", ["controllerPath"] = controllerPath }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), result.ToString()); var controller = AssetDatabase.LoadAssetAtPath(controllerPath); Assert.IsNotNull(controller, "Controller asset should exist"); } [Test] public void ControllerCreate_DuplicatePath_ReturnsError() { string controllerPath = $"{TempRoot}/DuplicateController.controller"; var controller = AnimatorController.CreateAnimatorControllerAtPath(controllerPath); AssetDatabase.SaveAssets(); var paramsObj = new JObject { ["action"] = "controller_create", ["controllerPath"] = controllerPath }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); Assert.That(result["message"].ToString(), Does.Contain("already exists")); } [Test] public void ControllerCreate_MissingPath_ReturnsError() { var paramsObj = new JObject { ["action"] = "controller_create" }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); Assert.That(result["message"].ToString(), Does.Contain("controllerPath")); } // ============================================================================= // Controller: Add State // ============================================================================= [Test] public void ControllerAddState_AddsState() { string controllerPath = $"{TempRoot}/StateController_{Guid.NewGuid():N}.controller"; AnimatorController.CreateAnimatorControllerAtPath(controllerPath); AssetDatabase.SaveAssets(); var paramsObj = new JObject { ["action"] = "controller_add_state", ["controllerPath"] = controllerPath, ["stateName"] = "Walk" }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), result.ToString()); var controller = AssetDatabase.LoadAssetAtPath(controllerPath); var states = controller.layers[0].stateMachine.states; Assert.IsTrue(states.Any(s => s.state.name == "Walk"), "State 'Walk' should exist"); } [Test] public void ControllerAddState_DuplicateName_ReturnsError() { string controllerPath = $"{TempRoot}/DupStateController_{Guid.NewGuid():N}.controller"; var controller = AnimatorController.CreateAnimatorControllerAtPath(controllerPath); controller.layers[0].stateMachine.AddState("Idle"); AssetDatabase.SaveAssets(); var paramsObj = new JObject { ["action"] = "controller_add_state", ["controllerPath"] = controllerPath, ["stateName"] = "Idle" }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); Assert.That(result["message"].ToString(), Does.Contain("already exists")); } [Test] public void ControllerAddState_WithClip_AssignsMotion() { string controllerPath = $"{TempRoot}/MotionController_{Guid.NewGuid():N}.controller"; AnimatorController.CreateAnimatorControllerAtPath(controllerPath); string clipPath = $"{TempRoot}/MotionClip_{Guid.NewGuid():N}.anim"; var clip = new AnimationClip { name = "MotionClip" }; AssetDatabase.CreateAsset(clip, clipPath); AssetDatabase.SaveAssets(); var paramsObj = new JObject { ["action"] = "controller_add_state", ["controllerPath"] = controllerPath, ["stateName"] = "Run", ["clipPath"] = clipPath }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), result.ToString()); var controller = AssetDatabase.LoadAssetAtPath(controllerPath); var state = controller.layers[0].stateMachine.states.First(s => s.state.name == "Run").state; Assert.IsNotNull(state.motion, "State should have motion assigned"); } // ============================================================================= // Controller: Add Transition // ============================================================================= [Test] public void ControllerAddTransition_AddsTransition() { string controllerPath = $"{TempRoot}/TransController_{Guid.NewGuid():N}.controller"; var controller = AnimatorController.CreateAnimatorControllerAtPath(controllerPath); var sm = controller.layers[0].stateMachine; sm.AddState("Idle"); sm.AddState("Walk"); AssetDatabase.SaveAssets(); var paramsObj = new JObject { ["action"] = "controller_add_transition", ["controllerPath"] = controllerPath, ["fromState"] = "Idle", ["toState"] = "Walk", ["hasExitTime"] = false, ["duration"] = 0.1f }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), result.ToString()); controller = AssetDatabase.LoadAssetAtPath(controllerPath); var idleState = controller.layers[0].stateMachine.states.First(s => s.state.name == "Idle").state; Assert.AreEqual(1, idleState.transitions.Length); Assert.AreEqual("Walk", idleState.transitions[0].destinationState.name); Assert.IsFalse(idleState.transitions[0].hasExitTime); } [Test] public void ControllerAddTransition_WithConditions_AddsConditions() { string controllerPath = $"{TempRoot}/CondController_{Guid.NewGuid():N}.controller"; var controller = AnimatorController.CreateAnimatorControllerAtPath(controllerPath); controller.AddParameter("Speed", AnimatorControllerParameterType.Float); var sm = controller.layers[0].stateMachine; sm.AddState("Idle"); sm.AddState("Walk"); AssetDatabase.SaveAssets(); var paramsObj = new JObject { ["action"] = "controller_add_transition", ["controllerPath"] = controllerPath, ["fromState"] = "Idle", ["toState"] = "Walk", ["conditions"] = new JArray( new JObject { ["parameter"] = "Speed", ["mode"] = "greater", ["threshold"] = 0.1f } ) }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), result.ToString()); controller = AssetDatabase.LoadAssetAtPath(controllerPath); var idleState = controller.layers[0].stateMachine.states.First(s => s.state.name == "Idle").state; Assert.AreEqual(1, idleState.transitions[0].conditions.Length); Assert.AreEqual("Speed", idleState.transitions[0].conditions[0].parameter); } [Test] public void ControllerAddTransition_MissingState_ReturnsError() { string controllerPath = $"{TempRoot}/MissStateController_{Guid.NewGuid():N}.controller"; var controller = AnimatorController.CreateAnimatorControllerAtPath(controllerPath); controller.layers[0].stateMachine.AddState("Idle"); AssetDatabase.SaveAssets(); var paramsObj = new JObject { ["action"] = "controller_add_transition", ["controllerPath"] = controllerPath, ["fromState"] = "Idle", ["toState"] = "Nonexistent" }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); Assert.That(result["message"].ToString(), Does.Contain("not found")); } // ============================================================================= // Controller: Add Parameter // ============================================================================= [Test] public void ControllerAddParameter_AddsParameter() { string controllerPath = $"{TempRoot}/ParamController_{Guid.NewGuid():N}.controller"; AnimatorController.CreateAnimatorControllerAtPath(controllerPath); AssetDatabase.SaveAssets(); var paramsObj = new JObject { ["action"] = "controller_add_parameter", ["controllerPath"] = controllerPath, ["parameterName"] = "Speed", ["parameterType"] = "float", ["defaultValue"] = 1.5f }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), result.ToString()); var controller = AssetDatabase.LoadAssetAtPath(controllerPath); Assert.IsTrue(controller.parameters.Any(p => p.name == "Speed"), "Parameter 'Speed' should exist"); var param = controller.parameters.First(p => p.name == "Speed"); Assert.AreEqual(AnimatorControllerParameterType.Float, param.type); Assert.AreEqual(1.5f, param.defaultFloat, 0.01f); } [Test] public void ControllerAddParameter_DuplicateName_ReturnsError() { string controllerPath = $"{TempRoot}/DupParamController_{Guid.NewGuid():N}.controller"; var controller = AnimatorController.CreateAnimatorControllerAtPath(controllerPath); controller.AddParameter("Speed", AnimatorControllerParameterType.Float); AssetDatabase.SaveAssets(); var paramsObj = new JObject { ["action"] = "controller_add_parameter", ["controllerPath"] = controllerPath, ["parameterName"] = "Speed", ["parameterType"] = "float" }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); Assert.That(result["message"].ToString(), Does.Contain("already exists")); } [Test] public void ControllerAddParameter_AllTypes() { string controllerPath = $"{TempRoot}/AllTypesController_{Guid.NewGuid():N}.controller"; AnimatorController.CreateAnimatorControllerAtPath(controllerPath); AssetDatabase.SaveAssets(); string[] types = { "float", "int", "bool", "trigger" }; foreach (var t in types) { var paramsObj = new JObject { ["action"] = "controller_add_parameter", ["controllerPath"] = controllerPath, ["parameterName"] = $"Param_{t}", ["parameterType"] = t }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), $"Failed for type {t}: {result}"); } var controller = AssetDatabase.LoadAssetAtPath(controllerPath); Assert.AreEqual(4, controller.parameters.Length); } // ============================================================================= // Controller: Get Info // ============================================================================= [Test] public void ControllerGetInfo_ReturnsData() { string controllerPath = $"{TempRoot}/InfoController_{Guid.NewGuid():N}.controller"; var controller = AnimatorController.CreateAnimatorControllerAtPath(controllerPath); controller.AddParameter("Speed", AnimatorControllerParameterType.Float); var sm = controller.layers[0].stateMachine; sm.AddState("Idle"); sm.AddState("Walk"); AssetDatabase.SaveAssets(); var paramsObj = new JObject { ["action"] = "controller_get_info", ["controllerPath"] = controllerPath }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), result.ToString()); var data = result["data"] as JObject; Assert.IsNotNull(data); Assert.AreEqual(1, data.Value("parameterCount")); Assert.AreEqual(1, data.Value("layerCount")); var layers = data["layers"] as JArray; Assert.IsNotNull(layers); Assert.AreEqual(1, layers.Count); } [Test] public void ControllerGetInfo_NotFound_ReturnsError() { var paramsObj = new JObject { ["action"] = "controller_get_info", ["controllerPath"] = "Assets/Nonexistent.controller" }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); } // ============================================================================= // Controller: Assign // ============================================================================= [Test] public void ControllerAssign_AddsAnimatorAndAssigns() { string controllerPath = $"{TempRoot}/AssignController_{Guid.NewGuid():N}.controller"; AnimatorController.CreateAnimatorControllerAtPath(controllerPath); AssetDatabase.SaveAssets(); var go = new GameObject("AnimTest_ControllerAssign"); try { Assert.IsNull(go.GetComponent()); var paramsObj = new JObject { ["action"] = "controller_assign", ["controllerPath"] = controllerPath, ["target"] = "AnimTest_ControllerAssign" }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), result.ToString()); var animator = go.GetComponent(); Assert.IsNotNull(animator, "Should have added Animator component"); Assert.IsNotNull(animator.runtimeAnimatorController, "Should have assigned controller"); } finally { UnityEngine.Object.DestroyImmediate(go); } } // ============================================================================= // Clip: Set Vector Curve // ============================================================================= [Test] public void ClipSetVectorCurve_Sets3Curves() { string clipPath = $"{TempRoot}/VectorClip_{Guid.NewGuid():N}.anim"; var clip = new AnimationClip { name = "VectorClip" }; AssetDatabase.CreateAsset(clip, clipPath); AssetDatabase.SaveAssets(); var paramsObj = new JObject { ["action"] = "clip_set_vector_curve", ["clipPath"] = clipPath, ["property"] = "localPosition", ["keys"] = new JArray( new JObject { ["time"] = 0f, ["value"] = new JArray(0f, 1f, -10f) }, new JObject { ["time"] = 1f, ["value"] = new JArray(2f, 1f, -10f) } ) }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), result.ToString()); clip = AssetDatabase.LoadAssetAtPath(clipPath); var bindings = AnimationUtility.GetCurveBindings(clip); // clip.SetCurve doesn't populate EditorCurve bindings — it uses legacy runtime curves // Verify via the data response var data = result["data"] as JObject; Assert.IsNotNull(data); Assert.AreEqual(2, data.Value("keyframeCount")); var curves = data["curves"] as JArray; Assert.IsNotNull(curves); Assert.AreEqual(3, curves.Count); } [Test] public void ClipSetVectorCurve_MissingProperty_ReturnsError() { string clipPath = $"{TempRoot}/NoPropertyClip_{Guid.NewGuid():N}.anim"; var clip = new AnimationClip { name = "NoPropertyClip" }; AssetDatabase.CreateAsset(clip, clipPath); AssetDatabase.SaveAssets(); var paramsObj = new JObject { ["action"] = "clip_set_vector_curve", ["clipPath"] = clipPath, ["keys"] = new JArray( new JObject { ["time"] = 0f, ["value"] = new JArray(0f, 0f, 0f) } ) }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); Assert.That(result["message"].ToString(), Does.Contain("property")); } [Test] public void ClipSetVectorCurve_InvalidValueFormat_ReturnsError() { string clipPath = $"{TempRoot}/BadValueClip_{Guid.NewGuid():N}.anim"; var clip = new AnimationClip { name = "BadValueClip" }; AssetDatabase.CreateAsset(clip, clipPath); AssetDatabase.SaveAssets(); var paramsObj = new JObject { ["action"] = "clip_set_vector_curve", ["clipPath"] = clipPath, ["property"] = "localPosition", ["keys"] = new JArray( new JObject { ["time"] = 0f, ["value"] = new JArray(0f, 1f) } // Only 2 elements ) }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); Assert.That(result["message"].ToString(), Does.Contain("3-element")); } // ============================================================================= // Clip: Create Preset // ============================================================================= [Test] public void ClipCreatePreset_Bounce_CreatesClip() { string clipPath = $"{TempRoot}/BouncePreset_{Guid.NewGuid():N}.anim"; var paramsObj = new JObject { ["action"] = "clip_create_preset", ["clipPath"] = clipPath, ["preset"] = "bounce", ["duration"] = 2.0f, ["amplitude"] = 0.5f, ["loop"] = true }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), result.ToString()); var clip = AssetDatabase.LoadAssetAtPath(clipPath); Assert.IsNotNull(clip, "Bounce preset clip should exist"); var settings = AnimationUtility.GetAnimationClipSettings(clip); Assert.IsTrue(settings.loopTime, "Should be looping"); } [Test] public void ClipCreatePreset_AllPresetsCreateSuccessfully() { string[] presets = { "bounce", "rotate", "pulse", "fade", "shake", "hover", "spin" }; foreach (var preset in presets) { string clipPath = $"{TempRoot}/{preset}Preset_{Guid.NewGuid():N}.anim"; var paramsObj = new JObject { ["action"] = "clip_create_preset", ["clipPath"] = clipPath, ["preset"] = preset }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsTrue(result.Value("success"), $"Preset '{preset}' failed: {result}"); var clip = AssetDatabase.LoadAssetAtPath(clipPath); Assert.IsNotNull(clip, $"Clip for preset '{preset}' should exist"); } } [Test] public void ClipCreatePreset_InvalidPreset_ReturnsError() { string clipPath = $"{TempRoot}/BadPreset_{Guid.NewGuid():N}.anim"; var paramsObj = new JObject { ["action"] = "clip_create_preset", ["clipPath"] = clipPath, ["preset"] = "nonexistent" }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); Assert.That(result["message"].ToString(), Does.Contain("Unknown preset")); } [Test] public void ClipCreatePreset_MissingPreset_ReturnsError() { string clipPath = $"{TempRoot}/NoPreset_{Guid.NewGuid():N}.anim"; var paramsObj = new JObject { ["action"] = "clip_create_preset", ["clipPath"] = clipPath }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); Assert.That(result["message"].ToString(), Does.Contain("preset")); } [Test] public void ClipCreatePreset_DuplicatePath_ReturnsError() { string clipPath = $"{TempRoot}/ExistingPreset.anim"; var clip = new AnimationClip { name = "ExistingPreset" }; AssetDatabase.CreateAsset(clip, clipPath); AssetDatabase.SaveAssets(); var paramsObj = new JObject { ["action"] = "clip_create_preset", ["clipPath"] = clipPath, ["preset"] = "bounce" }; var result = ToJObject(ManageAnimation.HandleCommand(paramsObj)); Assert.IsFalse(result.Value("success")); Assert.That(result["message"].ToString(), Does.Contain("already exists")); } } }