using System; using System.Text.RegularExpressions; using NUnit.Framework; using UnityEngine; using UnityEngine.TestTools; using Newtonsoft.Json.Linq; using MCPForUnity.Editor.Tools; using MCPForUnity.Editor.Helpers; namespace MCPForUnityTests.Editor.Tools { /// /// Tests to reproduce issue #654: PropertyConversion crash causing dispatcher unavailability /// while telemetry continues reporting success. /// public class PropertyConversionErrorHandlingTests { private GameObject testGameObject; [SetUp] public void SetUp() { testGameObject = new GameObject("PropertyConversionTestObject"); CommandRegistry.Initialize(); } [TearDown] public void TearDown() { if (testGameObject != null) { UnityEngine.Object.DestroyImmediate(testGameObject); } } /// /// Test case 1: Integer value for object reference property (AudioClip on AudioSource) /// Should return graceful error, not crash dispatcher /// [Test] public void ManageComponents_SetProperty_IntegerForObjectReference_ReturnsGracefulError() { // Add AudioSource component var audioSource = testGameObject.AddComponent(); // Try to set AudioClip (object reference) to integer 12345 var setPropertyParams = new JObject { ["action"] = "set_property", ["target"] = testGameObject.name, ["componentType"] = "AudioSource", ["property"] = "clip", ["value"] = 12345 // INCOMPATIBLE: int for AudioClip }; var result = ManageComponents.HandleCommand(setPropertyParams); // Main test: should return a result without crashing Assert.IsNotNull(result, "Should return a result, not crash dispatcher"); // If it's an ErrorResponse, verify it properly reports failure if (result is ErrorResponse errorResp) { Assert.IsFalse(errorResp.Success, "Should report failure for incompatible type"); } } /// /// Test case 2: Array format for float property (spatialBlend expects float, not array) /// Mirrors the "Array format [0, 0] for Vector2 properties" from issue #654 /// This test documents that the error is caught and doesn't crash the dispatcher /// [Test] public void ManageComponents_SetProperty_ArrayForFloatProperty_DoesNotCrashDispatcher() { // Expect the error log that will be generated LogAssert.Expect(LogType.Error, new Regex("Error converting token to System.Single")); // Add AudioSource component var audioSource = testGameObject.AddComponent(); // Try to set spatialBlend (float) to array [0, 0] // This triggers: "Error converting token to System.Single: Error reading double. Unexpected token: StartArray" var setPropertyParams = new JObject { ["action"] = "set_property", ["target"] = testGameObject.name, ["componentType"] = "AudioSource", ["property"] = "spatialBlend", ["value"] = JArray.Parse("[0, 0]") // INCOMPATIBLE: array for float }; var result = ManageComponents.HandleCommand(setPropertyParams); // Main test: dispatcher should remain responsive and return a result Assert.IsNotNull(result, "Should return a result, not crash dispatcher"); // Verify subsequent commands still work var followupParams = new JObject { ["action"] = "set_property", ["target"] = testGameObject.name, ["componentType"] = "AudioSource", ["property"] = "volume", ["value"] = 0.5f }; var followupResult = ManageComponents.HandleCommand(followupParams); Assert.IsNotNull(followupResult, "Dispatcher should still be responsive after conversion error"); } /// /// Test case 3: Multiple property conversion failures in sequence /// Tests if dispatcher remains responsive after multiple errors /// [Test] public void ManageComponents_MultipleSetPropertyFailures_DispatcherStaysResponsive() { // Expect the error log for the invalid string conversion LogAssert.Expect(LogType.Error, new Regex("Error converting token to System.Single")); var audioSource = testGameObject.AddComponent(); // First bad conversion attempt - int for AudioClip doesn't generate an error log var badParam1 = new JObject { ["action"] = "set_property", ["target"] = testGameObject.name, ["componentType"] = "AudioSource", ["property"] = "clip", ["value"] = 999 // bad: int for AudioClip }; var result1 = ManageComponents.HandleCommand(badParam1); Assert.IsNotNull(result1, "First call should return result"); // Second bad conversion attempt - generates error log var badParam2 = new JObject { ["action"] = "set_property", ["target"] = testGameObject.name, ["componentType"] = "AudioSource", ["property"] = "rolloffFactor", ["value"] = "invalid_string" // bad: string for float }; var result2 = ManageComponents.HandleCommand(badParam2); Assert.IsNotNull(result2, "Second call should return result"); // Third attempt - valid conversion var badParam3 = new JObject { ["action"] = "set_property", ["target"] = testGameObject.name, ["componentType"] = "AudioSource", ["property"] = "volume", ["value"] = 0.5f // good: float for float - dispatcher should still work }; var result3 = ManageComponents.HandleCommand(badParam3); Assert.IsNotNull(result3, "Third call should return result (dispatcher should still be responsive)"); } /// /// Test case 4: After property conversion failures, other commands still work /// Tests dispatcher responsiveness /// [Test] public void ManageComponents_AfterConversionFailure_OtherOperationsWork() { var audioSource = testGameObject.AddComponent(); // Trigger a conversion failure var failParam = new JObject { ["action"] = "set_property", ["target"] = testGameObject.name, ["componentType"] = "AudioSource", ["property"] = "clip", ["value"] = 12345 // bad }; var failResult = ManageComponents.HandleCommand(failParam); Assert.IsNotNull(failResult, "Should return result for failed conversion"); // Now try a valid operation on the same component var validParam = new JObject { ["action"] = "set_property", ["target"] = testGameObject.name, ["componentType"] = "AudioSource", ["property"] = "volume", ["value"] = 0.5f // valid: float for float }; var validResult = ManageComponents.HandleCommand(validParam); Assert.IsNotNull(validResult, "Should still be able to execute valid commands after conversion failure"); // Verify the property was actually set Assert.AreEqual(0.5f, audioSource.volume, "Volume should have been set to 0.5"); } /// /// Test case 5: Telemetry continues reporting success even after conversion errors /// This is the core of issue #654: telemetry should accurately reflect dispatcher health /// [Test] public void ManageEditor_TelemetryStatus_ReportsAccurateHealth() { // Trigger multiple conversion failures first var audioSource = testGameObject.AddComponent(); for (int i = 0; i < 3; i++) { var badParam = new JObject { ["action"] = "set_property", ["target"] = testGameObject.name, ["componentType"] = "AudioSource", ["property"] = "clip", ["value"] = i * 1000 // bad }; ManageComponents.HandleCommand(badParam); } // Now check telemetry var telemetryParams = new JObject { ["action"] = "telemetry_status" }; var telemetryResult = ManageEditor.HandleCommand(telemetryParams); Assert.IsNotNull(telemetryResult, "Telemetry should return result"); // NOTE: Issue #654 noted that telemetry returns success even when dispatcher is dead. // If telemetry returns success, that's the actual current behavior (which may be a problem). // This test just documents what happens. } /// /// Test case 6: Direct PropertyConversion error handling /// Tests if PropertyConversion.ConvertToType properly handles exceptions /// [Test] public void PropertyConversion_ConvertToType_HandlesIncompatibleTypes() { // Try to convert integer to AudioClip type var token = JToken.FromObject(12345); // PropertyConversion.ConvertToType should either: // 1. Return a valid converted value // 2. Throw an exception that can be caught // 3. Return null Exception thrownException = null; object result = null; try { result = PropertyConversion.ConvertToType(token, typeof(AudioClip)); } catch (Exception ex) { thrownException = ex; } // Document what actually happens if (thrownException != null) { Debug.Log($"PropertyConversion threw exception: {thrownException.GetType().Name}: {thrownException.Message}"); Assert.Pass($"PropertyConversion threw {thrownException.GetType().Name} - exception is being raised, not swallowed"); } else if (result == null) { Debug.Log("PropertyConversion returned null for incompatible type"); Assert.Pass("PropertyConversion returned null for incompatible type"); } else { Debug.Log($"PropertyConversion returned unexpected result: {result}"); Assert.Pass("PropertyConversion produced some result"); } } /// /// Test case 7: TryConvertToType should never throw /// [Test] public void PropertyConversion_TryConvertToType_NeverThrows() { var token = JToken.FromObject(12345); // This should never throw, only return null object result = null; Exception thrownException = null; try { result = PropertyConversion.TryConvertToType(token, typeof(AudioClip)); } catch (Exception ex) { thrownException = ex; } Assert.IsNull(thrownException, "TryConvertToType should never throw"); // Result can be null or a value, but shouldn't throw } /// /// Test case 8: ComponentOps error handling /// Tests if ComponentOps.SetProperty properly catches exceptions /// [Test] public void ComponentOps_SetProperty_HandlesConversionErrors() { var audioSource = testGameObject.AddComponent(); var token = JToken.FromObject(12345); // Try to set clip (AudioClip) to integer value bool success = ComponentOps.SetProperty(audioSource, "clip", token, out string error); Assert.IsFalse(success, "Should fail to set incompatible type"); Assert.IsNotEmpty(error, "Should provide error message"); // Verify the object is still in a valid state Assert.IsNotNull(audioSource, "AudioSource should still exist"); Assert.IsNull(audioSource.clip, "Clip should remain null (not corrupted)"); } } }