chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:49:17 +08:00
commit 7243d5823b
2201 changed files with 257291 additions and 0 deletions
@@ -0,0 +1,168 @@
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using MCPForUnity.Editor.Tools;
namespace MCPForUnityTests.Editor.Tools
{
public class AIPropertyMatchingTests
{
private List<string> sampleProperties;
[SetUp]
public void SetUp()
{
sampleProperties = new List<string>
{
"maxReachDistance",
"maxHorizontalDistance",
"maxVerticalDistance",
"moveSpeed",
"healthPoints",
"playerName",
"isEnabled",
"mass",
"velocity",
"transform"
};
}
[Test]
public void GetAllComponentProperties_ReturnsValidProperties_ForTransform()
{
var properties = ComponentResolver.GetAllComponentProperties(typeof(Transform));
Assert.IsNotEmpty(properties, "Transform should have properties");
Assert.Contains("position", properties, "Transform should have position property");
Assert.Contains("rotation", properties, "Transform should have rotation property");
Assert.Contains("localScale", properties, "Transform should have localScale property");
}
[Test]
public void GetAllComponentProperties_ReturnsEmpty_ForNullType()
{
var properties = ComponentResolver.GetAllComponentProperties(null);
Assert.IsEmpty(properties, "Null type should return empty list");
}
[Test]
public void GetFuzzyPropertySuggestions_ReturnsEmpty_ForNullInput()
{
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions(null, sampleProperties);
Assert.IsEmpty(suggestions, "Null input should return no suggestions");
}
[Test]
public void GetFuzzyPropertySuggestions_ReturnsEmpty_ForEmptyInput()
{
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions("", sampleProperties);
Assert.IsEmpty(suggestions, "Empty input should return no suggestions");
}
[Test]
public void GetFuzzyPropertySuggestions_ReturnsEmpty_ForEmptyPropertyList()
{
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions("test", new List<string>());
Assert.IsEmpty(suggestions, "Empty property list should return no suggestions");
}
[Test]
public void GetFuzzyPropertySuggestions_FindsExactMatch_AfterCleaning()
{
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions("Max Reach Distance", sampleProperties);
Assert.Contains("maxReachDistance", suggestions, "Should find exact match after cleaning spaces");
Assert.GreaterOrEqual(suggestions.Count, 1, "Should return at least one match for exact match");
}
[Test]
public void GetFuzzyPropertySuggestions_FindsMultipleWordMatches()
{
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions("max distance", sampleProperties);
Assert.Contains("maxReachDistance", suggestions, "Should match maxReachDistance");
Assert.Contains("maxHorizontalDistance", suggestions, "Should match maxHorizontalDistance");
Assert.Contains("maxVerticalDistance", suggestions, "Should match maxVerticalDistance");
}
[Test]
public void GetFuzzyPropertySuggestions_FindsSimilarStrings_WithTypos()
{
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions("movespeed", sampleProperties); // missing capital S
Assert.Contains("moveSpeed", suggestions, "Should find moveSpeed despite missing capital");
}
[Test]
public void GetFuzzyPropertySuggestions_FindsSemanticMatches_ForCommonTerms()
{
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions("weight", sampleProperties);
// Note: Current algorithm might not find "mass" but should handle it gracefully
Assert.IsNotNull(suggestions, "Should return valid suggestions list");
}
[Test]
public void GetFuzzyPropertySuggestions_LimitsResults_ToReasonableNumber()
{
// Test with input that might match many properties
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions("m", sampleProperties);
Assert.LessOrEqual(suggestions.Count, 3, "Should limit suggestions to 3 or fewer");
}
[Test]
public void GetFuzzyPropertySuggestions_CachesResults()
{
var input = "Max Reach Distance";
// First call
var suggestions1 = ComponentResolver.GetFuzzyPropertySuggestions(input, sampleProperties);
// Second call should use cache (tested indirectly by ensuring consistency)
var suggestions2 = ComponentResolver.GetFuzzyPropertySuggestions(input, sampleProperties);
Assert.AreEqual(suggestions1.Count, suggestions2.Count, "Cached results should be consistent");
CollectionAssert.AreEqual(suggestions1, suggestions2, "Cached results should be identical");
}
[Test]
public void GetFuzzyPropertySuggestions_HandlesUnityNamingConventions()
{
var unityStyleProperties = new List<string> { "isKinematic", "useGravity", "maxLinearVelocity" };
var suggestions1 = ComponentResolver.GetFuzzyPropertySuggestions("is kinematic", unityStyleProperties);
var suggestions2 = ComponentResolver.GetFuzzyPropertySuggestions("use gravity", unityStyleProperties);
var suggestions3 = ComponentResolver.GetFuzzyPropertySuggestions("max linear velocity", unityStyleProperties);
Assert.Contains("isKinematic", suggestions1, "Should handle 'is' prefix convention");
Assert.Contains("useGravity", suggestions2, "Should handle 'use' prefix convention");
Assert.Contains("maxLinearVelocity", suggestions3, "Should handle 'max' prefix convention");
}
[Test]
public void GetFuzzyPropertySuggestions_PrioritizesExactMatches()
{
var properties = new List<string> { "speed", "moveSpeed", "maxSpeed", "speedMultiplier" };
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions("speed", properties);
Assert.IsNotEmpty(suggestions, "Should find suggestions");
Assert.Contains("speed", suggestions, "Exact match should be included in results");
// Note: Implementation may or may not prioritize exact matches first
}
[Test]
public void GetFuzzyPropertySuggestions_HandlesCaseInsensitive()
{
var suggestions1 = ComponentResolver.GetFuzzyPropertySuggestions("MAXREACHDISTANCE", sampleProperties);
var suggestions2 = ComponentResolver.GetFuzzyPropertySuggestions("maxreachdistance", sampleProperties);
Assert.Contains("maxReachDistance", suggestions1, "Should handle uppercase input");
Assert.Contains("maxReachDistance", suggestions2, "Should handle lowercase input");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9e4468da1a15349029e52570b84ec4b0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,166 @@
using NUnit.Framework;
using UnityEngine;
using UnityEngine.Events;
using UnityEditor;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Tools;
using TestNamespace;
namespace MCPForUnityTests.Editor.Tools
{
/// <summary>
/// Verifies that batch_execute only normalizes top-level parameter keys (snake_case → camelCase)
/// and preserves nested value keys (e.g. Unity serialized property paths like m_PersistentCalls).
/// </summary>
public class BatchExecuteKeyPreservationTests
{
private GameObject testGo;
[OneTimeSetUp]
public void OneTimeSetUp()
{
CommandRegistry.Initialize();
}
[SetUp]
public void SetUp()
{
testGo = new GameObject("BatchKeyTestGO");
}
[TearDown]
public void TearDown()
{
if (testGo != null)
Object.DestroyImmediate(testGo);
}
[Test]
public void NestedValueKeys_WithUnderscores_ArePreservedThroughBatch()
{
testGo.AddComponent<UnityEventTestComponent>();
int targetId = testGo.GetInstanceID();
var batchParams = new JObject
{
["commands"] = new JArray
{
new JObject
{
["tool"] = "manage_components",
["params"] = new JObject
{
["action"] = "set_property",
["target"] = testGo.name,
["search_method"] = "by_name",
["component_type"] = "UnityEventTestComponent",
["property"] = "onSimpleEvent",
["value"] = JObject.Parse(@"{
""m_PersistentCalls"": {
""m_Calls"": [
{
""m_Target"": { ""instanceID"": " + targetId + @" },
""m_TargetAssemblyTypeName"": ""UnityEngine.GameObject, UnityEngine"",
""m_MethodName"": ""SetActive"",
""m_Mode"": 6,
""m_Arguments"": { ""m_BoolArgument"": true },
""m_CallState"": 2
}
]
}
}")
}
}
}
};
var result = BatchExecute.HandleCommand(batchParams).GetAwaiter().GetResult();
var resultObj = JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), $"Batch should succeed: {resultObj}");
// Verify the nested m_PersistentCalls keys were preserved (not mangled to mPersistentCalls)
var comp = testGo.GetComponent<UnityEventTestComponent>();
var so = new SerializedObject(comp);
var callsProp = so.FindProperty("onSimpleEvent.m_PersistentCalls.m_Calls");
Assert.IsNotNull(callsProp, "m_Calls property should exist");
Assert.AreEqual(1, callsProp.arraySize, "Should have 1 persistent call");
Assert.AreEqual("SetActive",
callsProp.GetArrayElementAtIndex(0).FindPropertyRelative("m_MethodName").stringValue);
}
[Test]
public void TopLevelParameterKeys_AreStillNormalized()
{
testGo.AddComponent<AudioSource>();
// Use snake_case top-level keys: search_method, component_type
var batchParams = new JObject
{
["commands"] = new JArray
{
new JObject
{
["tool"] = "manage_components",
["params"] = new JObject
{
["action"] = "set_property",
["target"] = testGo.name,
["search_method"] = "by_name",
["component_type"] = "AudioSource",
["property"] = "volume",
["value"] = 0.42f
}
}
}
};
var result = BatchExecute.HandleCommand(batchParams).GetAwaiter().GetResult();
var resultObj = JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"),
$"Batch with snake_case top-level keys should succeed: {resultObj}");
Assert.AreEqual(0.42f, testGo.GetComponent<AudioSource>().volume, 0.001f);
}
[Test]
public void Regression_CreateGameObject_StillWorksViaBatch()
{
string goName = "BatchCreatedGO_" + System.Guid.NewGuid().ToString("N").Substring(0, 8);
GameObject created = null;
try
{
var batchParams = new JObject
{
["commands"] = new JArray
{
new JObject
{
["tool"] = "manage_gameobject",
["params"] = new JObject
{
["action"] = "create",
["name"] = goName,
["primitive_type"] = "Cube"
}
}
}
};
var result = BatchExecute.HandleCommand(batchParams).GetAwaiter().GetResult();
var resultObj = JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), $"Batch create GO should succeed: {resultObj}");
created = GameObject.Find(goName);
Assert.IsNotNull(created, $"GameObject '{goName}' should exist in scene");
}
finally
{
if (created != null)
Object.DestroyImmediate(created);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d612089c86a754594916e29bb33b340d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,67 @@
using System;
using MCPForUnity.Editor.Tools.Build;
using NUnit.Framework;
using UnityEditor;
namespace MCPForUnity.Tests.EditMode.Tools
{
[TestFixture]
public class BuildTargetMappingTests
{
[TestCase("windows64", BuildTarget.StandaloneWindows64)]
[TestCase("macos", BuildTarget.StandaloneOSX)]
[TestCase("linux", BuildTarget.StandaloneLinux64)]
[TestCase("tvos", BuildTarget.tvOS)]
public void TryResolveBuildTarget_KnownAliasesResolve(string name, BuildTarget expected)
{
Assert.IsTrue(BuildTargetMapping.TryResolveBuildTarget(name, out var target));
Assert.AreEqual(expected, target);
}
[Test]
public void TryResolveBuildTarget_NumericInputDoesNotResolve()
{
Assert.IsFalse(BuildTargetMapping.TryResolveBuildTarget("5", out _));
}
[Test]
public void TryResolveNamedBuildTarget_UnknownTargetListsOnlyAvailableTargets()
{
string error = BuildTargetMapping.TryResolveNamedBuildTarget("not-a-target", out _);
Assert.IsNotNull(error);
StringAssert.Contains("windows64", error);
bool visionOSAvailable = Enum.TryParse("VisionOS", true, out BuildTarget _);
if (visionOSAvailable)
{
StringAssert.Contains("visionos", error);
}
else
{
Assert.IsFalse(error.Contains("visionos"));
}
}
[Test]
public void TryResolveNamedBuildTarget_VisionOSUnavailableReturnsHelpfulError()
{
bool visionOSAvailable = Enum.TryParse("VisionOS", true, out BuildTarget _);
string error = BuildTargetMapping.TryResolveNamedBuildTarget("visionos", out _);
if (visionOSAvailable)
{
Assert.IsTrue(BuildTargetMapping.TryResolveBuildTarget("visionos", out _));
Assert.IsTrue(
error == null || error.Contains("VisionOS"),
$"Expected no error or a VisionOS-specific error, got: {error}");
}
else
{
Assert.IsFalse(BuildTargetMapping.TryResolveBuildTarget("visionos", out _));
Assert.IsNotNull(error);
StringAssert.Contains("VisionOS build target is not available", error);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 94419cfeebb59e84ea16331766eaad52
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8c3f1a2b4d5e6f7089012345abcdef01
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,594 @@
using System;
using NUnit.Framework;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Tools;
using MCPForUnity.Editor.Tools.Prefabs;
using UnityEditor;
using UnityEngine;
namespace MCPForUnityTests.Editor.Tools.Characterization
{
/// <summary>
/// Characterization tests for Editor Tools domain.
/// These tests capture CURRENT behavior without refactoring.
/// They serve as a regression baseline for future refactoring work.
///
/// Based on analysis in: MCPForUnity/Editor/Tools/Tests/CHARACTERIZATION_SUMMARY.md
///
/// Sampled tools: ManageEditor, ManageMaterial, FindGameObjects, ManagePrefabs, ExecuteMenuItem
/// </summary>
[TestFixture]
public class EditorToolsCharacterizationTests
{
private static JObject ToJO(object o) => JObject.FromObject(o);
#region Section 1: HandleCommand Entry Point and Null/Empty Parameter Handling
/// <summary>
/// Current behavior: All tools have a single public HandleCommand(JObject) entry point.
/// This is the standard pattern - tests verify all sampled tools follow it.
/// </summary>
[Test]
public void HandleCommand_ManageEditor_WithNullParams_ReturnsErrorResponse()
{
// FIXED BEHAVIOR (P1-1 ToolParams refactoring): ManageEditor now handles null params gracefully
// Returns ErrorResponse instead of throwing NullReferenceException
var result = ManageEditor.HandleCommand(null);
var jo = ToJO(result);
Assert.IsFalse((bool)jo["success"], "Should return error for null params");
Assert.IsNotNull(jo["error"], "Should have error message");
Assert.That((string)jo["error"], Does.Contain("cannot be null"), "Should indicate parameters are null");
}
[Test]
public void HandleCommand_FindGameObjects_WithNullParams_ReturnsErrorResponse()
{
// CURRENT BEHAVIOR: FindGameObjects DOES handle null params gracefully - returns ErrorResponse
// This is good design and should be preserved during refactoring.
var result = FindGameObjects.HandleCommand(null);
var jo = ToJO(result);
Assert.IsFalse((bool)jo["success"], "Should return error for null params");
Assert.IsNotNull(jo["error"], "Should have error message");
}
[Test]
public void HandleCommand_ManageEditor_WithoutActionParameter_ReturnsError()
{
// Current behavior: Action parameter is required for dispatch
var result = ManageEditor.HandleCommand(new JObject());
var jo = ToJO(result);
Assert.IsFalse((bool)jo["success"], "Should require action parameter");
Assert.IsNotNull(jo["error"], "Should have error message");
}
[Test]
public void HandleCommand_ActionNormalization_CaseInsensitive()
{
// Current behavior: Actions are normalized to lowercase for comparison
// Using telemetry_status (read-only) instead of play to avoid mutating editor state
var upperResult = ManageEditor.HandleCommand(new JObject { ["action"] = "TELEMETRY_STATUS" });
var lowerResult = ManageEditor.HandleCommand(new JObject { ["action"] = "telemetry_status" });
var upperJo = ToJO(upperResult);
var lowerJo = ToJO(lowerResult);
// Both should succeed or both should fail in the same way (action recognized)
Assert.AreEqual((bool)upperJo["success"], (bool)lowerJo["success"],
"Case normalization should make both behave identically");
}
#endregion
#region Section 2: Parameter Extraction and Validation
[Test]
public void HandleCommand_FindGameObjects_WithCamelCaseSearchMethod_Succeeds()
{
// Current behavior: Tools accept camelCase parameter names
var result = FindGameObjects.HandleCommand(new JObject
{
["searchTerm"] = "TestObject",
["searchMethod"] = "by_name"
});
var jo = ToJO(result);
// FindGameObjects should accept the parameter (may return empty results)
Assert.IsTrue((bool)jo["success"], "Should accept camelCase parameter");
}
[Test]
public void HandleCommand_FindGameObjects_WithSnakeCaseSearchMethod_Succeeds()
{
// Current behavior: Tools also accept snake_case parameter names
var result = FindGameObjects.HandleCommand(new JObject
{
["searchTerm"] = "TestObject",
["search_method"] = "by_name"
});
var jo = ToJO(result);
Assert.IsTrue((bool)jo["success"], "Should accept snake_case parameter");
}
[Test]
public void HandleCommand_FindGameObjects_WithoutSearchMethod_UsesDefault()
{
// Current behavior: searchMethod defaults to "by_name"
var result = FindGameObjects.HandleCommand(new JObject
{
["searchTerm"] = "TestObject"
});
var jo = ToJO(result);
Assert.IsTrue((bool)jo["success"], "Should use default search method");
}
[Test]
public void HandleCommand_FindGameObjects_ClampsPageSizeToValidRange()
{
// Current behavior: pageSize is clamped to 1-500 range
var result = FindGameObjects.HandleCommand(new JObject
{
["searchTerm"] = "TestObject",
["pageSize"] = 1000 // Exceeds max
});
var jo = ToJO(result);
Assert.IsTrue((bool)jo["success"], "Should clamp and succeed");
}
[Test]
public void HandleCommand_ManageEditor_SetActiveTool_RequiresToolNameParameter()
{
// Current behavior: set_active_tool requires tool_name parameter
var result = ManageEditor.HandleCommand(new JObject
{
["action"] = "set_active_tool"
// Missing tool_name
});
var jo = ToJO(result);
Assert.IsFalse((bool)jo["success"], "Should require tool_name");
}
[Test]
public void HandleCommand_ManageEditor_ActionsRecognized()
{
// Current behavior: Valid actions are recognized and return response objects
// Using telemetry_status (read-only) to avoid mutating editor state
var result = ManageEditor.HandleCommand(new JObject
{
["action"] = "telemetry_status"
});
// Action should be recognized and return valid response
var jo = ToJO(result);
Assert.IsNotNull(jo, "Should return a response object");
Assert.IsTrue(jo.ContainsKey("success"), "Response should have success field");
}
#endregion
#region Section 3: Action Switch Dispatch
[Test]
public void HandleCommand_ManageEditor_WithUnknownAction_ReturnsError()
{
// Current behavior: Unknown actions return error with descriptive message
var result = ManageEditor.HandleCommand(new JObject
{
["action"] = "nonexistent_action_xyz"
});
var jo = ToJO(result);
Assert.IsFalse((bool)jo["success"], "Should fail for unknown action");
StringAssert.Contains("nonexistent_action_xyz", jo["error"]?.ToString() ?? "",
"Error should mention the unknown action");
}
[Test]
public void HandleCommand_ManageEditor_DifferentActionsDispatchToDifferentHandlers()
{
// Current behavior: Different actions dispatch to different handlers
// Using read-only actions to avoid mutating editor state
var statusResult = ManageEditor.HandleCommand(new JObject { ["action"] = "telemetry_status" });
var pingResult = ManageEditor.HandleCommand(new JObject { ["action"] = "telemetry_ping" });
// Both should return responses
Assert.IsNotNull(statusResult, "Status should return response");
Assert.IsNotNull(pingResult, "Ping should return response");
}
[Test]
public void HandleCommand_ManageMaterial_WithUnknownAction_ReturnsError()
{
// Current behavior: Material tool also returns error for unknown actions
var result = ManageMaterial.HandleCommand(new JObject
{
["action"] = "unknown_material_action"
});
var jo = ToJO(result);
Assert.IsFalse((bool)jo["success"], "Should fail for unknown action");
}
#endregion
#region Section 4: Error Handling and Logging
[Test]
public void HandleCommand_ManagePrefabs_WithInvalidParameters_ReturnsError()
{
// Current behavior: Invalid parameters caught and returned as ErrorResponse
var result = ManagePrefabs.HandleCommand(new JObject
{
["action"] = "create_from_gameobject"
// Missing required parameters
});
var jo = ToJO(result);
Assert.IsFalse((bool)jo["success"], "Should fail with invalid params");
Assert.IsNotNull(jo["error"], "Should have error description");
}
[Test]
public void HandleCommand_ManageEditor_ReturnsResponseObject()
{
// Current behavior: All responses are either SuccessResponse or ErrorResponse
// Using telemetry_status (read-only) to avoid mutating editor state
var result = ManageEditor.HandleCommand(new JObject { ["action"] = "telemetry_status" });
var jo = ToJO(result);
// Verify response has expected shape
Assert.IsTrue(jo.ContainsKey("success"), "Response should have 'success' field");
}
[Test]
public void HandleCommand_ErrorMessages_AreContextSpecific()
{
// Current behavior: Error messages include context about what went wrong
var result = ManageEditor.HandleCommand(new JObject
{
["action"] = "add_tag"
// Missing tag_name
});
var jo = ToJO(result);
var error = jo["error"]?.ToString() ?? "";
// Error should mention what's missing or wrong
Assert.IsTrue(error.Length > 0, "Should have descriptive error message");
}
[Test]
public void HandleCommand_ManageEditor_SafelyHandlesNullTokens()
{
// Current behavior: Null-safe token access pattern prevents NullReferenceException
// This test verifies ManageEditor doesn't crash on partial params
Assert.DoesNotThrow(() =>
{
ManageEditor.HandleCommand(new JObject { ["action"] = null });
}, "Should handle null action token without exception");
}
#endregion
#region Section 5: Inline Parameter Validation and Coercion
[Test]
public void HandleCommand_ManageEditor_AddTag_RequiresTagName()
{
// Current behavior: add_tag validates tag_name is present before mutation
var result = ManageEditor.HandleCommand(new JObject
{
["action"] = "add_tag"
});
var jo = ToJO(result);
Assert.IsFalse((bool)jo["success"], "Should require tag_name parameter");
}
[Test]
public void HandleCommand_ManagePrefabs_WithoutRequiredPath_ReturnsError()
{
// Current behavior: Required path parameter validated before operation
var result = ManagePrefabs.HandleCommand(new JObject
{
["action"] = "get_info"
// Missing path parameter
});
var jo = ToJO(result);
Assert.IsFalse((bool)jo["success"], "Should require path parameter");
}
[Test]
public void HandleCommand_ManageMaterial_Create_RequiresNameParameter()
{
// Current behavior: create action requires name parameter
var result = ManageMaterial.HandleCommand(new JObject
{
["action"] = "create"
// Missing name
});
var jo = ToJO(result);
Assert.IsFalse((bool)jo["success"], "Should require name parameter");
}
[Test]
public void HandleCommand_ValidationOccursBeforeStateMutation()
{
// Current behavior: Parameters are validated before any state changes
// This is verified by checking that invalid params don't cause side effects
var result = ManageEditor.HandleCommand(new JObject
{
["action"] = "add_layer"
// Missing layer_name - should fail before attempting to add
});
var jo = ToJO(result);
Assert.IsFalse((bool)jo["success"], "Should validate before mutation");
}
#endregion
#region Section 6: State Mutation and Side Effects
[Test]
public void HandleCommand_ManageEditor_ReadOnlyActionsDoNotMutateState()
{
// Current behavior: Read-only actions like telemetry_status don't mutate editor state
// Verify isPlaying remains false after calling telemetry_status
var wasPlayingBefore = UnityEditor.EditorApplication.isPlaying;
var result = ManageEditor.HandleCommand(new JObject { ["action"] = "telemetry_status" });
var isPlayingAfter = UnityEditor.EditorApplication.isPlaying;
Assert.AreEqual(wasPlayingBefore, isPlayingAfter, "Read-only actions should not change play mode state");
}
[Test]
public void HandleCommand_ManageMaterial_CreateAction_RequiresValidParams()
{
// Current behavior: Asset creation requires valid parameters
// This documents that side effects only occur with valid params
var result = ManageMaterial.HandleCommand(new JObject
{
["action"] = "create",
["name"] = "" // Empty name should fail
});
var jo = ToJO(result);
// Either fails validation or succeeds (behavior may vary)
Assert.IsTrue(jo.ContainsKey("success"), "Should return response");
}
#endregion
#region Section 7: Complex Parameter Handling and Object Resolution
[Test]
public void HandleCommand_FindGameObjects_ReturnsPaginationMetadata()
{
// Current behavior: FindGameObjects returns pagination info
var result = FindGameObjects.HandleCommand(new JObject
{
["searchTerm"] = "*",
["pageSize"] = 10
});
var jo = ToJO(result);
if ((bool)jo["success"])
{
var data = jo["data"];
// Pagination metadata should be present
Assert.IsNotNull(data, "Should have data field");
}
}
[Test]
public void HandleCommand_FindGameObjects_SearchMethodOptions()
{
// Current behavior: Supports multiple search methods
string[] methods = { "by_name", "by_path", "by_tag", "by_layer", "by_component" };
foreach (var method in methods)
{
var result = FindGameObjects.HandleCommand(new JObject
{
["searchTerm"] = "TestQuery",
["searchMethod"] = method
});
var jo = ToJO(result);
// All methods should be recognized and succeed
Assert.IsTrue((bool)jo["success"], $"Method {method} should be recognized and succeed");
}
}
[Test]
public void HandleCommand_FindGameObjects_PageSizeRange()
{
// Current behavior: pageSize clamped to 1-500
// Test with boundary values
var minResult = FindGameObjects.HandleCommand(new JObject
{
["searchTerm"] = "Test",
["pageSize"] = 0 // Should clamp to 1
});
var maxResult = FindGameObjects.HandleCommand(new JObject
{
["searchTerm"] = "Test",
["pageSize"] = 1000 // Should clamp to 500
});
Assert.IsNotNull(ToJO(minResult), "Should handle min boundary");
Assert.IsNotNull(ToJO(maxResult), "Should handle max boundary");
}
#endregion
#region Section 8: Security and Filtering
[Test]
public void HandleCommand_ExecuteMenuItem_BlacklistsQuit()
{
// Current behavior: File/Quit is blacklisted for safety
var result = ExecuteMenuItem.HandleCommand(new JObject
{
["menuPath"] = "File/Quit"
});
var jo = ToJO(result);
Assert.IsFalse((bool)jo["success"], "Quit should be blocked");
StringAssert.Contains("blocked", jo["error"]?.ToString()?.ToLower() ?? "",
"Error should mention blocking");
}
[Test]
public void HandleCommand_ExecuteMenuItem_RequiresMenuPath()
{
// Current behavior: menu_path/menuPath parameter is required
var result = ExecuteMenuItem.HandleCommand(new JObject());
var jo = ToJO(result);
Assert.IsFalse((bool)jo["success"], "Should require menuPath");
}
#endregion
#region Section 9: Response Objects and Data Structures
[Test]
public void HandleCommand_ResponsesHaveConsistentShape()
{
// Current behavior: All responses have success field
var tools = new Func<JObject, object>[]
{
p => ManageEditor.HandleCommand(p),
p => FindGameObjects.HandleCommand(p),
p => ManagePrefabs.HandleCommand(p),
p => ManageMaterial.HandleCommand(p),
p => ExecuteMenuItem.HandleCommand(p)
};
foreach (var tool in tools)
{
var result = tool(new JObject { ["action"] = "ping" });
var jo = ToJO(result);
Assert.IsTrue(jo.ContainsKey("success"), "All responses should have success field");
}
}
[Test]
public void HandleCommand_SuccessResponse_HasMessageField()
{
// Current behavior: Success responses typically have message field
var result = ManageMaterial.HandleCommand(new JObject { ["action"] = "ping" });
var jo = ToJO(result);
if ((bool)jo["success"])
{
Assert.IsTrue(jo.ContainsKey("message") || jo.ContainsKey("data"),
"Success should have message or data");
}
}
[Test]
public void HandleCommand_ErrorResponse_HasErrorField()
{
// Current behavior: Error responses have error field with description
var result = ManageEditor.HandleCommand(new JObject { ["action"] = "invalid" });
var jo = ToJO(result);
if (!(bool)jo["success"])
{
Assert.IsTrue(jo.ContainsKey("error"), "Error response should have error field");
}
}
#endregion
#region Section 10: Tool Registration
[Test]
public void AllSampledTools_HaveMcpForUnityToolAttribute()
{
// Current behavior: Tools are registered via McpForUnityTool attribute
// This verifies the sampled tools have the attribute
var toolTypes = new[]
{
typeof(ManageEditor),
typeof(FindGameObjects),
typeof(ManagePrefabs),
typeof(ManageMaterial),
typeof(ExecuteMenuItem)
};
foreach (var type in toolTypes)
{
var attr = Attribute.GetCustomAttribute(type, typeof(McpForUnityToolAttribute));
Assert.IsNotNull(attr, $"{type.Name} should have McpForUnityTool attribute");
}
}
#endregion
#region Section 11: Tool-Specific Behaviors
[Test]
public void HandleCommand_ManageEditor_PlayPauseStopStateMachine()
{
// Current behavior: play/pause/stop form a state machine
// pause only works when playing
var pauseResult = ManageEditor.HandleCommand(new JObject { ["action"] = "pause" });
var jo = ToJO(pauseResult);
// Pause behavior depends on current play state
Assert.IsNotNull(jo, "Should return response");
}
[Test]
public void HandleCommand_ManageMaterial_ColorCoercion()
{
// Current behavior: Colors can be specified in multiple formats
var result = ManageMaterial.HandleCommand(new JObject
{
["action"] = "set_material_color",
["path"] = "NonExistent/Material",
["color"] = new JArray(1.0f, 0.5f, 0.5f, 1.0f)
});
// Even if material doesn't exist, the color parsing should not throw
var jo = ToJO(result);
Assert.IsNotNull(jo, "Should handle color array format");
}
[Test]
public void HandleCommand_FindGameObjects_EmptyResultsAreValid()
{
// Current behavior: Finding no objects is a valid success case
var result = FindGameObjects.HandleCommand(new JObject
{
["searchTerm"] = "DEFINITELY_NONEXISTENT_OBJECT_NAME_12345"
});
var jo = ToJO(result);
Assert.IsTrue((bool)jo["success"], "Empty results should still be success");
}
[Test]
public void HandleCommand_ManagePrefabs_GetInfo_RequiresPath()
{
// Current behavior: get_info needs path to prefab
var result = ManagePrefabs.HandleCommand(new JObject
{
["action"] = "get_info"
});
var jo = ToJO(result);
Assert.IsFalse((bool)jo["success"], "Should require path");
}
[Test]
public void HandleCommand_ManagePrefabs_CreateFromGameObject_RequiresTargetAndPath()
{
// Current behavior: create_from_gameobject needs both target and path
var result = ManagePrefabs.HandleCommand(new JObject
{
["action"] = "create_from_gameobject"
});
var jo = ToJO(result);
Assert.IsFalse((bool)jo["success"], "Should require target and path");
}
[Test]
[Explicit("Opens Console window - steals focus")]
public void HandleCommand_ExecuteMenuItem_ExecutesNonBlacklistedItems()
{
// Current behavior: Non-blacklisted items are executed
// NOTE: This test opens the Console window which steals focus from the terminal
var result = ExecuteMenuItem.HandleCommand(new JObject
{
["menuPath"] = "Window/General/Console"
});
var jo = ToJO(result);
// Should attempt execution (success depends on menu existence)
Assert.IsTrue((bool)jo["success"], "Non-blacklisted item should be attempted");
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9d4e2b3c5f6a7801234567890abcdef2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,56 @@
using System;
using NUnit.Framework;
using MCPForUnity.Editor.Tools;
namespace MCPForUnityTests.Editor.Tools
{
public class CommandRegistryTests
{
[OneTimeSetUp]
public void OneTimeSetUp()
{
// Ensure CommandRegistry is initialized before tests run
CommandRegistry.Initialize();
}
[Test]
public void GetHandler_ThrowsException_ForUnknownCommand()
{
var unknown = "nonexistent_command_that_should_not_exist";
Assert.Throws<InvalidOperationException>(() =>
{
CommandRegistry.GetHandler(unknown);
}, "Should throw InvalidOperationException for unknown handler");
}
[Test]
public void AutoDiscovery_RegistersAllBuiltInTools()
{
// Verify that all expected built-in tools are registered by trying to get their handlers
var expectedTools = new[]
{
"manage_asset",
"manage_editor",
"manage_gameobject",
"manage_scene",
"manage_script",
"manage_shader",
"read_console",
"execute_menu_item",
"manage_prefabs"
};
foreach (var toolName in expectedTools)
{
var handler = CommandRegistry.GetHandler(toolName);
Assert.IsNotNull(handler, $"Handler for '{toolName}' should not be null");
// Verify the handler is actually callable (returns a result, not throws)
var emptyParams = new Newtonsoft.Json.Linq.JObject();
var result = handler(emptyParams);
Assert.IsNotNull(result, $"Handler for '{toolName}' should return a result even for empty params");
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ed0df02ef7d99451f9f3b2bc3e3aba28
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,233 @@
using NUnit.Framework;
using UnityEngine;
using UnityEngine.Events;
using UnityEditor;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Tools;
using TestNamespace;
namespace MCPForUnityTests.Editor.Tools
{
public class ComponentOpsUnityEventTests
{
private GameObject testGo;
[SetUp]
public void SetUp()
{
testGo = new GameObject("UnityEventTestGO");
}
[TearDown]
public void TearDown()
{
if (testGo != null)
Object.DestroyImmediate(testGo);
}
[Test]
public void SetProperty_UnityEvent_SinglePersistentCall_PersistsViaSerialization()
{
var comp = testGo.AddComponent<UnityEventTestComponent>();
int targetId = testGo.GetInstanceID();
var value = JObject.Parse(@"{
""m_PersistentCalls"": {
""m_Calls"": [
{
""m_Target"": { ""instanceID"": " + targetId + @" },
""m_TargetAssemblyTypeName"": ""UnityEngine.GameObject, UnityEngine"",
""m_MethodName"": ""SetActive"",
""m_Mode"": 6,
""m_Arguments"": {
""m_BoolArgument"": true
},
""m_CallState"": 2
}
]
}
}");
bool ok = ComponentOps.SetProperty(comp, "onSimpleEvent", value, out string error);
Assert.IsTrue(ok, $"SetProperty should succeed, got error: {error}");
// Verify via SerializedObject readback
var so = new SerializedObject(comp);
var callsProp = so.FindProperty("onSimpleEvent.m_PersistentCalls.m_Calls");
Assert.IsNotNull(callsProp, "m_Calls property should exist");
Assert.AreEqual(1, callsProp.arraySize, "Should have 1 persistent call");
var call0 = callsProp.GetArrayElementAtIndex(0);
Assert.AreEqual("SetActive", call0.FindPropertyRelative("m_MethodName").stringValue);
Assert.AreEqual(testGo, call0.FindPropertyRelative("m_Target").objectReferenceValue);
Assert.AreEqual(6, call0.FindPropertyRelative("m_Mode").enumValueIndex);
Assert.AreEqual(2, call0.FindPropertyRelative("m_CallState").enumValueIndex);
}
[Test]
public void SetProperty_UnityEvent_MultiplePersistentCalls_AllPersist()
{
var comp = testGo.AddComponent<UnityEventTestComponent>();
int targetId = testGo.GetInstanceID();
var value = JObject.Parse(@"{
""m_PersistentCalls"": {
""m_Calls"": [
{
""m_Target"": { ""instanceID"": " + targetId + @" },
""m_TargetAssemblyTypeName"": ""UnityEngine.GameObject, UnityEngine"",
""m_MethodName"": ""SetActive"",
""m_Mode"": 6,
""m_Arguments"": { ""m_BoolArgument"": true },
""m_CallState"": 2
},
{
""m_Target"": { ""instanceID"": " + targetId + @" },
""m_TargetAssemblyTypeName"": ""UnityEngine.GameObject, UnityEngine"",
""m_MethodName"": ""SetActive"",
""m_Mode"": 6,
""m_Arguments"": { ""m_BoolArgument"": false },
""m_CallState"": 2
}
]
}
}");
bool ok = ComponentOps.SetProperty(comp, "onSimpleEvent", value, out string error);
Assert.IsTrue(ok, $"SetProperty should succeed, got error: {error}");
var so = new SerializedObject(comp);
var callsProp = so.FindProperty("onSimpleEvent.m_PersistentCalls.m_Calls");
Assert.AreEqual(2, callsProp.arraySize, "Should have 2 persistent calls");
Assert.AreEqual("SetActive", callsProp.GetArrayElementAtIndex(0).FindPropertyRelative("m_MethodName").stringValue);
Assert.AreEqual("SetActive", callsProp.GetArrayElementAtIndex(1).FindPropertyRelative("m_MethodName").stringValue);
}
[Test]
public void SetProperty_UnityEvent_EmptyCalls_ClearsEvent()
{
var comp = testGo.AddComponent<UnityEventTestComponent>();
// First set a call
int targetId = testGo.GetInstanceID();
var withCall = JObject.Parse(@"{
""m_PersistentCalls"": {
""m_Calls"": [
{
""m_Target"": { ""instanceID"": " + targetId + @" },
""m_TargetAssemblyTypeName"": ""UnityEngine.GameObject, UnityEngine"",
""m_MethodName"": ""SetActive"",
""m_Mode"": 6,
""m_Arguments"": { ""m_BoolArgument"": true },
""m_CallState"": 2
}
]
}
}");
ComponentOps.SetProperty(comp, "onSimpleEvent", withCall, out _);
// Now clear it
var empty = JObject.Parse(@"{
""m_PersistentCalls"": {
""m_Calls"": []
}
}");
bool ok = ComponentOps.SetProperty(comp, "onSimpleEvent", empty, out string error);
Assert.IsTrue(ok, $"SetProperty should succeed, got error: {error}");
var so = new SerializedObject(comp);
var callsProp = so.FindProperty("onSimpleEvent.m_PersistentCalls.m_Calls");
Assert.AreEqual(0, callsProp.arraySize, "Should have 0 persistent calls after clearing");
}
[Test]
public void SetProperty_PrivateSerializedUnityEvent_RoutesViaSerialization()
{
var comp = testGo.AddComponent<UnityEventTestComponent>();
int targetId = testGo.GetInstanceID();
var value = JObject.Parse(@"{
""m_PersistentCalls"": {
""m_Calls"": [
{
""m_Target"": { ""instanceID"": " + targetId + @" },
""m_TargetAssemblyTypeName"": ""UnityEngine.GameObject, UnityEngine"",
""m_MethodName"": ""SetActive"",
""m_Mode"": 6,
""m_Arguments"": { ""m_BoolArgument"": true },
""m_CallState"": 2
}
]
}
}");
bool ok = ComponentOps.SetProperty(comp, "_onPrivateEvent", value, out string error);
Assert.IsTrue(ok, $"SetProperty on private [SerializeField] UnityEvent should succeed, got error: {error}");
var so = new SerializedObject(comp);
var callsProp = so.FindProperty("_onPrivateEvent.m_PersistentCalls.m_Calls");
Assert.IsNotNull(callsProp, "Private event m_Calls should exist");
Assert.AreEqual(1, callsProp.arraySize, "Should have 1 persistent call");
}
[Test]
public void SetProperty_SimpleFloat_StillWorksViaReflection()
{
var audioSource = testGo.AddComponent<AudioSource>();
bool ok = ComponentOps.SetProperty(audioSource, "volume", new JValue(0.5f), out string error);
Assert.IsTrue(ok, $"SetProperty for float should succeed, got error: {error}");
Assert.AreEqual(0.5f, audioSource.volume, 0.001f);
}
[Test]
public void HandleCommand_EndToEnd_UnityEventWiring()
{
testGo.AddComponent<UnityEventTestComponent>();
int targetId = testGo.GetInstanceID();
var p = new JObject
{
["action"] = "set_property",
["target"] = testGo.name,
["search_method"] = "by_name",
["component_type"] = "UnityEventTestComponent",
["property"] = "onSimpleEvent",
["value"] = JObject.Parse(@"{
""m_PersistentCalls"": {
""m_Calls"": [
{
""m_Target"": { ""instanceID"": " + targetId + @" },
""m_TargetAssemblyTypeName"": ""UnityEngine.GameObject, UnityEngine"",
""m_MethodName"": ""SetActive"",
""m_Mode"": 6,
""m_Arguments"": { ""m_BoolArgument"": true },
""m_CallState"": 2
}
]
}
}")
};
var result = ManageComponents.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), $"HandleCommand should succeed: {resultObj}");
// Verify via SerializedObject
var comp = testGo.GetComponent<UnityEventTestComponent>();
var so = new SerializedObject(comp);
var callsProp = so.FindProperty("onSimpleEvent.m_PersistentCalls.m_Calls");
Assert.AreEqual(1, callsProp.arraySize, "Should have 1 persistent call after end-to-end");
Assert.AreEqual("SetActive", callsProp.GetArrayElementAtIndex(0).FindPropertyRelative("m_MethodName").stringValue);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: aaea4d5f23d824f45a6f48cf7ca57407
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,145 @@
using System;
using NUnit.Framework;
using UnityEngine;
using MCPForUnity.Editor.Tools;
namespace MCPForUnityTests.Editor.Tools
{
public class ComponentResolverTests
{
[Test]
public void TryResolve_ReturnsTrue_ForBuiltInComponentShortName()
{
bool result = ComponentResolver.TryResolve("Transform", out Type type, out string error);
Assert.IsTrue(result, "Should resolve Transform component");
Assert.AreEqual(typeof(Transform), type, "Should return correct Transform type");
Assert.IsEmpty(error, "Should have no error message");
}
[Test]
public void TryResolve_ReturnsTrue_ForBuiltInComponentFullyQualifiedName()
{
bool result = ComponentResolver.TryResolve("UnityEngine.Rigidbody", out Type type, out string error);
Assert.IsTrue(result, "Should resolve UnityEngine.Rigidbody component");
Assert.AreEqual(typeof(Rigidbody), type, "Should return correct Rigidbody type");
Assert.IsEmpty(error, "Should have no error message");
}
[Test]
public void TryResolve_ReturnsTrue_ForCustomComponentShortName()
{
bool result = ComponentResolver.TryResolve("CustomComponent", out Type type, out string error);
Assert.IsTrue(result, "Should resolve CustomComponent");
Assert.IsNotNull(type, "Should return valid type");
Assert.AreEqual("CustomComponent", type.Name, "Should have correct type name");
Assert.IsTrue(typeof(Component).IsAssignableFrom(type), "Should be a Component type");
Assert.IsEmpty(error, "Should have no error message");
}
[Test]
public void TryResolve_ReturnsTrue_ForCustomComponentFullyQualifiedName()
{
bool result = ComponentResolver.TryResolve("TestNamespace.CustomComponent", out Type type, out string error);
Assert.IsTrue(result, "Should resolve TestNamespace.CustomComponent");
Assert.IsNotNull(type, "Should return valid type");
Assert.AreEqual("CustomComponent", type.Name, "Should have correct type name");
Assert.AreEqual("TestNamespace.CustomComponent", type.FullName, "Should have correct full name");
Assert.IsTrue(typeof(Component).IsAssignableFrom(type), "Should be a Component type");
Assert.IsEmpty(error, "Should have no error message");
}
[Test]
public void TryResolve_ReturnsFalse_ForNonExistentComponent()
{
bool result = ComponentResolver.TryResolve("NonExistentComponent", out Type type, out string error);
Assert.IsFalse(result, "Should not resolve non-existent component");
Assert.IsNull(type, "Should return null type");
Assert.IsNotEmpty(error, "Should have error message");
Assert.That(error, Does.Contain("not found"), "Error should mention component not found");
}
[Test]
public void TryResolve_ReturnsFalse_ForEmptyString()
{
bool result = ComponentResolver.TryResolve("", out Type type, out string error);
Assert.IsFalse(result, "Should not resolve empty string");
Assert.IsNull(type, "Should return null type");
Assert.IsNotEmpty(error, "Should have error message");
}
[Test]
public void TryResolve_ReturnsFalse_ForNullString()
{
bool result = ComponentResolver.TryResolve(null, out Type type, out string error);
Assert.IsFalse(result, "Should not resolve null string");
Assert.IsNull(type, "Should return null type");
Assert.IsNotEmpty(error, "Should have error message");
Assert.That(error, Does.Contain("null or empty"), "Error should mention null or empty");
}
[Test]
public void TryResolve_CachesResolvedTypes()
{
// First call
bool result1 = ComponentResolver.TryResolve("Transform", out Type type1, out string error1);
// Second call should use cache
bool result2 = ComponentResolver.TryResolve("Transform", out Type type2, out string error2);
Assert.IsTrue(result1, "First call should succeed");
Assert.IsTrue(result2, "Second call should succeed");
Assert.AreSame(type1, type2, "Should return same type instance (cached)");
Assert.IsEmpty(error1, "First call should have no error");
Assert.IsEmpty(error2, "Second call should have no error");
}
[Test]
public void TryResolve_PrefersPlayerAssemblies()
{
// Test that custom user scripts (in Player assemblies) are found
bool result = ComponentResolver.TryResolve("CustomComponent", out Type type, out string error);
Assert.IsTrue(result, "Should resolve user script from Player assembly");
Assert.IsNotNull(type, "Should return valid type");
// Verify it's not from an Editor assembly by checking the assembly name
string assemblyName = type.Assembly.GetName().Name;
Assert.That(assemblyName, Does.Not.Contain("Editor"),
"User script should come from Player assembly, not Editor assembly");
// Verify it's from the TestAsmdef assembly (which is a Player assembly)
Assert.AreEqual("TestAsmdef", assemblyName,
"CustomComponent should be resolved from TestAsmdef assembly");
}
[Test]
public void TryResolve_HandlesDuplicateNames_WithAmbiguityError()
{
// This test would need duplicate component names to be meaningful
// For now, test with a built-in component that should not have duplicates
bool result = ComponentResolver.TryResolve("Transform", out Type type, out string error);
Assert.IsTrue(result, "Transform should resolve uniquely");
Assert.AreEqual(typeof(Transform), type, "Should return correct type");
Assert.IsEmpty(error, "Should have no ambiguity error");
}
[Test]
public void ResolvedType_IsValidComponent()
{
bool result = ComponentResolver.TryResolve("Rigidbody", out Type type, out string error);
Assert.IsTrue(result, "Should resolve Rigidbody");
Assert.IsTrue(typeof(Component).IsAssignableFrom(type), "Resolved type should be assignable from Component");
Assert.IsTrue(typeof(MonoBehaviour).IsAssignableFrom(type) ||
typeof(Component).IsAssignableFrom(type), "Should be a valid Unity component");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c15ba6502927e4901a43826c43debd7c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,289 @@
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using UnityEditor;
using System.Collections;
using System.IO;
using MCPForUnity.Editor.Tools;
using Newtonsoft.Json.Linq;
namespace MCPForUnityTests.Editor.Tools
{
/// <summary>
/// Tests for domain reload resilience - ensuring MCP requests succeed even during Unity domain reloads.
///
/// These tests trigger script compilation which can cause test timing issues when Unity is
/// backgrounded, but the MCP workflow itself is unaffected - socket messages provide external
/// stimulus that keeps Unity responsive.
///
/// Note: Focus nudge improvements (P2-9) should help with background test reliability.
/// </summary>
[Category("domain_reload")]
[Explicit("Domain reload stress tests; run manually when needed.")]
public class DomainReloadResilienceTests
{
private const string TempDir = "Assets/Temp/DomainReloadTests";
[SetUp]
public void Setup()
{
// Ensure temp directory exists
if (!AssetDatabase.IsValidFolder(TempDir))
{
Directory.CreateDirectory(TempDir);
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
}
}
[TearDown]
public void TearDown()
{
// Clean up temp directory - this deletes any scripts we created
if (AssetDatabase.IsValidFolder(TempDir))
{
AssetDatabase.DeleteAsset(TempDir);
}
// Remove parent temp folder if nothing else is inside
if (AssetDatabase.IsValidFolder("Assets/Temp"))
{
var remainingDirs = Directory.GetDirectories("Assets/Temp");
var remainingFiles = Directory.GetFiles("Assets/Temp");
if (remainingDirs.Length == 0 && remainingFiles.Length == 0)
{
AssetDatabase.DeleteAsset("Assets/Temp");
}
}
// CRITICAL: Force a synchronous refresh and wait for any pending compilation to finish.
// This prevents leaving compilation running that could stall subsequent tests.
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
}
/// <summary>
/// This test simulates the stress test scenario:
/// 1. Create a script (triggers domain reload)
/// 2. Make multiple rapid read_console calls
/// 3. Verify all calls succeed (no "No Unity plugins are currently connected" errors)
///
/// Note: This test uses UnityTest coroutine to handle the async nature of domain reloads.
/// </summary>
[UnityTest]
public IEnumerator StressTest_CreateScriptAndReadConsoleMultipleTimes()
{
// Step 1: Create a script to trigger domain reload
var scriptPath = Path.Combine(TempDir, "StressTestScript.cs").Replace("\\", "/");
var scriptContent = @"using UnityEngine;
public class StressTestScript : MonoBehaviour
{
void Start() { }
}";
// Write script file
File.WriteAllText(scriptPath, scriptContent);
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
Debug.Log("[DomainReloadTest] Script created, domain reload triggered");
// Wait a frame for the domain reload to start
yield return null;
// Step 2: Make multiple rapid read_console calls
// These should succeed even during the reload window
int successCount = 0;
int totalCalls = 5;
for (int i = 0; i < totalCalls; i++)
{
var request = new JObject
{
["action"] = "get",
["types"] = new JArray { "all" },
["count"] = 50,
["format"] = "plain",
["includeStacktrace"] = false
};
var result = ExecuteReadConsole(request);
// Check if the call succeeded
if (result != null && result["success"]?.Value<bool>() == true)
{
successCount++;
Debug.Log($"[DomainReloadTest] read_console call {i+1}/{totalCalls} succeeded");
}
else
{
var error = result?["error"]?.ToString() ?? "Unknown error";
Debug.LogError($"[DomainReloadTest] read_console call {i+1}/{totalCalls} failed: {error}");
}
// Small delay between calls to simulate rapid-fire scenario
yield return WaitFrames(6);
}
// Step 3: Verify all calls succeeded
Debug.Log($"[DomainReloadTest] {successCount}/{totalCalls} read_console calls succeeded");
Assert.AreEqual(totalCalls, successCount,
$"Expected all {totalCalls} read_console calls to succeed during domain reload, but only {successCount} succeeded");
}
/// <summary>
/// Test that read_console works reliably after a domain reload completes.
/// </summary>
[Test]
public void ReadConsole_AfterDomainReload_Succeeds()
{
// This test assumes domain reload has already completed
// (Unity tests run after domain reload)
var request = new JObject
{
["action"] = "get",
["types"] = new JArray { "error", "warning", "log" },
["count"] = 10,
["format"] = "plain"
};
var result = ExecuteReadConsole(request);
Assert.IsNotNull(result, "read_console should return a result");
Assert.IsTrue(result["success"]?.Value<bool>() ?? false,
$"read_console should succeed: {result["error"]}");
}
/// <summary>
/// Test creating a script and immediately querying console logs.
/// This simulates a common AI workflow pattern.
/// </summary>
[UnityTest]
public IEnumerator CreateScript_ThenQueryConsole_Succeeds()
{
// Create a simple script
var scriptPath = Path.Combine(TempDir, "TestScript1.cs").Replace("\\", "/");
var scriptContent = @"using UnityEngine;
public class TestScript1 : MonoBehaviour
{
void Start()
{
Debug.Log(""TestScript1 initialized"");
}
}";
File.WriteAllText(scriptPath, scriptContent);
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
Debug.Log("[DomainReloadTest] Script created");
// Wait a frame
yield return null;
// Immediately try to read console
var request = new JObject
{
["action"] = "get",
["types"] = new JArray { "all" },
["count"] = 50,
["format"] = "plain"
};
var result = ExecuteReadConsole(request);
// Should succeed even if domain reload is happening
Assert.IsNotNull(result, "read_console should return a result");
Assert.IsTrue(result["success"]?.Value<bool>() ?? false,
$"read_console should succeed even during/after script creation: {result["error"]}");
}
/// <summary>
/// Test rapid script creation followed by console reads.
/// This is an even more aggressive stress test.
/// </summary>
[UnityTest]
public IEnumerator RapidScriptCreation_WithConsoleReads_AllSucceed()
{
int scriptCount = 3;
int consoleReadsPerScript = 2;
int successCount = 0;
int totalExpectedReads = scriptCount * consoleReadsPerScript;
for (int i = 0; i < scriptCount; i++)
{
// Create script
var scriptPath = Path.Combine(TempDir, $"RapidScript{i}.cs").Replace("\\", "/");
var scriptContent = $@"using UnityEngine;
public class RapidScript{i} : MonoBehaviour
{{
void Start() {{ Debug.Log(""RapidScript{i}""); }}
}}";
File.WriteAllText(scriptPath, scriptContent);
AssetDatabase.Refresh(ImportAssetOptions.ForceSynchronousImport);
Debug.Log($"[DomainReloadTest] Created script {i+1}/{scriptCount}");
// Immediately try console reads
for (int j = 0; j < consoleReadsPerScript; j++)
{
var request = new JObject
{
["action"] = "get",
["types"] = new JArray { "all" },
["count"] = 20,
["format"] = "plain"
};
var result = ExecuteReadConsole(request);
if (result != null && result["success"]?.Value<bool>() == true)
{
successCount++;
}
else
{
var error = result?["error"]?.ToString() ?? "Unknown error";
Debug.LogError($"[DomainReloadTest] Console read failed: {error}");
}
yield return WaitFrames(3);
}
// Brief wait between script creations
yield return WaitFrames(12);
}
Debug.Log($"[DomainReloadTest] {successCount}/{totalExpectedReads} console reads succeeded");
// We expect at least 80% success rate (some may fail due to timing, but resilience should help most)
int minExpectedSuccess = (int)(totalExpectedReads * 0.8f);
Assert.GreaterOrEqual(successCount, minExpectedSuccess,
$"Expected at least {minExpectedSuccess} console reads to succeed, but only {successCount} succeeded");
}
private static JObject ExecuteReadConsole(JObject request)
{
var raw = ReadConsole.HandleCommand(request);
if (raw == null)
{
return new JObject
{
["success"] = false,
["error"] = "ReadConsole returned null"
};
}
return JObject.FromObject(raw);
}
private static IEnumerator WaitFrames(int frameCount)
{
for (int i = 0; i < frameCount; i++)
{
yield return null;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a3d4e0a7c0814f75a3cf7d1eee7bcdd1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,382 @@
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using MCPForUnity.Editor.Tools;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
public class ExecuteCodeTests
{
[SetUp]
public void SetUp()
{
ExecuteCode.HandleCommand(new JObject { ["action"] = "clear_history" });
}
// ──────────────────── Execute: success cases ────────────────────
[Test]
public void Execute_ReturnString_ReturnsSuccess()
{
var result = Execute("return \"hello\";");
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.AreEqual("hello", result["data"]["result"].Value<string>());
}
[Test]
public void Execute_ReturnInt_ReturnsSuccess()
{
var result = Execute("return 42;");
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.AreEqual(42, result["data"]["result"].Value<int>());
}
[Test]
public void Execute_ReturnNull_NoResultValue()
{
var result = Execute("int x = 1; return null;");
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
// data may contain compiler info but should not have a "result" key
var data = result["data"] as JObject;
if (data != null)
Assert.IsNull(data["result"], "Expected no 'result' key when code returns null");
}
[Test]
public void Execute_VoidReturn_Succeeds()
{
var result = Execute("UnityEngine.Debug.Log(\"test\"); return null;");
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
}
[Test]
public void Execute_UnityAPI_CanAccessSceneManager()
{
var result = Execute(
"var scene = UnityEngine.SceneManagement.SceneManager.GetActiveScene();\n" +
"return scene.name;");
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.IsNotNull(result["data"]["result"]);
}
[Test]
public void Execute_Generics_ListOfString()
{
var result = Execute(
"var list = new System.Collections.Generic.List<string>();\n" +
"list.Add(\"a\"); list.Add(\"b\");\n" +
"return list;");
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var arr = result["data"]["result"] as JArray;
Assert.IsNotNull(arr, "Expected array result");
Assert.AreEqual(2, arr.Count);
}
[Test]
public void Execute_LINQ_SelectWorks()
{
var result = Execute(
"var nums = new int[] { 1, 2, 3 };\n" +
"return nums.Select(n => n * 2).ToList();");
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var arr = result["data"]["result"] as JArray;
Assert.IsNotNull(arr);
Assert.AreEqual(3, arr.Count);
Assert.AreEqual(2, arr[0].Value<int>());
Assert.AreEqual(6, arr[2].Value<int>());
}
[Test]
public void Execute_Dictionary_ReturnsStructured()
{
var result = Execute(
"var dict = new Dictionary<string, int> { { \"a\", 1 }, { \"b\", 2 } };\n" +
"return dict;");
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.IsNotNull(result["data"]["result"]);
}
// ──────────────────── Execute: error cases ────────────────────
[Test]
public void Execute_CompilationError_ReturnsErrors()
{
var result = Execute("int x = \"not an int\";");
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
StringAssert.Contains("Compilation failed", result.Value<string>("error"));
Assert.IsNotNull(result["data"]["errors"]);
}
[Test]
public void Execute_RuntimeException_ReturnsError()
{
var result = Execute("throw new System.Exception(\"boom\");");
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
StringAssert.Contains("boom", result.Value<string>("error"));
}
[Test]
public void Execute_MissingCode_ReturnsError()
{
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
{
["action"] = "execute"
}));
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
StringAssert.Contains("code", result.Value<string>("error").ToLowerInvariant());
}
[Test]
public void Execute_EmptyCode_ReturnsError()
{
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
{
["action"] = "execute",
["code"] = " "
}));
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
}
// ──────────────────── Safety checks ────────────────────
[Test]
public void Execute_SafetyChecks_BlocksFileDelete()
{
var result = Execute("System.IO.File.Delete(\"x\");");
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
StringAssert.Contains("Blocked pattern", result.Value<string>("error"));
}
[Test]
public void Execute_SafetyChecks_BlocksProcessStart()
{
var result = Execute("Process.Start(\"cmd\");");
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
StringAssert.Contains("Blocked pattern", result.Value<string>("error"));
}
[Test]
public void Execute_SafetyChecks_BlocksInfiniteLoop()
{
var result = Execute("while (true) { }");
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
StringAssert.Contains("Blocked pattern", result.Value<string>("error"));
}
[Test]
public void Execute_SafetyChecksDisabled_AllowsBlockedPattern()
{
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
{
["action"] = "execute",
["code"] = "while (true) { break; } return null;",
["safety_checks"] = false
}));
if (!result.Value<bool>("success"))
{
var error = result.Value<string>("error") ?? "";
Assert.IsFalse(error.Contains("Blocked pattern"),
"Safety checks should be disabled but still blocked");
}
}
// ──────────────────── History ────────────────────
[Test]
public void GetHistory_Empty_ReturnsZero()
{
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
{
["action"] = "get_history"
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.AreEqual(0, result["data"]["total"].Value<int>());
}
[Test]
public void GetHistory_AfterExecution_RecordsEntry()
{
Execute("return 1;");
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
{
["action"] = "get_history"
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.AreEqual(1, result["data"]["total"].Value<int>());
var entries = result["data"]["entries"] as JArray;
Assert.IsNotNull(entries);
Assert.AreEqual(1, entries.Count);
Assert.IsTrue(entries[0]["success"].Value<bool>());
}
[Test]
public void GetHistory_Limit_RespectsParameter()
{
Execute("return 1;");
Execute("return 2;");
Execute("return 3;");
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
{
["action"] = "get_history",
["limit"] = 2
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.AreEqual(3, result["data"]["total"].Value<int>());
var entries = result["data"]["entries"] as JArray;
Assert.AreEqual(2, entries.Count);
}
[Test]
public void ClearHistory_RemovesAll()
{
Execute("return 1;");
Execute("return 2;");
var clearResult = ToJObject(ExecuteCode.HandleCommand(new JObject
{
["action"] = "clear_history"
}));
Assert.IsTrue(clearResult.Value<bool>("success"), clearResult.ToString());
var historyResult = ToJObject(ExecuteCode.HandleCommand(new JObject
{
["action"] = "get_history"
}));
Assert.AreEqual(0, historyResult["data"]["total"].Value<int>());
}
// ──────────────────── Replay ────────────────────
[Test]
public void Replay_ValidIndex_ReExecutes()
{
Execute("return 42;");
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
{
["action"] = "replay",
["index"] = 0
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.AreEqual(42, result["data"]["result"].Value<int>());
}
[Test]
public void Replay_InvalidIndex_ReturnsError()
{
Execute("return 1;");
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
{
["action"] = "replay",
["index"] = 99
}));
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
StringAssert.Contains("Invalid history index", result.Value<string>("error"));
}
[Test]
public void Replay_EmptyHistory_ReturnsError()
{
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
{
["action"] = "replay",
["index"] = 0
}));
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
}
// ──────────────────── Action validation ────────────────────
[Test]
public void UnknownAction_ReturnsError()
{
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
{
["action"] = "invalid_action"
}));
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
StringAssert.Contains("Unknown action", result.Value<string>("error"));
}
[Test]
public void NullParams_ReturnsError()
{
var result = ToJObject(ExecuteCode.HandleCommand(null));
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
}
// ──────────────────── CodeDom backend ────────────────────
// Regression for CoplayDev/unity-mcp#1144: large projects (~100+ asmdefs) blew past the
// Windows 32 KB CreateProcess limit because every reference became an inline /r: flag.
// The fix routes references through a @responsefile, so this just verifies that the
// codedom path still compiles and runs end-to-end.
[Test]
public void Execute_CodedomBackend_CompilesAndRuns()
{
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
{
["action"] = "execute",
["code"] = "return 1 + 1;",
["compiler"] = "codedom"
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.AreEqual(2, result["data"]["result"].Value<int>());
Assert.AreEqual("codedom", result["data"]["compiler"].Value<string>());
}
[Test]
public void Execute_CodedomBackend_ResolvesUnityTypes()
{
var result = ToJObject(ExecuteCode.HandleCommand(new JObject
{
["action"] = "execute",
["code"] = "return UnityEngine.Application.unityVersion;",
["compiler"] = "codedom"
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.IsNotNull(result["data"]["result"]);
}
// ──────────────────── Helpers ────────────────────
private static JObject Execute(string code)
{
return ToJObject(ExecuteCode.HandleCommand(new JObject
{
["action"] = "execute",
["code"] = code
}));
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5ada2677bb6004017b54f9dea47e403a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,39 @@
using NUnit.Framework;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Tools;
namespace MCPForUnityTests.Editor.Tools
{
public class ExecuteMenuItemTests
{
private static JObject ToJO(object o) => JObject.FromObject(o);
[Test]
public void Execute_MissingParam_ReturnsError()
{
var res = ExecuteMenuItem.HandleCommand(new JObject());
var jo = ToJO(res);
Assert.IsFalse((bool)jo["success"], "Expected success false");
StringAssert.Contains("Required parameter", (string)jo["error"]);
}
[Test]
public void Execute_Blacklisted_ReturnsError()
{
var res = ExecuteMenuItem.HandleCommand(new JObject { ["menuPath"] = "File/Quit" });
var jo = ToJO(res);
Assert.IsFalse((bool)jo["success"], "Expected success false for blacklisted menu");
StringAssert.Contains("blocked for safety", (string)jo["error"], "Expected blacklist message");
}
[Test]
public void Execute_NonBlacklisted_ReturnsImmediateSuccess()
{
// We don't rely on the menu actually existing; execution is delayed and we only check the immediate response shape
var res = ExecuteMenuItem.HandleCommand(new JObject { ["menuPath"] = "File/Save Project" });
var jo = ToJO(res);
Assert.IsTrue((bool)jo["success"], "Expected immediate success response");
StringAssert.Contains("Attempted to execute menu item", (string)jo["message"], "Expected attempt message");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ae694b6ac48824768a319eb378e7fb63
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: fbad1c3ddb00a48918a2b59a2a1714cb
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace MCPForUnityTests.Editor.Tools.Fixtures
{
[Serializable]
public struct ManageScriptableObjectNestedData
{
public string note;
}
// NOTE: File name matches class name so Unity can resolve a MonoScript asset for this ScriptableObject type.
public class ManageScriptableObjectTestDefinition : ManageScriptableObjectTestDefinitionBase
{
[SerializeField] private string displayName;
[SerializeField] private List<Material> materials = new();
[SerializeField] private ManageScriptableObjectNestedData nested;
public string DisplayName => displayName;
public IReadOnlyList<Material> Materials => materials;
public string NestedNote => nested.note;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ba28697c0d65145a1ad753ef73c53185
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,14 @@
using UnityEngine;
namespace MCPForUnityTests.Editor.Tools.Fixtures
{
// NOTE: File name matches class name so Unity can resolve a MonoScript asset for this ScriptableObject type.
public class ManageScriptableObjectTestDefinitionBase : ScriptableObject
{
[SerializeField] private int baseNumber = 1;
public int BaseNumber => baseNumber;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a757068d676ee47dba1045bfb8b8fb12
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e8f17bd366ad941fc95a0b60a727a90d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,7 @@
using UnityEngine;
[CreateAssetMenu(fileName = "ArrayStressSO", menuName = "StressTests/ArrayStressSO")]
public class ArrayStressSO : ScriptableObject
{
public float[] floatArray = new float[3];
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9eec250e4deee48c69c12acfde8c2adc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,52 @@
using UnityEngine;
using System.Collections.Generic;
[System.Serializable]
public struct NestedData
{
public string id;
public float value;
public Vector3 position;
}
[System.Serializable]
public class ComplexSubClass
{
public string name;
public int level;
public List<float> scores;
}
public enum TestEnum
{
Alpha,
Beta,
Gamma
}
[CreateAssetMenu(fileName = "ComplexStressSO", menuName = "StressTests/ComplexStressSO")]
public class ComplexStressSO : ScriptableObject
{
[Header("Basic Types")]
public int intValue;
public float floatValue;
public string stringValue;
public bool boolValue;
public Vector3 vectorValue;
public Color colorValue;
public TestEnum enumValue;
[Header("Arrays & Lists")]
public int[] intArray;
public List<string> stringList;
public Vector3[] vectorArray;
[Header("Complex Types")]
public NestedData nestedStruct;
public ComplexSubClass nestedClass;
public List<NestedData> nestedDataList;
[Header("Extended Types (Phase 6)")]
public AnimationCurve animCurve;
public Quaternion rotation;
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a73b1ff3fe1fa4b3fadb70c7c257d5a8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,30 @@
using UnityEngine;
using System.Collections.Generic;
[System.Serializable]
public class Level3
{
public string detail;
public Vector3 pos;
}
[System.Serializable]
public class Level2
{
public string midName;
public Level3 deep;
}
[System.Serializable]
public class Level1
{
public string topName;
public Level2 mid;
}
[CreateAssetMenu(fileName = "DeepStressSO", menuName = "StressTests/DeepStressSO")]
public class DeepStressSO : ScriptableObject
{
public Level1 level1;
public Color overtone;
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cc8fe16aef3ae4cbc949300f5fed2187
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,68 @@
using System.Collections.Generic;
using MCPForUnity.Editor.Helpers;
using NUnit.Framework;
using UnityEngine;
namespace Fusion
{
public struct NetworkBehaviourBuffer
{
public int Value;
}
public struct Changed<T>
{
public T Value;
}
}
namespace MCPForUnityTests.Editor.Tools
{
public class FusionUnsafeTypeSerializationTests
{
[Test]
public void GetComponentData_SkipsFusionUnsafeTypesInsideContainers()
{
var testObject = new GameObject("FusionUnsafeTypeTestObject");
try
{
var component = testObject.AddComponent<FusionUnsafeTypeComponent>();
component.bufferList.Add(new Fusion.NetworkBehaviourBuffer { Value = 1 });
component.nestedChangedLookup["changed"] = new List<Fusion.Changed<int>>
{
new Fusion.Changed<int> { Value = 2 }
};
component.ChangedListProperty.Add(new Fusion.Changed<int> { Value = 3 });
var result = GameObjectSerializer.GetComponentData(component) as Dictionary<string, object>;
Assert.IsNotNull(result, "GetComponentData should return dictionary data.");
Assert.IsTrue(result.TryGetValue("properties", out object propertiesObject), "Serialized data should contain properties.");
var properties = propertiesObject as Dictionary<string, object>;
Assert.IsNotNull(properties, "Serialized properties should be a dictionary.");
Assert.IsTrue(properties.ContainsKey(nameof(FusionUnsafeTypeComponent.safeValue)), "Safe fields should still serialize.");
Assert.IsFalse(properties.ContainsKey(nameof(FusionUnsafeTypeComponent.directBuffer)), "Direct Fusion buffer fields should be skipped.");
Assert.IsFalse(properties.ContainsKey(nameof(FusionUnsafeTypeComponent.bufferList)), "Collections containing Fusion buffer types should be skipped.");
Assert.IsFalse(properties.ContainsKey(nameof(FusionUnsafeTypeComponent.nestedChangedLookup)), "Nested generic containers containing Fusion Changed<T> should be skipped.");
Assert.IsFalse(properties.ContainsKey(nameof(FusionUnsafeTypeComponent.ChangedListProperty)), "Properties returning collections of Fusion Changed<T> should be skipped.");
}
finally
{
UnityEngine.Object.DestroyImmediate(testObject);
}
}
}
public sealed class FusionUnsafeTypeComponent : MonoBehaviour
{
public string safeValue = "kept";
public Fusion.NetworkBehaviourBuffer directBuffer;
public List<Fusion.NetworkBehaviourBuffer> bufferList = new List<Fusion.NetworkBehaviourBuffer>();
public Dictionary<string, List<Fusion.Changed<int>>> nestedChangedLookup = new Dictionary<string, List<Fusion.Changed<int>>>();
public List<Fusion.Changed<int>> ChangedListProperty { get; } = new List<Fusion.Changed<int>>();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0a2e39a143bd4d46a03d27cc30468d2e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,570 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Resources.Scene;
using MCPForUnity.Editor.Tools;
using MCPForUnity.Editor.Tools.GameObjects;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using UnityEngine.TestTools;
using static MCPForUnityTests.Editor.TestUtilities;
using Debug = UnityEngine.Debug;
namespace MCPForUnityTests.Editor.Tools
{
/// <summary>
/// Stress tests for the GameObject API redesign.
/// Tests volume operations, pagination, and performance with large datasets.
/// </summary>
[TestFixture]
public class GameObjectAPIStressTests
{
private List<GameObject> _createdObjects = new List<GameObject>();
private const int SMALL_BATCH = 10;
private const int MEDIUM_BATCH = 50;
private const int LARGE_BATCH = 100;
[SetUp]
public void SetUp()
{
_createdObjects.Clear();
}
[TearDown]
public void TearDown()
{
foreach (var go in _createdObjects)
{
if (go != null)
{
UnityEngine.Object.DestroyImmediate(go);
}
}
_createdObjects.Clear();
}
private GameObject CreateTestObject(string name)
{
var go = new GameObject(name);
_createdObjects.Add(go);
return go;
}
#region Bulk GameObject Creation
[Test]
public void BulkCreate_SmallBatch_AllSucceed()
{
var sw = Stopwatch.StartNew();
for (int i = 0; i < SMALL_BATCH; i++)
{
var result = ToJObject(ManageGameObject.HandleCommand(new JObject
{
["action"] = "create",
["name"] = $"BulkTest_{i}"
}));
Assert.IsTrue(result["success"]?.Value<bool>() ?? false, $"Failed to create object {i}");
// Track for cleanup
int instanceId = result["data"]?["instanceID"]?.Value<int>() ?? 0;
if (instanceId != 0)
{
var go = EditorUtility.InstanceIDToObject(instanceId) as GameObject;
if (go != null) _createdObjects.Add(go);
}
}
sw.Stop();
Debug.Log($"[BulkCreate] Created {SMALL_BATCH} objects in {sw.ElapsedMilliseconds}ms");
// Use generous threshold for CI variability
Assert.Less(sw.ElapsedMilliseconds, 10000, "Bulk create took too long (CI threshold)");
}
[Test]
public void BulkCreate_MediumBatch_AllSucceed()
{
var sw = Stopwatch.StartNew();
for (int i = 0; i < MEDIUM_BATCH; i++)
{
var result = ToJObject(ManageGameObject.HandleCommand(new JObject
{
["action"] = "create",
["name"] = $"MediumBulk_{i}"
}));
Assert.IsTrue(result["success"]?.Value<bool>() ?? false, $"Failed to create object {i}");
int instanceId = result["data"]?["instanceID"]?.Value<int>() ?? 0;
if (instanceId != 0)
{
var go = EditorUtility.InstanceIDToObject(instanceId) as GameObject;
if (go != null) _createdObjects.Add(go);
}
}
sw.Stop();
Debug.Log($"[BulkCreate] Created {MEDIUM_BATCH} objects in {sw.ElapsedMilliseconds}ms");
Assert.Less(sw.ElapsedMilliseconds, 15000, "Medium batch create took too long");
}
#endregion
#region Find GameObjects Pagination
[Test]
public void FindGameObjects_LargeBatch_PaginatesCorrectly()
{
// Create many objects with a unique marker component for reliable search
for (int i = 0; i < LARGE_BATCH; i++)
{
var go = CreateTestObject($"Searchable_{i:D3}");
go.AddComponent<GameObjectAPIStressTestMarker>();
}
// Find by searching for a specific object first
var firstResult = ToJObject(FindGameObjects.HandleCommand(new JObject
{
["searchTerm"] = "Searchable_000",
["searchMethod"] = "by_name",
["pageSize"] = 10
}));
Assert.IsTrue(firstResult["success"]?.Value<bool>() ?? false, "Should find specific named object");
var firstData = firstResult["data"] as JObject;
var firstIds = firstData?["instanceIDs"] as JArray;
Assert.IsNotNull(firstIds);
Assert.AreEqual(1, firstIds.Count, "Should find exactly one object with exact name match");
Debug.Log($"[FindGameObjects] Found object by exact name. Testing pagination with a unique marker component.");
// Now test pagination by searching for only the objects created by this test
var result = ToJObject(FindGameObjects.HandleCommand(new JObject
{
["searchTerm"] = typeof(GameObjectAPIStressTestMarker).FullName,
["searchMethod"] = "by_component",
["pageSize"] = 25
}));
Assert.IsTrue(result["success"]?.Value<bool>() ?? false);
var data = result["data"] as JObject;
Assert.IsNotNull(data);
var instanceIds = data["instanceIDs"] as JArray;
Assert.IsNotNull(instanceIds);
Assert.AreEqual(25, instanceIds.Count, "First page should have 25 items");
int totalCount = data["totalCount"]?.Value<int>() ?? 0;
Assert.AreEqual(LARGE_BATCH, totalCount, $"Should find exactly {LARGE_BATCH} objects created by this test");
bool hasMore = data["hasMore"]?.Value<bool>() ?? false;
Assert.IsTrue(hasMore, "Should have more pages");
Debug.Log($"[FindGameObjects] Found {totalCount} objects, first page has {instanceIds.Count}");
}
[Test]
public void FindGameObjects_PaginateThroughAll()
{
// Create objects - all will have a unique marker component
for (int i = 0; i < MEDIUM_BATCH; i++)
{
var go = CreateTestObject($"Paginate_{i:D3}");
go.AddComponent<GameObjectAPIStressTestMarker>();
}
// Track IDs we've created for verification
var createdIds = new HashSet<int>();
foreach (var go in _createdObjects)
{
if (go != null && go.name.StartsWith("Paginate_"))
{
createdIds.Add(go.GetInstanceID());
}
}
int pageSize = 10;
int cursor = 0;
int foundFromCreated = 0;
int pageCount = 0;
// Search by the unique marker component and check our created objects
while (true)
{
var result = ToJObject(FindGameObjects.HandleCommand(new JObject
{
["searchTerm"] = typeof(GameObjectAPIStressTestMarker).FullName,
["searchMethod"] = "by_component",
["pageSize"] = pageSize,
["cursor"] = cursor
}));
Assert.IsTrue(result["success"]?.Value<bool>() ?? false);
var data = result["data"] as JObject;
var instanceIds = data["instanceIDs"] as JArray;
// Count how many of our created objects are in this page
foreach (var id in instanceIds)
{
if (createdIds.Contains(id.Value<int>()))
{
foundFromCreated++;
}
}
pageCount++;
bool hasMore = data["hasMore"]?.Value<bool>() ?? false;
if (!hasMore) break;
cursor = data["nextCursor"]?.Value<int>() ?? cursor + pageSize;
// Safety limit
if (pageCount > 50) break;
}
Assert.AreEqual(MEDIUM_BATCH, foundFromCreated, $"Should find all {MEDIUM_BATCH} created objects across pages");
Debug.Log($"[Pagination] Found {foundFromCreated} created objects across {pageCount} pages");
}
#endregion
#region Component Operations at Scale
[Test]
public void AddComponents_MultipleToSingleObject()
{
var go = CreateTestObject("ComponentHost");
string[] componentTypeNames = new[]
{
"BoxCollider",
"Rigidbody",
"Light",
"Camera"
};
var sw = Stopwatch.StartNew();
foreach (var compType in componentTypeNames)
{
var result = ToJObject(ManageComponents.HandleCommand(new JObject
{
["action"] = "add",
["target"] = go.GetInstanceID().ToString(),
["searchMethod"] = "by_id",
["componentType"] = compType // Correct parameter name
}));
Assert.IsTrue(result["success"]?.Value<bool>() ?? false, $"Failed to add {compType}: {result["message"]}");
}
sw.Stop();
Debug.Log($"[AddComponents] Added {componentTypeNames.Length} components in {sw.ElapsedMilliseconds}ms");
// Verify all components present
Assert.AreEqual(componentTypeNames.Length + 1, go.GetComponents<Component>().Length); // +1 for Transform
}
[Test]
public void GetComponents_ObjectWithManyComponents()
{
var go = CreateTestObject("HeavyComponents");
// Add many components - but skip AudioSource as it triggers deprecated API warnings
go.AddComponent<BoxCollider>();
go.AddComponent<SphereCollider>();
go.AddComponent<CapsuleCollider>();
go.AddComponent<MeshCollider>();
go.AddComponent<Rigidbody>();
go.AddComponent<Light>();
go.AddComponent<Camera>();
go.AddComponent<AudioListener>();
var sw = Stopwatch.StartNew();
// Use the resource handler for getting components
var result = ToJObject(GameObjectComponentsResource.HandleCommand(new JObject
{
["instanceID"] = go.GetInstanceID(),
["includeProperties"] = true,
["pageSize"] = 50
}));
sw.Stop();
Assert.IsTrue(result["success"]?.Value<bool>() ?? false, $"GetComponents failed: {result["message"]}");
var data = result["data"] as JObject;
var components = data?["components"] as JArray;
Assert.IsNotNull(components);
Assert.AreEqual(9, components.Count); // 8 added + Transform
Debug.Log($"[GetComponents] Retrieved {components.Count} components with properties in {sw.ElapsedMilliseconds}ms");
}
[Test]
public void SetComponentProperties_ComplexRigidbody()
{
var go = CreateTestObject("RigidbodyTest");
go.AddComponent<Rigidbody>();
var result = ToJObject(ManageComponents.HandleCommand(new JObject
{
["action"] = "set_property",
["target"] = go.GetInstanceID().ToString(),
["searchMethod"] = "by_id",
["componentType"] = "Rigidbody", // Correct parameter name
["properties"] = new JObject // Correct parameter name
{
["mass"] = 10.5f,
["drag"] = 0.5f,
["angularDrag"] = 0.1f,
["useGravity"] = false,
["isKinematic"] = true
}
}));
Assert.IsTrue(result["success"]?.Value<bool>() ?? false, $"Set property failed: {result["message"]}");
var rb = go.GetComponent<Rigidbody>();
Assert.AreEqual(10.5f, rb.mass, 0.01f);
Assert.AreEqual(0.5f, rb.drag, 0.01f);
Assert.AreEqual(0.1f, rb.angularDrag, 0.01f);
Assert.IsFalse(rb.useGravity);
Assert.IsTrue(rb.isKinematic);
}
#endregion
#region Deep Hierarchy Operations
[Test]
public void CreateDeepHierarchy_FindByPath()
{
// Create a deep hierarchy: Root/Level1/Level2/Level3/Target
var root = CreateTestObject("DeepRoot");
var current = root;
for (int i = 1; i <= 5; i++)
{
var child = CreateTestObject($"Level{i}");
child.transform.SetParent(current.transform);
current = child;
}
var target = CreateTestObject("DeepTarget");
target.transform.SetParent(current.transform);
// Find by path
var result = ToJObject(FindGameObjects.HandleCommand(new JObject
{
["searchTerm"] = "DeepRoot/Level1/Level2/Level3/Level4/Level5/DeepTarget",
["searchMethod"] = "by_path"
}));
Assert.IsTrue(result["success"]?.Value<bool>() ?? false);
var data = result["data"] as JObject;
var ids = data?["instanceIDs"] as JArray;
Assert.IsNotNull(ids);
Assert.AreEqual(1, ids.Count);
Assert.AreEqual(target.GetInstanceID(), ids[0].Value<int>());
}
[Test]
public void GetHierarchy_LargeScene_Paginated()
{
// Create flat hierarchy with many objects
for (int i = 0; i < MEDIUM_BATCH; i++)
{
CreateTestObject($"HierarchyItem_{i:D3}");
}
var result = ToJObject(ManageScene.HandleCommand(new JObject
{
["action"] = "get_hierarchy",
["pageSize"] = 20,
["maxNodes"] = 100
}));
Assert.IsTrue(result["success"]?.Value<bool>() ?? false);
var data = result["data"] as JObject;
var items = data?["items"] as JArray;
Assert.IsNotNull(items);
Assert.GreaterOrEqual(items.Count, 1);
// Verify componentTypes is included
var firstItem = items[0] as JObject;
Assert.IsNotNull(firstItem?["componentTypes"], "Should include componentTypes");
Debug.Log($"[GetHierarchy] Retrieved {items.Count} items from hierarchy");
}
#endregion
#region Resource Read Performance
[Test]
public void GameObjectResource_ReadComplexObject()
{
var go = CreateTestObject("ComplexObject");
go.tag = "Player";
go.layer = 8;
go.isStatic = true;
// Add components - AudioSource is OK here since we're only reading component types, not serializing properties
go.AddComponent<Rigidbody>();
go.AddComponent<BoxCollider>();
go.AddComponent<AudioSource>();
// Add children
for (int i = 0; i < 5; i++)
{
var child = CreateTestObject($"Child_{i}");
child.transform.SetParent(go.transform);
}
var sw = Stopwatch.StartNew();
// Call the resource directly (no action param needed)
var result = ToJObject(GameObjectResource.HandleCommand(new JObject
{
["instanceID"] = go.GetInstanceID()
}));
sw.Stop();
Assert.IsTrue(result["success"]?.Value<bool>() ?? false);
var data = result["data"] as JObject;
Assert.AreEqual("ComplexObject", data?["name"]?.Value<string>());
Assert.AreEqual("Player", data?["tag"]?.Value<string>());
Assert.AreEqual(8, data?["layer"]?.Value<int>());
var componentTypes = data?["componentTypes"] as JArray;
Assert.IsNotNull(componentTypes);
Assert.AreEqual(4, componentTypes.Count); // Transform + 3 added
var children = data?["children"] as JArray;
Assert.IsNotNull(children);
Assert.AreEqual(5, children.Count);
Debug.Log($"[GameObjectResource] Read complex object in {sw.ElapsedMilliseconds}ms");
}
[Test]
public void ComponentsResource_ReadAllWithFullSerialization()
{
var go = CreateTestObject("FullSerialize");
var rb = go.AddComponent<Rigidbody>();
rb.mass = 5.5f;
rb.drag = 1.2f;
var col = go.AddComponent<BoxCollider>();
col.size = new Vector3(2, 3, 4);
col.center = new Vector3(0.5f, 0.5f, 0.5f);
// Skip AudioSource to avoid deprecated API warnings
var sw = Stopwatch.StartNew();
// Use the components resource handler
var result = ToJObject(GameObjectComponentsResource.HandleCommand(new JObject
{
["instanceID"] = go.GetInstanceID(),
["includeProperties"] = true
}));
sw.Stop();
Assert.IsTrue(result["success"]?.Value<bool>() ?? false);
var data = result["data"] as JObject;
var components = data?["components"] as JArray;
Assert.IsNotNull(components);
Assert.AreEqual(3, components.Count); // Transform + Rigidbody + BoxCollider
Debug.Log($"[ComponentsResource] Full serialization of {components.Count} components in {sw.ElapsedMilliseconds}ms");
// Verify serialized data includes properties
bool foundRigidbody = false;
foreach (JObject comp in components)
{
var typeName = comp["typeName"]?.Value<string>();
if (typeName != null && typeName.Contains("Rigidbody"))
{
foundRigidbody = true;
// GameObjectSerializer puts properties inside a "properties" nested object
var props = comp["properties"] as JObject;
Assert.IsNotNull(props, $"Rigidbody should have properties. Component data: {comp}");
float massValue = props["mass"]?.Value<float>() ?? 0;
Assert.AreEqual(5.5f, massValue, 0.01f, $"Mass should be 5.5");
}
}
Assert.IsTrue(foundRigidbody, "Should find Rigidbody with serialized properties");
}
#endregion
#region Concurrent-Like Operations
[Test]
public void RapidFireOperations_CreateModifyDelete()
{
var sw = Stopwatch.StartNew();
for (int i = 0; i < SMALL_BATCH; i++)
{
// Create
var createResult = ToJObject(ManageGameObject.HandleCommand(new JObject
{
["action"] = "create",
["name"] = $"RapidFire_{i}"
}));
Assert.IsTrue(createResult["success"]?.Value<bool>() ?? false, $"Create failed: {createResult["message"]}");
int instanceId = createResult["data"]?["instanceID"]?.Value<int>() ?? 0;
Assert.AreNotEqual(0, instanceId, "Instance ID should not be 0");
// Modify - use layer 0 (Default) to avoid layer name issues
var modifyResult = ToJObject(ManageGameObject.HandleCommand(new JObject
{
["action"] = "modify",
["target"] = instanceId.ToString(),
["searchMethod"] = "by_id",
["name"] = $"RapidFire_Modified_{i}", // Use name modification instead
["setActive"] = true
}));
Assert.IsTrue(modifyResult["success"]?.Value<bool>() ?? false, $"Modify failed: {modifyResult["message"]}");
// Delete
var deleteResult = ToJObject(ManageGameObject.HandleCommand(new JObject
{
["action"] = "delete",
["target"] = instanceId.ToString(),
["searchMethod"] = "by_id"
}));
Assert.IsTrue(deleteResult["success"]?.Value<bool>() ?? false, $"Delete failed: {deleteResult["message"]}");
}
sw.Stop();
Debug.Log($"[RapidFire] {SMALL_BATCH} create-modify-delete cycles in {sw.ElapsedMilliseconds}ms");
Assert.Less(sw.ElapsedMilliseconds, 10000, "Rapid fire operations took too long");
}
#endregion
}
/// <summary>
/// Marker component used for isolating component-based searches to objects created by this test fixture.
/// </summary>
public sealed class GameObjectAPIStressTestMarker : MonoBehaviour { }
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7392c46e26c4649479cce9912fa94c1d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,108 @@
using System.Text.RegularExpressions;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Tools;
using MCPForUnity.Editor.Tools.GameObjects;
namespace MCPForUnityTests.Editor.Tools
{
/// <summary>
/// Tests for GameObjectComponentHelpers.SetComponentPropertiesInternal error reporting.
/// Reproduces issue #765: conversion failures incorrectly reported as "Property not found".
/// </summary>
public class GameObjectComponentHelpersErrorTests
{
private GameObject testGo;
[SetUp]
public void SetUp()
{
testGo = new GameObject("ErrorTestGO");
CommandRegistry.Initialize();
}
[TearDown]
public void TearDown()
{
if (testGo != null)
Object.DestroyImmediate(testGo);
}
/// <summary>
/// When a property exists but conversion fails, the error should say
/// "Failed to convert" rather than "Property not found. Did you mean: X?"
/// </summary>
[Test]
public void SetComponentProperties_ConversionFailure_ReportsConversionError_NotPropertyNotFound()
{
// Expect conversion error log from PropertyConversion (ComponentOps reflection attempt)
LogAssert.Expect(LogType.Error, new Regex("Error converting token"));
// Expect the warning log from SetComponentPropertiesInternal
LogAssert.Expect(LogType.Warning, new Regex("Failed to set"));
var audioSource = testGo.AddComponent<AudioSource>();
// spatialBlend is a float property — passing an array triggers conversion failure
var props = new JObject { ["spatialBlend"] = JArray.Parse("[1, 2, 3]") };
var result = GameObjectComponentHelpers.SetComponentPropertiesInternal(
testGo, "AudioSource", props, audioSource);
Assert.IsNotNull(result, "Should return an error response");
Assert.IsInstanceOf<ErrorResponse>(result);
var errorResponse = (ErrorResponse)result;
// The error message must NOT say "not found" for a property that exists
Assert.IsFalse(
errorResponse.Error.Contains("not found"),
$"Error should report conversion failure, not 'not found'. Got: {errorResponse.Error}");
}
/// <summary>
/// When a property genuinely doesn't exist, the error should still say "not found" with suggestions.
/// </summary>
[Test]
public void SetComponentProperties_NonexistentProperty_ReportsNotFound()
{
// Expect the "not found" warning
LogAssert.Expect(LogType.Warning, new Regex("not found"));
var audioSource = testGo.AddComponent<AudioSource>();
var props = new JObject { ["totallyFakeProperty"] = 42 };
var result = GameObjectComponentHelpers.SetComponentPropertiesInternal(
testGo, "AudioSource", props, audioSource);
Assert.IsNotNull(result);
Assert.IsInstanceOf<ErrorResponse>(result);
var errorResponse = (ErrorResponse)result;
Assert.IsTrue(
errorResponse.Error.Contains("not found") || errorResponse.Error.Contains("failed"),
$"Error for nonexistent property should say 'not found'. Got: {errorResponse.Error}");
}
/// <summary>
/// Valid property setting should still succeed.
/// </summary>
[Test]
public void SetComponentProperties_ValidProperty_Succeeds()
{
var audioSource = testGo.AddComponent<AudioSource>();
var props = new JObject { ["volume"] = 0.42f };
var result = GameObjectComponentHelpers.SetComponentPropertiesInternal(
testGo, "AudioSource", props, audioSource);
Assert.IsNull(result, "Should return null on success (no errors)");
Assert.AreEqual(0.42f, audioSource.volume, 0.001f);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 51225acc846dc412b82750041eb0ee61
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,381 @@
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using UnityEditor;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Tools;
using MCPForUnity.Editor.Tools.GameObjects;
using System;
using System.IO;
using System.Text.RegularExpressions;
namespace MCPForUnityTests.Editor.Tools
{
/// <summary>
/// Tests for MCP tool parameter handling - JSON parsing in manage_asset and manage_gameobject tools.
/// Consolidated from multiple redundant tests into focused, non-overlapping test cases.
/// </summary>
public class MCPToolParameterTests
{
private const string TempDir = "Assets/Temp/MCPToolParameterTests";
private const string TempLiveDir = "Assets/Temp/LiveTests";
private static void AssertColorsEqual(Color expected, Color actual, string message)
{
const float tolerance = 0.001f;
Assert.AreEqual(expected.r, actual.r, tolerance, $"{message} - Red component mismatch");
Assert.AreEqual(expected.g, actual.g, tolerance, $"{message} - Green component mismatch");
Assert.AreEqual(expected.b, actual.b, tolerance, $"{message} - Blue component mismatch");
Assert.AreEqual(expected.a, actual.a, tolerance, $"{message} - Alpha component mismatch");
}
private static void AssertShaderIsSupported(Shader s)
{
Assert.IsNotNull(s, "Shader should not be null");
var name = s.name;
bool ok = name == "Universal Render Pipeline/Lit"
|| name == "HDRP/Lit"
|| name == "Standard"
|| name == "Unlit/Color";
Assert.IsTrue(ok, $"Unexpected shader: {name}");
}
private static void EnsureTempFolders()
{
if (!AssetDatabase.IsValidFolder("Assets/Temp"))
AssetDatabase.CreateFolder("Assets", "Temp");
if (!AssetDatabase.IsValidFolder(TempDir))
AssetDatabase.CreateFolder("Assets/Temp", "MCPToolParameterTests");
if (!AssetDatabase.IsValidFolder(TempLiveDir))
AssetDatabase.CreateFolder("Assets/Temp", "LiveTests");
}
[TearDown]
public void TearDown()
{
if (AssetDatabase.IsValidFolder(TempDir))
AssetDatabase.DeleteAsset(TempDir);
if (AssetDatabase.IsValidFolder(TempLiveDir))
AssetDatabase.DeleteAsset(TempLiveDir);
if (AssetDatabase.IsValidFolder("Assets/Temp"))
{
var remainingDirs = Directory.GetDirectories("Assets/Temp");
var remainingFiles = Directory.GetFiles("Assets/Temp");
if (remainingDirs.Length == 0 && remainingFiles.Length == 0)
AssetDatabase.DeleteAsset("Assets/Temp");
}
}
/// <summary>
/// Tests GameObject componentProperties JSON string coercion path.
/// Verifies material assignment via JSON string works correctly.
/// </summary>
[Test]
public void ManageGameObject_JSONComponentProperties_AssignsMaterial()
{
EnsureTempFolders();
var matPath = $"{TempDir}/Mat_{Guid.NewGuid():N}.mat";
// Create material with object-typed properties
var createMat = new JObject
{
["action"] = "create",
["path"] = matPath,
["assetType"] = "Material",
["properties"] = new JObject { ["shader"] = "Universal Render Pipeline/Lit", ["color"] = new JArray(0, 0, 1, 1) }
};
var createMatRes = ManageAsset.HandleCommand(createMat);
var createMatObj = createMatRes as JObject ?? JObject.FromObject(createMatRes);
Assert.IsTrue(createMatObj.Value<bool>("success"), createMatObj.ToString());
// Create a sphere
var createGo = new JObject { ["action"] = "create", ["name"] = "MCPParamTestSphere", ["primitiveType"] = "Sphere" };
var createGoRes = ManageGameObject.HandleCommand(createGo);
var createGoObj = createGoRes as JObject ?? JObject.FromObject(createGoRes);
Assert.IsTrue(createGoObj.Value<bool>("success"), createGoObj.ToString());
try
{
// Assign material via JSON string componentProperties (coercion path)
var compJsonObj = new JObject { ["MeshRenderer"] = new JObject { ["sharedMaterial"] = matPath } };
var compJson = compJsonObj.ToString(Newtonsoft.Json.Formatting.None);
var modify = new JObject
{
["action"] = "modify",
["target"] = "MCPParamTestSphere",
["searchMethod"] = "by_name",
["componentProperties"] = compJson
};
var raw = ManageGameObject.HandleCommand(modify);
var result = raw as JObject ?? JObject.FromObject(raw);
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
// Verify material assignment
var goVerify = GameObject.Find("MCPParamTestSphere");
Assert.IsNotNull(goVerify, "GameObject should exist");
var renderer = goVerify.GetComponent<MeshRenderer>();
Assert.IsNotNull(renderer, "MeshRenderer should exist");
var assignedMat = renderer.sharedMaterial;
Assert.IsNotNull(assignedMat, "sharedMaterial should be assigned");
AssertShaderIsSupported(assignedMat.shader);
var createdMat = AssetDatabase.LoadAssetAtPath<Material>(matPath);
Assert.AreEqual(createdMat, assignedMat, "Assigned material should match created material");
}
finally
{
var go = GameObject.Find("MCPParamTestSphere");
if (go != null) UnityEngine.Object.DestroyImmediate(go);
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(matPath) != null)
AssetDatabase.DeleteAsset(matPath);
AssetDatabase.Refresh();
}
}
/// <summary>
/// Comprehensive end-to-end test covering all 10 property handling scenarios:
/// 1. Create material via JSON string
/// 2. Modify color and metallic (friendly names)
/// 3. Modify using structured float block
/// 4. Assign texture via direct prop alias
/// 5. Assign texture via structured block
/// 6. Create sphere and assign material via componentProperties JSON
/// 7. Use URP color alias key
/// 8. Invalid JSON handling (graceful degradation)
/// 9. Switch shader pipeline dynamically
/// 10. Mixed friendly and alias keys
/// </summary>
[Test]
public void EndToEnd_PropertyHandling_AllScenarios()
{
EnsureTempFolders();
string guidSuffix = Guid.NewGuid().ToString("N").Substring(0, 8);
string matPath = $"{TempLiveDir}/Mat_{guidSuffix}.mat";
string texPath = $"{TempLiveDir}/TempBaseTex_{guidSuffix}.asset";
string sphereName = $"LiveSphere_{guidSuffix}";
string badJsonPath = $"{TempLiveDir}/BadJson_{guidSuffix}.mat";
// Ensure clean state
var preSphere = GameObject.Find(sphereName);
if (preSphere != null) UnityEngine.Object.DestroyImmediate(preSphere);
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(matPath) != null)
AssetDatabase.DeleteAsset(matPath);
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(badJsonPath) != null)
AssetDatabase.DeleteAsset(badJsonPath);
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(texPath) != null)
AssetDatabase.DeleteAsset(texPath);
// Create test texture for texture-dependent scenarios (4, 5, 10)
var tex = new Texture2D(4, 4, TextureFormat.RGBA32, false);
var pixels = new Color[16];
for (int i = 0; i < pixels.Length; i++) pixels[i] = Color.white;
tex.SetPixels(pixels);
tex.Apply();
AssetDatabase.CreateAsset(tex, texPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
try
{
// 1. Create material via JSON string
var createParams = new JObject
{
["action"] = "create",
["path"] = matPath,
["assetType"] = "Material",
["properties"] = "{\"shader\":\"Universal Render Pipeline/Lit\",\"color\":[1,0,0,1]}"
};
var createRaw = ManageAsset.HandleCommand(createParams);
var createResult = createRaw as JObject ?? JObject.FromObject(createRaw);
Assert.IsTrue(createResult.Value<bool>("success"), $"Test 1 failed: {createResult}");
var mat = AssetDatabase.LoadAssetAtPath<Material>(matPath);
Assert.IsNotNull(mat, "Material should be created");
if (mat.HasProperty("_BaseColor"))
Assert.AreEqual(Color.red, mat.GetColor("_BaseColor"), "Test 1: _BaseColor should be red");
else if (mat.HasProperty("_Color"))
Assert.AreEqual(Color.red, mat.GetColor("_Color"), "Test 1: _Color should be red");
else
Assert.Inconclusive("Material has neither _BaseColor nor _Color");
// 2. Modify color and metallic (friendly names)
var modify1 = new JObject
{
["action"] = "modify",
["path"] = matPath,
["properties"] = "{\"color\":[0,0.5,1,1],\"metallic\":0.6}"
};
var modifyRaw1 = ManageAsset.HandleCommand(modify1);
var modifyResult1 = modifyRaw1 as JObject ?? JObject.FromObject(modifyRaw1);
Assert.IsTrue(modifyResult1.Value<bool>("success"), $"Test 2 failed: {modifyResult1}");
mat = AssetDatabase.LoadAssetAtPath<Material>(matPath);
var expectedCyan = new Color(0, 0.5f, 1, 1);
if (mat.HasProperty("_BaseColor"))
Assert.AreEqual(expectedCyan, mat.GetColor("_BaseColor"), "Test 2: _BaseColor should be cyan");
else if (mat.HasProperty("_Color"))
Assert.AreEqual(expectedCyan, mat.GetColor("_Color"), "Test 2: _Color should be cyan");
Assert.AreEqual(0.6f, mat.GetFloat("_Metallic"), 0.001f, "Test 2: Metallic should be 0.6");
// 3. Modify using structured float block
var modify2 = new JObject
{
["action"] = "modify",
["path"] = matPath,
["properties"] = new JObject
{
["float"] = new JObject { ["name"] = "_Metallic", ["value"] = 0.1 }
}
};
var modifyRaw2 = ManageAsset.HandleCommand(modify2);
var modifyResult2 = modifyRaw2 as JObject ?? JObject.FromObject(modifyRaw2);
Assert.IsTrue(modifyResult2.Value<bool>("success"), $"Test 3 failed: {modifyResult2}");
mat = AssetDatabase.LoadAssetAtPath<Material>(matPath);
Assert.AreEqual(0.1f, mat.GetFloat("_Metallic"), 0.001f, "Test 3: Metallic should be 0.1");
// 4. Assign texture via direct prop alias
var modify3 = new JObject
{
["action"] = "modify",
["path"] = matPath,
["properties"] = "{\"_BaseMap\":\"" + texPath + "\"}"
};
var modifyRaw3 = ManageAsset.HandleCommand(modify3);
var modifyResult3 = modifyRaw3 as JObject ?? JObject.FromObject(modifyRaw3);
Assert.IsTrue(modifyResult3.Value<bool>("success"), $"Test 4 failed: {modifyResult3}");
// 5. Assign texture via structured block
var modify4 = new JObject
{
["action"] = "modify",
["path"] = matPath,
["properties"] = new JObject
{
["texture"] = new JObject { ["name"] = "_MainTex", ["path"] = texPath }
}
};
var modifyRaw4 = ManageAsset.HandleCommand(modify4);
var modifyResult4 = modifyRaw4 as JObject ?? JObject.FromObject(modifyRaw4);
Assert.IsTrue(modifyResult4.Value<bool>("success"), $"Test 5 failed: {modifyResult4}");
// 6. Create sphere and assign material via componentProperties JSON string
var createSphere = new JObject
{
["action"] = "create",
["name"] = sphereName,
["primitiveType"] = "Sphere"
};
var sphereRaw = ManageGameObject.HandleCommand(createSphere);
var sphereResult = sphereRaw as JObject ?? JObject.FromObject(sphereRaw);
Assert.IsTrue(sphereResult.Value<bool>("success"), $"Test 6 - Create sphere failed: {sphereResult}");
var modifySphere = new JObject
{
["action"] = "modify",
["target"] = sphereName,
["searchMethod"] = "by_name",
["componentProperties"] = "{\"MeshRenderer\":{\"sharedMaterial\":\"" + matPath + "\"}}"
};
var sphereModifyRaw = ManageGameObject.HandleCommand(modifySphere);
var sphereModifyResult = sphereModifyRaw as JObject ?? JObject.FromObject(sphereModifyRaw);
Assert.IsTrue(sphereModifyResult.Value<bool>("success"), $"Test 6 - Assign material failed: {sphereModifyResult}");
var sphere = GameObject.Find(sphereName);
Assert.IsNotNull(sphere, "Test 6: Sphere should exist");
var renderer = sphere.GetComponent<MeshRenderer>();
Assert.IsNotNull(renderer.sharedMaterial, "Test 6: Material should be assigned");
// 7. Use URP color alias key
var modify5 = new JObject
{
["action"] = "modify",
["path"] = matPath,
["properties"] = new JObject
{
["_BaseColor"] = new JArray(0.2, 0.8, 0.3, 1)
}
};
var modifyRaw5 = ManageAsset.HandleCommand(modify5);
var modifyResult5 = modifyRaw5 as JObject ?? JObject.FromObject(modifyRaw5);
Assert.IsTrue(modifyResult5.Value<bool>("success"), $"Test 7 failed: {modifyResult5}");
mat = AssetDatabase.LoadAssetAtPath<Material>(matPath);
Color expectedColor = new Color(0.2f, 0.8f, 0.3f, 1f);
if (mat.HasProperty("_BaseColor"))
AssertColorsEqual(expectedColor, mat.GetColor("_BaseColor"), "Test 7: _BaseColor should be set");
else if (mat.HasProperty("_Color"))
AssertColorsEqual(expectedColor, mat.GetColor("_Color"), "Test 7: Fallback _Color should be set");
// 8. Invalid JSON should warn (don't fail)
var invalidJson = new JObject
{
["action"] = "create",
["path"] = badJsonPath,
["assetType"] = "Material",
["properties"] = "{\"invalid\": json, \"missing\": quotes}"
};
LogAssert.Expect(LogType.Warning, new Regex("(failed to parse)|(Could not parse 'properties' JSON string)", RegexOptions.IgnoreCase));
var invalidRaw = ManageAsset.HandleCommand(invalidJson);
var invalidResult = invalidRaw as JObject ?? JObject.FromObject(invalidRaw);
Assert.IsNotNull(invalidResult, "Test 8: Should return a result");
// 9. Switch shader pipeline dynamically
var modify6 = new JObject
{
["action"] = "modify",
["path"] = matPath,
["properties"] = "{\"shader\":\"Standard\",\"color\":[1,1,0,1]}"
};
var modifyRaw6 = ManageAsset.HandleCommand(modify6);
var modifyResult6 = modifyRaw6 as JObject ?? JObject.FromObject(modifyRaw6);
Assert.IsTrue(modifyResult6.Value<bool>("success"), $"Test 9 failed: {modifyResult6}");
mat = AssetDatabase.LoadAssetAtPath<Material>(matPath);
// The "Standard" shader request is aliased to the active pipeline's lit shader.
var pipeline9 = RenderPipelineUtility.GetActivePipeline();
string expectedShader9 = pipeline9 switch
{
RenderPipelineUtility.PipelineKind.Universal => "Universal Render Pipeline/Lit",
RenderPipelineUtility.PipelineKind.HighDefinition => "HDRP/Lit",
_ => "Standard"
};
Assert.AreEqual(expectedShader9, mat.shader.name, $"Test 9: Shader should be {expectedShader9}");
string colorProp9 = mat.HasProperty("_BaseColor") ? "_BaseColor" : "_Color";
var c9 = mat.GetColor(colorProp9);
// Looser tolerance (0.02) for shader-switched colors due to color space conversion differences
Assert.IsTrue(Mathf.Abs(c9.r - 1f) < 0.02f && Mathf.Abs(c9.g - 1f) < 0.02f && Mathf.Abs(c9.b - 0f) < 0.02f,
"Test 9: Color should be near yellow");
// 10. Mixed friendly and alias keys in one go
var modify7 = new JObject
{
["action"] = "modify",
["path"] = matPath,
["properties"] = new JObject
{
["metallic"] = 0.8,
["smoothness"] = 0.3,
["albedo"] = texPath
}
};
var modifyRaw7 = ManageAsset.HandleCommand(modify7);
var modifyResult7 = modifyRaw7 as JObject ?? JObject.FromObject(modifyRaw7);
Assert.IsTrue(modifyResult7.Value<bool>("success"), $"Test 10 failed: {modifyResult7}");
mat = AssetDatabase.LoadAssetAtPath<Material>(matPath);
Assert.AreEqual(0.8f, mat.GetFloat("_Metallic"), 0.001f, "Test 10: Metallic should be 0.8");
// Built-in Standard stores smoothness in "_Glossiness"; URP/HDRP Lit uses "_Smoothness".
string smoothnessProp10 = mat.HasProperty("_Smoothness") ? "_Smoothness" : "_Glossiness";
Assert.AreEqual(0.3f, mat.GetFloat(smoothnessProp10), 0.001f, "Test 10: Smoothness should be 0.3");
}
finally
{
var sphere = GameObject.Find(sphereName);
if (sphere != null) UnityEngine.Object.DestroyImmediate(sphere);
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(matPath) != null)
AssetDatabase.DeleteAsset(matPath);
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(badJsonPath) != null)
AssetDatabase.DeleteAsset(badJsonPath);
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(texPath) != null)
AssetDatabase.DeleteAsset(texPath);
AssetDatabase.Refresh();
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 80144860477bb4293acf4669566b27b8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 486fc2e6daa8426383a93b7fafaa3800
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,50 @@
using NUnit.Framework;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Tools;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Tests.EditMode.Tools
{
[TestFixture]
public class ManageEditorUndoTests
{
[Test]
public void Undo_ReturnsSuccess()
{
var p = new JObject { ["action"] = "undo" };
var result = ManageEditor.HandleCommand(p);
var r = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(r.Value<bool>("success"), r.ToString());
}
[Test]
public void Redo_ReturnsSuccess()
{
var p = new JObject { ["action"] = "redo" };
var result = ManageEditor.HandleCommand(p);
var r = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(r.Value<bool>("success"), r.ToString());
}
[Test]
public void Undo_AfterRecordedChange_RevertsChange()
{
var go = new GameObject("UndoTestGO");
try
{
Undo.RecordObject(go, "Rename UndoTestGO");
go.name = "RenamedGO";
Undo.FlushUndoRecordObjects();
var p = new JObject { ["action"] = "undo" };
ManageEditor.HandleCommand(p);
Assert.AreEqual("UndoTestGO", go.name, "Name should revert after undo");
}
finally
{
Object.DestroyImmediate(go);
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9beec84be6a1148dd97618c11bd70104
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,491 @@
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using UnityEditorInternal;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Tools.GameObjects;
namespace MCPForUnityTests.Editor.Tools
{
/// <summary>
/// Comprehensive baseline tests for ManageGameObject "create" action.
/// These tests capture existing behavior before API redesign.
/// </summary>
public class ManageGameObjectCreateTests
{
private List<GameObject> createdObjects = new List<GameObject>();
[TearDown]
public void TearDown()
{
foreach (var go in createdObjects)
{
if (go != null)
{
Object.DestroyImmediate(go);
}
}
createdObjects.Clear();
}
private GameObject FindAndTrack(string name)
{
var go = GameObject.Find(name);
if (go != null && !createdObjects.Contains(go))
{
createdObjects.Add(go);
}
return go;
}
#region Basic Create Tests
[Test]
public void Create_WithNameOnly_CreatesEmptyGameObject()
{
var p = new JObject
{
["action"] = "create",
["name"] = "TestEmptyObject"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
var created = FindAndTrack("TestEmptyObject");
Assert.IsNotNull(created, "GameObject should be created");
Assert.AreEqual("TestEmptyObject", created.name);
}
[Test]
public void Create_WithoutName_ReturnsError()
{
var p = new JObject
{
["action"] = "create"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsFalse(resultObj.Value<bool>("success"), "Should fail without name");
}
[Test]
public void Create_WithEmptyName_ReturnsError()
{
var p = new JObject
{
["action"] = "create",
["name"] = ""
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsFalse(resultObj.Value<bool>("success"), "Should fail with empty name");
}
#endregion
#region Primitive Type Tests
[Test]
public void Create_PrimitiveCube_CreatesCubeWithComponents()
{
var p = new JObject
{
["action"] = "create",
["name"] = "TestCube",
["primitiveType"] = "Cube"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
var created = FindAndTrack("TestCube");
Assert.IsNotNull(created, "Cube should be created");
Assert.IsNotNull(created.GetComponent<MeshFilter>(), "Cube should have MeshFilter");
Assert.IsNotNull(created.GetComponent<MeshRenderer>(), "Cube should have MeshRenderer");
Assert.IsNotNull(created.GetComponent<BoxCollider>(), "Cube should have BoxCollider");
}
[Test]
public void Create_PrimitiveSphere_CreatesSphereWithComponents()
{
var p = new JObject
{
["action"] = "create",
["name"] = "TestSphere",
["primitiveType"] = "Sphere"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
var created = FindAndTrack("TestSphere");
Assert.IsNotNull(created, "Sphere should be created");
Assert.IsNotNull(created.GetComponent<SphereCollider>(), "Sphere should have SphereCollider");
}
[Test]
public void Create_PrimitiveCapsule_CreatesCapsule()
{
var p = new JObject
{
["action"] = "create",
["name"] = "TestCapsule",
["primitiveType"] = "Capsule"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
var created = FindAndTrack("TestCapsule");
Assert.IsNotNull(created, "Capsule should be created");
Assert.IsNotNull(created.GetComponent<CapsuleCollider>(), "Capsule should have CapsuleCollider");
}
[Test]
public void Create_PrimitivePlane_CreatesPlane()
{
var p = new JObject
{
["action"] = "create",
["name"] = "TestPlane",
["primitiveType"] = "Plane"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
var created = FindAndTrack("TestPlane");
Assert.IsNotNull(created, "Plane should be created");
}
[Test]
public void Create_PrimitiveCylinder_CreatesCylinder()
{
var p = new JObject
{
["action"] = "create",
["name"] = "TestCylinder",
["primitiveType"] = "Cylinder"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
var created = FindAndTrack("TestCylinder");
Assert.IsNotNull(created, "Cylinder should be created");
}
[Test]
public void Create_PrimitiveQuad_CreatesQuad()
{
var p = new JObject
{
["action"] = "create",
["name"] = "TestQuad",
["primitiveType"] = "Quad"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
var created = FindAndTrack("TestQuad");
Assert.IsNotNull(created, "Quad should be created");
}
[Test]
public void Create_InvalidPrimitiveType_HandlesGracefully()
{
var p = new JObject
{
["action"] = "create",
["name"] = "TestInvalidPrimitive",
["primitiveType"] = "InvalidType"
};
var result = ManageGameObject.HandleCommand(p);
// Should either fail or create empty object - capture current behavior
Assert.IsNotNull(result, "Should return a result");
}
#endregion
#region Transform Tests
[Test]
public void Create_WithPosition_SetsPosition()
{
var p = new JObject
{
["action"] = "create",
["name"] = "TestPositioned",
["position"] = new JArray { 1.0f, 2.0f, 3.0f }
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
var created = FindAndTrack("TestPositioned");
Assert.IsNotNull(created);
Assert.AreEqual(new Vector3(1f, 2f, 3f), created.transform.position);
}
[Test]
public void Create_WithRotation_SetsRotation()
{
var p = new JObject
{
["action"] = "create",
["name"] = "TestRotated",
["rotation"] = new JArray { 0.0f, 90.0f, 0.0f }
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
var created = FindAndTrack("TestRotated");
Assert.IsNotNull(created);
// Check Y rotation is approximately 90 degrees
Assert.AreEqual(90f, created.transform.eulerAngles.y, 0.1f);
}
[Test]
public void Create_WithScale_SetsScale()
{
var p = new JObject
{
["action"] = "create",
["name"] = "TestScaled",
["scale"] = new JArray { 2.0f, 3.0f, 4.0f }
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
var created = FindAndTrack("TestScaled");
Assert.IsNotNull(created);
Assert.AreEqual(new Vector3(2f, 3f, 4f), created.transform.localScale);
}
[Test]
public void Create_WithAllTransformProperties_SetsAll()
{
var p = new JObject
{
["action"] = "create",
["name"] = "TestFullTransform",
["position"] = new JArray { 5.0f, 6.0f, 7.0f },
["rotation"] = new JArray { 45.0f, 90.0f, 0.0f },
["scale"] = new JArray { 1.5f, 1.5f, 1.5f }
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
var created = FindAndTrack("TestFullTransform");
Assert.IsNotNull(created);
Assert.AreEqual(new Vector3(5f, 6f, 7f), created.transform.position);
Assert.AreEqual(new Vector3(1.5f, 1.5f, 1.5f), created.transform.localScale);
}
#endregion
#region Parenting Tests
[Test]
public void Create_WithParentByName_SetsParent()
{
// Create parent first
var parent = new GameObject("TestParent");
createdObjects.Add(parent);
var p = new JObject
{
["action"] = "create",
["name"] = "TestChild",
["parent"] = "TestParent"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
var child = FindAndTrack("TestChild");
Assert.IsNotNull(child);
Assert.AreEqual(parent.transform, child.transform.parent);
}
[Test]
public void Create_WithNonExistentParent_HandlesGracefully()
{
var p = new JObject
{
["action"] = "create",
["name"] = "TestOrphan",
["parent"] = "NonExistentParent"
};
var result = ManageGameObject.HandleCommand(p);
// Should either fail or create without parent - capture current behavior
Assert.IsNotNull(result, "Should return a result");
}
#endregion
#region Tag and Layer Tests
[Test]
public void Create_WithTag_SetsTag()
{
var p = new JObject
{
["action"] = "create",
["name"] = "TestTagged",
["tag"] = "MainCamera" // Use built-in tag
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
var created = FindAndTrack("TestTagged");
Assert.IsNotNull(created);
Assert.AreEqual("MainCamera", created.tag);
}
[Test]
public void Create_WithLayer_SetsLayer()
{
var p = new JObject
{
["action"] = "create",
["name"] = "TestLayered",
["layer"] = "UI" // Use built-in layer
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
var created = FindAndTrack("TestLayered");
Assert.IsNotNull(created);
Assert.AreEqual(LayerMask.NameToLayer("UI"), created.layer);
}
[Test]
public void Create_WithNewTag_AutoCreatesTag()
{
const string testTag = "AutoCreatedTag12345";
// Tags that don't exist are now auto-created
var p = new JObject
{
["action"] = "create",
["name"] = "TestAutoTag",
["tag"] = testTag
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
var created = FindAndTrack("TestAutoTag");
Assert.IsNotNull(created, "Object should be created");
Assert.AreEqual(testTag, created.tag, "Tag should be auto-created and assigned");
// Verify tag was actually added to the tag manager
Assert.That(UnityEditorInternal.InternalEditorUtility.tags, Does.Contain(testTag),
"Tag should exist in Unity's tag manager");
// Clean up the created tag
try { UnityEditorInternal.InternalEditorUtility.RemoveTag(testTag); } catch { }
}
#endregion
#region Response Structure Tests
[Test]
public void Create_Success_ReturnsInstanceID()
{
var p = new JObject
{
["action"] = "create",
["name"] = "TestInstanceID"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
var data = resultObj["data"];
Assert.IsNotNull(data, "Response should include data");
// Check that instanceID is returned (case-insensitive check)
var instanceID = data["instanceID"]?.Value<int>() ?? data["InstanceID"]?.Value<int>();
Assert.IsTrue(instanceID.HasValue && instanceID.Value != 0,
$"Response should include a non-zero instanceID. Data: {data}");
FindAndTrack("TestInstanceID");
}
[Test]
public void Create_Success_ReturnsName()
{
var p = new JObject
{
["action"] = "create",
["name"] = "TestReturnedName"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
var data = resultObj["data"];
Assert.IsNotNull(data, "Response should include data");
// Check name is in response
var nameValue = data["name"]?.ToString() ?? data["Name"]?.ToString();
Assert.IsTrue(!string.IsNullOrEmpty(nameValue) || data.ToString().Contains("TestReturnedName"),
"Response should include name");
FindAndTrack("TestReturnedName");
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ec38858ff125347778a30792e4bb1c3e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,380 @@
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Tools.GameObjects;
namespace MCPForUnityTests.Editor.Tools
{
/// <summary>
/// Comprehensive baseline tests for ManageGameObject "delete" action.
/// These tests capture existing behavior before API redesign.
/// </summary>
public class ManageGameObjectDeleteTests
{
private List<GameObject> testObjects = new List<GameObject>();
[TearDown]
public void TearDown()
{
foreach (var go in testObjects)
{
if (go != null)
{
Object.DestroyImmediate(go);
}
}
testObjects.Clear();
}
private GameObject CreateTestObject(string name)
{
var go = new GameObject(name);
testObjects.Add(go);
return go;
}
#region Basic Delete Tests
[Test]
public void Delete_ByName_DeletesObject()
{
var target = CreateTestObject("DeleteTargetByName");
var p = new JObject
{
["action"] = "delete",
["target"] = "DeleteTargetByName",
["searchMethod"] = "by_name"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
// Verify object is deleted
var found = GameObject.Find("DeleteTargetByName");
Assert.IsNull(found, "Object should be deleted");
// Remove from our tracking list since it's deleted
testObjects.Remove(target);
}
[Test]
public void Delete_ByInstanceID_DeletesObject()
{
var target = CreateTestObject("DeleteTargetByID");
int instanceID = target.GetInstanceID();
var p = new JObject
{
["action"] = "delete",
["target"] = instanceID,
["searchMethod"] = "by_id"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
// Verify object is deleted
var found = GameObject.Find("DeleteTargetByID");
Assert.IsNull(found, "Object should be deleted");
testObjects.Remove(target);
}
[Test]
public void Delete_NonExistentObject_ReturnsError()
{
var p = new JObject
{
["action"] = "delete",
["target"] = "NonExistentObject12345",
["searchMethod"] = "by_name"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsFalse(resultObj.Value<bool>("success"), "Should fail for non-existent object");
}
[Test]
public void Delete_WithoutTarget_ReturnsError()
{
var p = new JObject
{
["action"] = "delete"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsFalse(resultObj.Value<bool>("success"), "Should fail without target");
}
#endregion
#region Search Method Tests
[Test]
public void Delete_ByTag_DeletesMatchingObjects()
{
// Current behavior: delete action finds first matching object and deletes it.
// This test verifies at least one tagged object is deleted.
var target1 = CreateTestObject("DeleteByTag1");
var target2 = CreateTestObject("DeleteByTag2");
// Use built-in tag
target1.tag = "MainCamera";
target2.tag = "MainCamera";
var p = new JObject
{
["action"] = "delete",
["target"] = "MainCamera",
["searchMethod"] = "by_tag"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
// Verify at least one object was deleted (current behavior deletes first match)
bool target1Deleted = target1 == null; // Unity Object == null check
bool target2Deleted = target2 == null;
Assert.IsTrue(target1Deleted || target2Deleted, "At least one tagged object should be deleted");
// Check response data for deletion info
var data = resultObj["data"];
Assert.IsNotNull(data, "Response should include data");
// Clean up only surviving objects from tracking
if (!target1Deleted) testObjects.Remove(target1);
if (!target2Deleted) testObjects.Remove(target2);
}
[Test]
public void Delete_ByLayer_DeletesMatchingObjects()
{
var target = CreateTestObject("DeleteByLayer");
target.layer = LayerMask.NameToLayer("UI");
var p = new JObject
{
["action"] = "delete",
["target"] = "UI",
["searchMethod"] = "by_layer"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
// Verify the object was actually deleted
bool targetDeleted = target == null; // Unity Object == null check
Assert.IsTrue(targetDeleted, "Object on UI layer should be deleted");
Assert.IsFalse(testObjects.Contains(target) && target != null, "Deleted object should not be findable");
// Only remove from tracking if not already destroyed
if (!targetDeleted) testObjects.Remove(target);
}
[Test]
public void Delete_ByPath_DeletesObject()
{
var parent = CreateTestObject("DeleteParent");
var child = CreateTestObject("DeleteChild");
child.transform.SetParent(parent.transform);
var p = new JObject
{
["action"] = "delete",
["target"] = "DeleteParent/DeleteChild",
["searchMethod"] = "by_path"
};
var result = ManageGameObject.HandleCommand(p);
// Capture current behavior
Assert.IsNotNull(result, "Should return a result");
testObjects.Remove(child);
}
#endregion
#region Hierarchy Tests
[Test]
public void Delete_Parent_DeletesChildren()
{
var parent = CreateTestObject("DeleteParentWithChildren");
var child1 = CreateTestObject("Child1");
var child2 = CreateTestObject("Child2");
var grandchild = CreateTestObject("Grandchild");
child1.transform.SetParent(parent.transform);
child2.transform.SetParent(parent.transform);
grandchild.transform.SetParent(child1.transform);
var p = new JObject
{
["action"] = "delete",
["target"] = "DeleteParentWithChildren",
["searchMethod"] = "by_name"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
// All should be deleted
Assert.IsNull(GameObject.Find("DeleteParentWithChildren"), "Parent should be deleted");
Assert.IsNull(GameObject.Find("Child1"), "Child1 should be deleted");
Assert.IsNull(GameObject.Find("Child2"), "Child2 should be deleted");
Assert.IsNull(GameObject.Find("Grandchild"), "Grandchild should be deleted");
testObjects.Remove(parent);
testObjects.Remove(child1);
testObjects.Remove(child2);
testObjects.Remove(grandchild);
}
[Test]
public void Delete_Child_DoesNotDeleteParent()
{
var parent = CreateTestObject("ParentShouldSurvive");
var child = CreateTestObject("ChildToDelete");
child.transform.SetParent(parent.transform);
var p = new JObject
{
["action"] = "delete",
["target"] = "ChildToDelete",
["searchMethod"] = "by_name"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
// Child deleted, parent survives
Assert.IsNull(GameObject.Find("ChildToDelete"), "Child should be deleted");
Assert.IsNotNull(GameObject.Find("ParentShouldSurvive"), "Parent should survive");
testObjects.Remove(child);
}
#endregion
#region Response Structure Tests
[Test]
public void Delete_Success_ReturnsDeletedCount()
{
var target = CreateTestObject("DeleteCountTest");
var p = new JObject
{
["action"] = "delete",
["target"] = "DeleteCountTest",
["searchMethod"] = "by_name"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
// Verify object was actually deleted
bool targetDeleted = target == null;
Assert.IsTrue(targetDeleted, "Object should be deleted");
// Check for deleted count in response
var data = resultObj["data"];
Assert.IsNotNull(data, "Response should include data");
// Verify the actual count if present
if (data is JObject dataObj && dataObj.ContainsKey("deletedCount"))
{
Assert.AreEqual(1, dataObj.Value<int>("deletedCount"), "Should report 1 deleted object");
}
// Only remove from tracking if not already destroyed
if (!targetDeleted) testObjects.Remove(target);
}
#endregion
#region Edge Cases
[Test]
public void Delete_InactiveObject_StillDeletes()
{
var target = CreateTestObject("InactiveDeleteTarget");
target.SetActive(false);
var p = new JObject
{
["action"] = "delete",
["target"] = "InactiveDeleteTarget",
["searchMethod"] = "by_name"
};
var result = ManageGameObject.HandleCommand(p);
// Capture current behavior for inactive objects
Assert.IsNotNull(result, "Should return a result");
testObjects.Remove(target);
}
[Test]
public void Delete_MultipleObjectsSameName_DeletesCorrectly()
{
// Expected behavior: delete action with by_name finds the FIRST matching object
// and deletes only that one. This is consistent with Unity's GameObject.Find behavior.
var target1 = CreateTestObject("DuplicateName");
var target2 = CreateTestObject("DuplicateName");
var p = new JObject
{
["action"] = "delete",
["target"] = "DuplicateName",
["searchMethod"] = "by_name"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
// Verify deletion occurred - at least one should be deleted
bool target1Deleted = target1 == null;
bool target2Deleted = target2 == null;
Assert.IsTrue(target1Deleted || target2Deleted, "At least one object should be deleted");
// Count remaining objects with the name to verify behavior
int remainingCount = 0;
if (!target1Deleted) remainingCount++;
if (!target2Deleted) remainingCount++;
// Document the actual behavior: first match is deleted, second survives
// If both are deleted, that's also acceptable (bulk delete mode)
Assert.IsTrue(remainingCount <= 1, $"Expected at most 1 remaining, got {remainingCount}");
// Clean up only survivors from tracking
if (!target1Deleted) testObjects.Remove(target1);
if (!target2Deleted) testObjects.Remove(target2);
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e74a6d8990a344fd6a1e4b175d411be1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,456 @@
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using UnityEditorInternal;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Tools.GameObjects;
namespace MCPForUnityTests.Editor.Tools
{
/// <summary>
/// Comprehensive baseline tests for ManageGameObject "modify" action.
/// These tests capture existing behavior before API redesign.
/// </summary>
public class ManageGameObjectModifyTests
{
private List<GameObject> testObjects = new List<GameObject>();
[SetUp]
public void SetUp()
{
// Create a standard test object for each test
var go = new GameObject("ModifyTestObject");
go.transform.position = Vector3.zero;
go.transform.rotation = Quaternion.identity;
go.transform.localScale = Vector3.one;
testObjects.Add(go);
}
[TearDown]
public void TearDown()
{
foreach (var go in testObjects)
{
if (go != null)
{
Object.DestroyImmediate(go);
}
}
testObjects.Clear();
}
private GameObject CreateTestObject(string name)
{
var go = new GameObject(name);
testObjects.Add(go);
return go;
}
#region Target Resolution Tests
[Test]
public void Modify_ByName_FindsAndModifiesObject()
{
var p = new JObject
{
["action"] = "modify",
["target"] = "ModifyTestObject",
["searchMethod"] = "by_name",
["position"] = new JArray { 10.0f, 0.0f, 0.0f }
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
Assert.AreEqual(new Vector3(10f, 0f, 0f), testObjects[0].transform.position);
}
[Test]
public void Modify_ByInstanceID_FindsAndModifiesObject()
{
int instanceID = testObjects[0].GetInstanceID();
var p = new JObject
{
["action"] = "modify",
["target"] = instanceID,
["searchMethod"] = "by_id",
["position"] = new JArray { 20.0f, 0.0f, 0.0f }
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
Assert.AreEqual(new Vector3(20f, 0f, 0f), testObjects[0].transform.position);
}
[Test]
public void Modify_WithNameAlias_UsesNameAsTarget()
{
// When target is missing but name is provided, should use name as target
var p = new JObject
{
["action"] = "modify",
["name"] = "ModifyTestObject",
["position"] = new JArray { 30.0f, 0.0f, 0.0f }
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
Assert.AreEqual(new Vector3(30f, 0f, 0f), testObjects[0].transform.position);
}
[Test]
public void Modify_NonExistentTarget_ReturnsError()
{
var p = new JObject
{
["action"] = "modify",
["target"] = "NonExistentObject12345",
["searchMethod"] = "by_name",
["position"] = new JArray { 0.0f, 0.0f, 0.0f }
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsFalse(resultObj.Value<bool>("success"), "Should fail for non-existent object");
}
[Test]
public void Modify_WithoutTarget_ReturnsError()
{
var p = new JObject
{
["action"] = "modify",
["position"] = new JArray { 0.0f, 0.0f, 0.0f }
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsFalse(resultObj.Value<bool>("success"), "Should fail without target");
}
#endregion
#region Transform Modification Tests
[Test]
public void Modify_Position_SetsNewPosition()
{
var p = new JObject
{
["action"] = "modify",
["target"] = "ModifyTestObject",
["position"] = new JArray { 1.0f, 2.0f, 3.0f }
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
Assert.AreEqual(new Vector3(1f, 2f, 3f), testObjects[0].transform.position);
}
[Test]
public void Modify_Rotation_SetsNewRotation()
{
var p = new JObject
{
["action"] = "modify",
["target"] = "ModifyTestObject",
["rotation"] = new JArray { 0.0f, 90.0f, 0.0f }
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
Assert.AreEqual(90f, testObjects[0].transform.eulerAngles.y, 0.1f);
}
[Test]
public void Modify_Scale_SetsNewScale()
{
var p = new JObject
{
["action"] = "modify",
["target"] = "ModifyTestObject",
["scale"] = new JArray { 2.0f, 3.0f, 4.0f }
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
Assert.AreEqual(new Vector3(2f, 3f, 4f), testObjects[0].transform.localScale);
}
[Test]
public void Modify_AllTransformProperties_SetsAll()
{
var p = new JObject
{
["action"] = "modify",
["target"] = "ModifyTestObject",
["position"] = new JArray { 5.0f, 6.0f, 7.0f },
["rotation"] = new JArray { 45.0f, 45.0f, 45.0f },
["scale"] = new JArray { 0.5f, 0.5f, 0.5f }
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
Assert.AreEqual(new Vector3(5f, 6f, 7f), testObjects[0].transform.position);
Assert.AreEqual(new Vector3(0.5f, 0.5f, 0.5f), testObjects[0].transform.localScale);
}
#endregion
#region Rename Tests
[Test]
public void Modify_Name_RenamesObject()
{
// Get instanceID first since name will change
int instanceID = testObjects[0].GetInstanceID();
var p = new JObject
{
["action"] = "modify",
["target"] = instanceID,
["searchMethod"] = "by_id",
["name"] = "RenamedObject" // Uses 'name' parameter, not 'newName'
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
Assert.AreEqual("RenamedObject", testObjects[0].name);
}
[Test]
public void Modify_NameToEmpty_HandlesGracefully()
{
int instanceID = testObjects[0].GetInstanceID();
var p = new JObject
{
["action"] = "modify",
["target"] = instanceID,
["searchMethod"] = "by_id",
["name"] = "" // Empty name
};
var result = ManageGameObject.HandleCommand(p);
// Capture current behavior - may reject or allow empty name
Assert.IsNotNull(result, "Should return a result");
}
#endregion
#region Reparenting Tests
[Test]
public void Modify_Parent_ReparentsObject()
{
var parent = CreateTestObject("NewParent");
var p = new JObject
{
["action"] = "modify",
["target"] = "ModifyTestObject",
["parent"] = "NewParent"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
Assert.AreEqual(parent.transform, testObjects[0].transform.parent);
}
[Test]
public void Modify_ParentToNull_UnparentsObject()
{
// First parent the object
var parent = CreateTestObject("TempParent");
testObjects[0].transform.SetParent(parent.transform);
var p = new JObject
{
["action"] = "modify",
["target"] = "ModifyTestObject",
["parent"] = JValue.CreateNull()
};
var result = ManageGameObject.HandleCommand(p);
// Capture current behavior for null parent
Assert.IsNotNull(result, "Should return a result");
}
[Test]
public void Modify_ParentToNonExistent_HandlesGracefully()
{
var p = new JObject
{
["action"] = "modify",
["target"] = "ModifyTestObject",
["parent"] = "NonExistentParent12345"
};
var result = ManageGameObject.HandleCommand(p);
// Should fail or handle gracefully
Assert.IsNotNull(result, "Should return a result");
}
#endregion
#region Active State Tests
[Test]
public void Modify_SetActive_DeactivatesObject()
{
Assert.IsTrue(testObjects[0].activeSelf, "Object should start active");
var p = new JObject
{
["action"] = "modify",
["target"] = "ModifyTestObject",
["setActive"] = false
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
Assert.IsFalse(testObjects[0].activeSelf, "Object should be deactivated");
}
[Test]
public void Modify_SetActive_ActivatesObject()
{
testObjects[0].SetActive(false);
Assert.IsFalse(testObjects[0].activeSelf, "Object should start inactive");
var p = new JObject
{
["action"] = "modify",
["target"] = "ModifyTestObject",
["setActive"] = true
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
Assert.IsTrue(testObjects[0].activeSelf, "Object should be activated");
}
#endregion
#region Tag and Layer Tests
[Test]
public void Modify_Tag_SetsNewTag()
{
var p = new JObject
{
["action"] = "modify",
["target"] = "ModifyTestObject",
["tag"] = "MainCamera"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
Assert.AreEqual("MainCamera", testObjects[0].tag);
}
[Test]
public void Modify_Layer_SetsNewLayer()
{
var p = new JObject
{
["action"] = "modify",
["target"] = "ModifyTestObject",
["layer"] = "UI"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
Assert.AreEqual(LayerMask.NameToLayer("UI"), testObjects[0].layer);
}
[Test]
public void Modify_NewTag_AutoCreatesTag()
{
const string testTag = "AutoModifyTag12345";
// Tags that don't exist are now auto-created
var p = new JObject
{
["action"] = "modify",
["target"] = "ModifyTestObject",
["tag"] = testTag
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
Assert.AreEqual(testTag, testObjects[0].tag, "Tag should be auto-created and assigned");
// Verify tag was actually added to the tag manager
Assert.That(UnityEditorInternal.InternalEditorUtility.tags, Does.Contain(testTag),
"Tag should exist in Unity's tag manager");
// Clean up the created tag
try { UnityEditorInternal.InternalEditorUtility.RemoveTag(testTag); } catch { }
}
#endregion
#region Multiple Modifications Tests
[Test]
public void Modify_MultipleProperties_AppliesAll()
{
var parent = CreateTestObject("MultiModifyParent");
int instanceID = testObjects[0].GetInstanceID();
var p = new JObject
{
["action"] = "modify",
["target"] = instanceID,
["searchMethod"] = "by_id",
["name"] = "MultiModifiedObject", // Uses 'name' not 'newName'
["position"] = new JArray { 100.0f, 200.0f, 300.0f },
["scale"] = new JArray { 5.0f, 5.0f, 5.0f },
["parent"] = "MultiModifyParent",
["tag"] = "MainCamera"
};
var result = ManageGameObject.HandleCommand(p);
var resultObj = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(resultObj.Value<bool>("success"), resultObj.ToString());
Assert.AreEqual("MultiModifiedObject", testObjects[0].name);
Assert.AreEqual(parent.transform, testObjects[0].transform.parent);
Assert.AreEqual("MainCamera", testObjects[0].tag);
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 042aca01b843348a3bc9ac86475e6293
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,645 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Tools;
using MCPForUnity.Editor.Tools.GameObjects;
namespace MCPForUnityTests.Editor.Tools
{
public class ManageGameObjectTests
{
private GameObject testGameObject;
[SetUp]
public void SetUp()
{
// Create a test GameObject for each test
testGameObject = new GameObject("TestObject");
}
[TearDown]
public void TearDown()
{
// Clean up test GameObject
if (testGameObject != null)
{
UnityEngine.Object.DestroyImmediate(testGameObject);
}
}
[Test]
public void HandleCommand_ReturnsError_ForNullParams()
{
var result = ManageGameObject.HandleCommand(null);
Assert.IsNotNull(result, "Should return a result object");
// Verify the result indicates an error state
var errorResponse = result as MCPForUnity.Editor.Helpers.ErrorResponse;
Assert.IsNotNull(errorResponse, "Should return an ErrorResponse for null params");
Assert.IsFalse(errorResponse.Success, "Success should be false for null params");
}
[Test]
public void HandleCommand_ReturnsError_ForEmptyParams()
{
var emptyParams = new JObject();
var result = ManageGameObject.HandleCommand(emptyParams);
Assert.IsNotNull(result, "Should return a result object for empty params");
// Verify the result indicates an error state (missing required action)
var errorResponse = result as MCPForUnity.Editor.Helpers.ErrorResponse;
Assert.IsNotNull(errorResponse, "Should return an ErrorResponse for empty params");
Assert.IsFalse(errorResponse.Success, "Success should be false for empty params");
}
[Test]
public void HandleCommand_ProcessesValidCreateAction()
{
var createParams = new JObject
{
["action"] = "create",
["name"] = "TestCreateObject"
};
var result = ManageGameObject.HandleCommand(createParams);
Assert.IsNotNull(result, "Should return a result for valid create action");
// Clean up - find and destroy the created object
var createdObject = GameObject.Find("TestCreateObject");
if (createdObject != null)
{
UnityEngine.Object.DestroyImmediate(createdObject);
}
}
[Test]
public void ComponentResolver_Integration_WorksWithRealComponents()
{
// Test that our ComponentResolver works with actual Unity components
var transformResult = ComponentResolver.TryResolve("Transform", out Type transformType, out string error);
Assert.IsTrue(transformResult, "Should resolve Transform component");
Assert.AreEqual(typeof(Transform), transformType, "Should return correct Transform type");
Assert.IsEmpty(error, "Should have no error for valid component");
}
[Test]
public void ComponentResolver_Integration_WorksWithBuiltInComponents()
{
var components = new[]
{
("Rigidbody", typeof(Rigidbody)),
("Collider", typeof(Collider)),
("Renderer", typeof(Renderer)),
("Camera", typeof(Camera)),
("Light", typeof(Light))
};
foreach (var (componentName, expectedType) in components)
{
var result = ComponentResolver.TryResolve(componentName, out Type actualType, out string error);
// Some components might not resolve (abstract classes), but the method should handle gracefully
if (result)
{
Assert.IsTrue(expectedType.IsAssignableFrom(actualType),
$"{componentName} should resolve to assignable type");
}
else
{
Assert.IsNotEmpty(error, $"Should have error message for {componentName}");
}
}
}
[Test]
public void PropertyMatching_Integration_WorksWithRealGameObject()
{
// Add a Rigidbody to test real property matching
var rigidbody = testGameObject.AddComponent<Rigidbody>();
var properties = ComponentResolver.GetAllComponentProperties(typeof(Rigidbody));
Assert.IsNotEmpty(properties, "Rigidbody should have properties");
Assert.Contains("mass", properties, "Rigidbody should have mass property");
Assert.Contains("useGravity", properties, "Rigidbody should have useGravity property");
// Test AI suggestions
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions("Use Gravity", properties);
Assert.Contains("useGravity", suggestions, "Should suggest useGravity for 'Use Gravity'");
}
[Test]
public void PropertyMatching_HandlesMonoBehaviourProperties()
{
var properties = ComponentResolver.GetAllComponentProperties(typeof(MonoBehaviour));
Assert.IsNotEmpty(properties, "MonoBehaviour should have properties");
Assert.Contains("enabled", properties, "MonoBehaviour should have enabled property");
Assert.Contains("name", properties, "MonoBehaviour should have name property");
Assert.Contains("tag", properties, "MonoBehaviour should have tag property");
}
[Test]
public void PropertyMatching_HandlesCaseVariations()
{
var testProperties = new List<string> { "maxReachDistance", "playerHealth", "movementSpeed" };
var testCases = new[]
{
("max reach distance", "maxReachDistance"),
("Max Reach Distance", "maxReachDistance"),
("MAX_REACH_DISTANCE", "maxReachDistance"),
("player health", "playerHealth"),
("movement speed", "movementSpeed")
};
foreach (var (input, expected) in testCases)
{
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions(input, testProperties);
Assert.Contains(expected, suggestions, $"Should suggest {expected} for input '{input}'");
}
}
[Test]
public void ErrorHandling_ReturnsHelpfulMessages()
{
// This test verifies that error messages are helpful and contain suggestions
var testProperties = new List<string> { "mass", "velocity", "drag", "useGravity" };
var suggestions = ComponentResolver.GetFuzzyPropertySuggestions("weight", testProperties);
// Even if no perfect match, should return valid list
Assert.IsNotNull(suggestions, "Should return valid suggestions list");
// Test with completely invalid input
var badSuggestions = ComponentResolver.GetFuzzyPropertySuggestions("xyz123invalid", testProperties);
Assert.IsNotNull(badSuggestions, "Should handle invalid input gracefully");
}
[Test]
public void PerformanceTest_CachingWorks()
{
var properties = ComponentResolver.GetAllComponentProperties(typeof(Transform));
var input = "Test Property Name";
// First call - populate cache
var startTime = System.DateTime.UtcNow;
var suggestions1 = ComponentResolver.GetFuzzyPropertySuggestions(input, properties);
var firstCallTime = (System.DateTime.UtcNow - startTime).TotalMilliseconds;
// Second call - should use cache
startTime = System.DateTime.UtcNow;
var suggestions2 = ComponentResolver.GetFuzzyPropertySuggestions(input, properties);
var secondCallTime = (System.DateTime.UtcNow - startTime).TotalMilliseconds;
Assert.AreEqual(suggestions1.Count, suggestions2.Count, "Cached results should be identical");
CollectionAssert.AreEqual(suggestions1, suggestions2, "Cached results should match exactly");
// Second call should be faster (though this test might be flaky)
Assert.LessOrEqual(secondCallTime, firstCallTime * 2, "Cached call should not be significantly slower");
}
[Test]
public void SetComponentProperties_CollectsAllFailuresAndAppliesValidOnes()
{
// Arrange - add Transform and Rigidbody components to test with
var transform = testGameObject.transform;
var rigidbody = testGameObject.AddComponent<Rigidbody>();
// Create a params object with mixed valid and invalid properties
var setPropertiesParams = new JObject
{
["action"] = "modify",
["target"] = testGameObject.name,
["search_method"] = "by_name",
["componentProperties"] = new JObject
{
["Transform"] = new JObject
{
["localPosition"] = new JObject { ["x"] = 1.0f, ["y"] = 2.0f, ["z"] = 3.0f }, // Valid
["rotatoin"] = new JObject { ["x"] = 0.0f, ["y"] = 90.0f, ["z"] = 0.0f }, // Invalid (typo - should be rotation)
["localScale"] = new JObject { ["x"] = 2.0f, ["y"] = 2.0f, ["z"] = 2.0f } // Valid
},
["Rigidbody"] = new JObject
{
["mass"] = 5.0f, // Valid
["invalidProp"] = "test", // Invalid - doesn't exist
["useGravity"] = true // Valid
}
}
};
// Store original values to verify changes
var originalLocalPosition = transform.localPosition;
var originalLocalScale = transform.localScale;
var originalMass = rigidbody.mass;
var originalUseGravity = rigidbody.useGravity;
Debug.Log($"BEFORE TEST - Mass: {rigidbody.mass}, UseGravity: {rigidbody.useGravity}");
// Expect the warning logs from the invalid properties
LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex("Property 'rotatoin' not found"));
LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex("Property 'invalidProp' not found"));
// Act
var result = ManageGameObject.HandleCommand(setPropertiesParams);
Debug.Log($"AFTER TEST - Mass: {rigidbody.mass}, UseGravity: {rigidbody.useGravity}");
Debug.Log($"AFTER TEST - LocalPosition: {transform.localPosition}");
Debug.Log($"AFTER TEST - LocalScale: {transform.localScale}");
// Assert - verify that valid properties were set despite invalid ones
Assert.AreEqual(new Vector3(1.0f, 2.0f, 3.0f), transform.localPosition,
"Valid localPosition should be set even with other invalid properties");
Assert.AreEqual(new Vector3(2.0f, 2.0f, 2.0f), transform.localScale,
"Valid localScale should be set even with other invalid properties");
Assert.AreEqual(5.0f, rigidbody.mass, 0.001f,
"Valid mass should be set even with other invalid properties");
Assert.AreEqual(true, rigidbody.useGravity,
"Valid useGravity should be set even with other invalid properties");
// Verify the result indicates errors (since we had invalid properties)
Assert.IsNotNull(result, "Should return a result object");
// The collect-and-continue behavior means we should get an error response
// that contains info about the failed properties, but valid ones were still applied
// This proves the collect-and-continue behavior is working
// Harden: verify structured error response with failures list contains both invalid fields
var successProp = result.GetType().GetProperty("success");
Assert.IsNotNull(successProp, "Result should expose 'success' property");
Assert.IsFalse((bool)successProp.GetValue(result), "Result.success should be false for partial failure");
var dataProp = result.GetType().GetProperty("data");
Assert.IsNotNull(dataProp, "Result should include 'data' with errors");
var dataVal = dataProp.GetValue(result);
Assert.IsNotNull(dataVal, "Result.data should not be null");
var errorsProp = dataVal.GetType().GetProperty("errors");
Assert.IsNotNull(errorsProp, "Result.data should include 'errors' list");
var errorsEnum = errorsProp.GetValue(dataVal) as System.Collections.IEnumerable;
Assert.IsNotNull(errorsEnum, "errors should be enumerable");
bool foundRotatoin = false;
bool foundInvalidProp = false;
foreach (var err in errorsEnum)
{
string s = err?.ToString() ?? string.Empty;
if (s.Contains("rotatoin")) foundRotatoin = true;
if (s.Contains("invalidProp")) foundInvalidProp = true;
}
Assert.IsTrue(foundRotatoin, "errors should mention the misspelled 'rotatoin' property");
Assert.IsTrue(foundInvalidProp, "errors should mention the 'invalidProp' property");
}
[Test]
public void SetComponentProperties_ContinuesAfterException()
{
// Arrange - create scenario that might cause exceptions
var rigidbody = testGameObject.AddComponent<Rigidbody>();
// Set initial values that we'll change
rigidbody.mass = 1.0f;
rigidbody.useGravity = true;
var setPropertiesParams = new JObject
{
["action"] = "modify",
["target"] = testGameObject.name,
["search_method"] = "by_name",
["componentProperties"] = new JObject
{
["Rigidbody"] = new JObject
{
["mass"] = 2.5f, // Valid - should be set
["velocity"] = "invalid_type", // Invalid type - will cause exception
["useGravity"] = false // Valid - should still be set after exception
}
}
};
// Expect the error logs from the invalid property
// Note: PropertyConversion logs "Error converting token to..." when conversion fails,
// then ComponentOps catches the exception and returns an error string (no second Error log).
// GameObjectComponentHelpers logs the failure as a warning.
LogAssert.Expect(LogType.Error, new System.Text.RegularExpressions.Regex("Error converting token to UnityEngine.Vector3"));
LogAssert.Expect(LogType.Warning, new System.Text.RegularExpressions.Regex(@"\[ManageGameObject\].*Failed to set property 'velocity'"));
// Act
var result = ManageGameObject.HandleCommand(setPropertiesParams);
// Assert - verify that valid properties before AND after the exception were still set
Assert.AreEqual(2.5f, rigidbody.mass, 0.001f,
"Mass should be set even if later property causes exception");
Assert.AreEqual(false, rigidbody.useGravity,
"UseGravity should be set even if previous property caused exception");
Assert.IsNotNull(result, "Should return a result even with exceptions");
// The key test: processing continued after the exception and set useGravity
// This proves the collect-and-continue behavior works even with exceptions
// Harden: verify structured error response contains velocity failure
var successProp2 = result.GetType().GetProperty("success");
Assert.IsNotNull(successProp2, "Result should expose 'success' property");
Assert.IsFalse((bool)successProp2.GetValue(result), "Result.success should be false when an exception occurs for a property");
var dataProp2 = result.GetType().GetProperty("data");
Assert.IsNotNull(dataProp2, "Result should include 'data' with errors");
var dataVal2 = dataProp2.GetValue(result);
Assert.IsNotNull(dataVal2, "Result.data should not be null");
var errorsProp2 = dataVal2.GetType().GetProperty("errors");
Assert.IsNotNull(errorsProp2, "Result.data should include 'errors' list");
var errorsEnum2 = errorsProp2.GetValue(dataVal2) as System.Collections.IEnumerable;
Assert.IsNotNull(errorsEnum2, "errors should be enumerable");
bool foundVelocityError = false;
foreach (var err in errorsEnum2)
{
string s = err?.ToString() ?? string.Empty;
if (s.Contains("velocity")) { foundVelocityError = true; break; }
}
Assert.IsTrue(foundVelocityError, "errors should include a message referencing 'velocity'");
}
[Test]
public void GetComponentData_DoesNotInstantiateMaterialsInEditMode()
{
// Arrange - Create a GameObject with MeshRenderer and MeshFilter components
var testObject = new GameObject("MaterialMeshTestObject");
var meshRenderer = testObject.AddComponent<MeshRenderer>();
var meshFilter = testObject.AddComponent<MeshFilter>();
// Create a simple material and mesh for testing
var testMaterial = new Material(Shader.Find("Standard"));
var tempCube = GameObject.CreatePrimitive(PrimitiveType.Cube);
var testMesh = tempCube.GetComponent<MeshFilter>().sharedMesh;
UnityEngine.Object.DestroyImmediate(tempCube);
// Set the shared material and mesh (these should be used in edit mode)
meshRenderer.sharedMaterial = testMaterial;
meshFilter.sharedMesh = testMesh;
// Act - Get component data which should trigger material/mesh property access
var prevIgnore = LogAssert.ignoreFailingMessages;
LogAssert.ignoreFailingMessages = true; // Avoid failing due to incidental editor logs during reflection
object result;
try
{
result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshRenderer);
}
finally
{
LogAssert.ignoreFailingMessages = prevIgnore;
}
// Assert - Basic success and shape tolerance
Assert.IsNotNull(result, "GetComponentData should return a result");
if (result is Dictionary<string, object> dict &&
dict.TryGetValue("properties", out var propsObj) &&
propsObj is Dictionary<string, object> properties)
{
Assert.IsTrue(properties.ContainsKey("material") || properties.ContainsKey("sharedMaterial") || properties.ContainsKey("materials") || properties.ContainsKey("sharedMaterials"),
"Serialized data should include a material-related key when present.");
}
// Clean up
UnityEngine.Object.DestroyImmediate(testMaterial);
UnityEngine.Object.DestroyImmediate(testObject);
}
[Test]
public void GetComponentData_DoesNotInstantiateMeshesInEditMode()
{
// Arrange - Create a GameObject with MeshFilter component
var testObject = new GameObject("MeshTestObject");
var meshFilter = testObject.AddComponent<MeshFilter>();
// Create a simple mesh for testing
var tempSphere = GameObject.CreatePrimitive(PrimitiveType.Sphere);
var testMesh = tempSphere.GetComponent<MeshFilter>().sharedMesh;
UnityEngine.Object.DestroyImmediate(tempSphere);
meshFilter.sharedMesh = testMesh;
// Act - Get component data which should trigger mesh property access
var prevIgnore2 = LogAssert.ignoreFailingMessages;
LogAssert.ignoreFailingMessages = true;
object result;
try
{
result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshFilter);
}
finally
{
LogAssert.ignoreFailingMessages = prevIgnore2;
}
// Assert - Basic success and shape tolerance
Assert.IsNotNull(result, "GetComponentData should return a result");
if (result is Dictionary<string, object> dict2 &&
dict2.TryGetValue("properties", out var propsObj2) &&
propsObj2 is Dictionary<string, object> properties2)
{
Assert.IsTrue(properties2.ContainsKey("mesh") || properties2.ContainsKey("sharedMesh"),
"Serialized data should include a mesh-related key when present.");
}
// Clean up
UnityEngine.Object.DestroyImmediate(testObject);
}
[Test]
public void GetComponentData_UsesSharedMaterialInEditMode()
{
// Arrange - Create a GameObject with MeshRenderer
var testObject = new GameObject("SharedMaterialTestObject");
var meshRenderer = testObject.AddComponent<MeshRenderer>();
// Create a test material
var testMaterial = new Material(Shader.Find("Standard"));
testMaterial.name = "TestMaterial";
meshRenderer.sharedMaterial = testMaterial;
// Act - Get component data in edit mode
var result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshRenderer);
// Assert - Verify that the material property was accessed without instantiation
Assert.IsNotNull(result, "GetComponentData should return a result");
// Check that result is a dictionary with properties key
if (result is Dictionary<string, object> resultDict &&
resultDict.TryGetValue("properties", out var propertiesObj) &&
propertiesObj is Dictionary<string, object> properties)
{
Assert.IsTrue(properties.ContainsKey("material") || properties.ContainsKey("sharedMaterial"),
"Serialized data should include 'material' or 'sharedMaterial' when present.");
}
// Clean up
UnityEngine.Object.DestroyImmediate(testMaterial);
UnityEngine.Object.DestroyImmediate(testObject);
}
[Test]
public void GetComponentData_UsesSharedMeshInEditMode()
{
// Arrange - Create a GameObject with MeshFilter
var testObject = new GameObject("SharedMeshTestObject");
var meshFilter = testObject.AddComponent<MeshFilter>();
// Create a test mesh
var tempCylinder = GameObject.CreatePrimitive(PrimitiveType.Cylinder);
var testMesh = tempCylinder.GetComponent<MeshFilter>().sharedMesh;
UnityEngine.Object.DestroyImmediate(tempCylinder);
testMesh.name = "TestMesh";
meshFilter.sharedMesh = testMesh;
// Act - Get component data in edit mode
var result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshFilter);
// Assert - Verify that the mesh property was accessed without instantiation
Assert.IsNotNull(result, "GetComponentData should return a result");
// Check that result is a dictionary with properties key
if (result is Dictionary<string, object> resultDict &&
resultDict.TryGetValue("properties", out var propertiesObj) &&
propertiesObj is Dictionary<string, object> properties)
{
Assert.IsTrue(properties.ContainsKey("mesh") || properties.ContainsKey("sharedMesh"),
"Serialized data should include 'mesh' or 'sharedMesh' when present.");
}
// Clean up
UnityEngine.Object.DestroyImmediate(testObject);
}
[Test]
public void GetComponentData_HandlesNullMaterialsAndMeshes()
{
// Arrange - Create a GameObject with MeshRenderer and MeshFilter but no materials/meshes
var testObject = new GameObject("NullMaterialMeshTestObject");
var meshRenderer = testObject.AddComponent<MeshRenderer>();
var meshFilter = testObject.AddComponent<MeshFilter>();
// Don't set any materials or meshes - they should be null
// Act - Get component data
var rendererResult = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshRenderer);
var meshFilterResult = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshFilter);
// Assert - Verify that the operations succeeded even with null materials/meshes
Assert.IsNotNull(rendererResult, "GetComponentData should handle null materials");
Assert.IsNotNull(meshFilterResult, "GetComponentData should handle null meshes");
// Clean up
UnityEngine.Object.DestroyImmediate(testObject);
}
[Test]
public void GetComponentData_WorksWithMultipleMaterials()
{
// Arrange - Create a GameObject with MeshRenderer that has multiple materials
var testObject = new GameObject("MultiMaterialTestObject");
var meshRenderer = testObject.AddComponent<MeshRenderer>();
// Create multiple test materials
var material1 = new Material(Shader.Find("Standard"));
material1.name = "TestMaterial1";
var material2 = new Material(Shader.Find("Standard"));
material2.name = "TestMaterial2";
meshRenderer.sharedMaterials = new Material[] { material1, material2 };
// Act - Get component data
var result = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(meshRenderer);
// Assert - Verify that the operation succeeded with multiple materials
Assert.IsNotNull(result, "GetComponentData should handle multiple materials");
// Clean up
UnityEngine.Object.DestroyImmediate(material1);
UnityEngine.Object.DestroyImmediate(material2);
UnityEngine.Object.DestroyImmediate(testObject);
}
#region Prefab Asset Handling Tests
[Test]
public void HandleCommand_WithPrefabPath_ReturnsGuidanceError_ForModifyAction()
{
// Arrange - Attempt to modify a prefab asset directly
var modifyParams = new JObject
{
["action"] = "modify",
["target"] = "Assets/Prefabs/MyPrefab.prefab"
};
// Act
var result = ManageGameObject.HandleCommand(modifyParams);
// Assert - Should return an error with guidance to use correct tools
Assert.IsNotNull(result, "Should return a result");
var errorResponse = result as MCPForUnity.Editor.Helpers.ErrorResponse;
Assert.IsNotNull(errorResponse, "Should return an ErrorResponse");
Assert.IsFalse(errorResponse.Success, "Should indicate failure");
Assert.That(errorResponse.Error, Does.Contain("prefab asset"), "Error should mention prefab asset");
Assert.That(errorResponse.Error, Does.Contain("manage_asset"), "Error should guide to manage_asset");
Assert.That(errorResponse.Error, Does.Contain("manage_prefabs"), "Error should guide to manage_prefabs");
}
[Test]
public void HandleCommand_WithPrefabPath_ReturnsGuidanceError_ForDeleteAction()
{
// Arrange - Attempt to delete a prefab asset directly
var deleteParams = new JObject
{
["action"] = "delete",
["target"] = "Assets/Prefabs/SomePrefab.prefab"
};
// Act
var result = ManageGameObject.HandleCommand(deleteParams);
// Assert - Should return an error with guidance
Assert.IsNotNull(result, "Should return a result");
var errorResponse = result as MCPForUnity.Editor.Helpers.ErrorResponse;
Assert.IsNotNull(errorResponse, "Should return an ErrorResponse");
Assert.IsFalse(errorResponse.Success, "Should indicate failure");
Assert.That(errorResponse.Error, Does.Contain("prefab asset"), "Error should mention prefab asset");
}
[Test]
public void HandleCommand_WithPrefabPath_AllowsCreateAction()
{
// Arrange - Create (instantiate) from a prefab should be allowed
// Note: This will fail because the prefab doesn't exist, but the error should NOT be
// the prefab redirection error - it should be a "prefab not found" type error
var createParams = new JObject
{
["action"] = "create",
["prefab_path"] = "Assets/Prefabs/NonExistent.prefab",
["name"] = "TestInstance"
};
// Act
var result = ManageGameObject.HandleCommand(createParams);
// Assert - Should NOT return the prefab redirection error
// (It may fail for other reasons like prefab not found, but not due to redirection)
var errorResponse = result as MCPForUnity.Editor.Helpers.ErrorResponse;
if (errorResponse != null)
{
// If there's an error, it should NOT be the prefab asset guidance error
Assert.That(errorResponse.Error, Does.Not.Contain("Use 'manage_asset'"),
"Create action should not be blocked by prefab check");
}
// If it's not an error, that's also fine (means create was allowed)
}
#endregion
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5931268353eab4ea5baa054e6231e824
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,697 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Tools.Graphics;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
public class ManageGraphicsTests
{
private const string TempRoot = "Assets/Temp/ManageGraphicsTests";
private bool _hasVolumeSystem;
private bool _hasURP;
private bool _hasHDRP;
private bool _hasSceneView;
[SetUp]
public void SetUp()
{
EnsureFolder(TempRoot);
var pingResult = ToJObject(ManageGraphics.HandleCommand(
new JObject { ["action"] = "ping" }));
if (pingResult.Value<bool>("success"))
{
var data = pingResult["data"];
_hasVolumeSystem = data?.Value<bool>("hasVolumeSystem") ?? false;
_hasURP = data?.Value<bool>("hasURP") ?? false;
_hasHDRP = data?.Value<bool>("hasHDRP") ?? false;
}
_hasSceneView = UnityEditor.SceneView.lastActiveSceneView != null;
}
[TearDown]
public void TearDown()
{
#if UNITY_2022_2_OR_NEWER
foreach (var go in UnityEngine.Object.FindObjectsByType<GameObject>(FindObjectsSortMode.None))
#else
foreach (var go in UnityEngine.Object.FindObjectsOfType<GameObject>())
#endif
{
if (go.name.StartsWith("GfxTest_"))
UnityEngine.Object.DestroyImmediate(go);
}
if (AssetDatabase.IsValidFolder(TempRoot))
AssetDatabase.DeleteAsset(TempRoot);
CleanupEmptyParentFolders(TempRoot);
// Reset scene debug mode
ManageGraphics.HandleCommand(new JObject
{
["action"] = "stats_set_scene_debug",
["mode"] = "Textured"
});
}
// =====================================================================
// Dispatch / Error Handling
// =====================================================================
[Test]
public void HandleCommand_NullParams_ReturnsError()
{
var result = ToJObject(ManageGraphics.HandleCommand(null));
Assert.IsFalse(result.Value<bool>("success"));
}
[Test]
public void HandleCommand_MissingAction_ReturnsError()
{
var result = ToJObject(ManageGraphics.HandleCommand(new JObject()));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("action"));
}
[Test]
public void HandleCommand_UnknownAction_ReturnsError()
{
var result = ToJObject(ManageGraphics.HandleCommand(
new JObject { ["action"] = "bogus_action" }));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("Unknown action"));
}
[Test]
public void Ping_ReturnsPipelineInfo()
{
var result = ToJObject(ManageGraphics.HandleCommand(
new JObject { ["action"] = "ping" }));
Assert.IsTrue(result.Value<bool>("success"));
Assert.That(result["message"].ToString(), Does.Contain("Pipeline"));
var data = result["data"];
Assert.IsNotNull(data);
Assert.IsNotNull(data["pipeline"]);
Assert.IsNotNull(data["pipelineName"]);
}
// =====================================================================
// Volume Actions
// =====================================================================
private void AssumeVolumeSystem()
{
Assume.That(_hasVolumeSystem, "Volume system not available — skipping.");
}
[Test]
public void VolumeCreate_Global_CreatesVolume()
{
AssumeVolumeSystem();
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "volume_create",
["name"] = "GfxTest_Volume",
["is_global"] = true,
["priority"] = 10
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.IsTrue(result["data"]["isGlobal"].Value<bool>());
Assert.AreEqual(10, result["data"]["priority"].Value<int>());
}
[Test]
public void VolumeCreate_WithEffects_AddsEffects()
{
AssumeVolumeSystem();
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "volume_create",
["name"] = "GfxTest_VolumeEffects",
["effects"] = new JArray
{
new JObject { ["type"] = "Bloom", ["intensity"] = 2 },
new JObject { ["type"] = "Vignette", ["intensity"] = 0.5 }
}
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var effects = result["data"]["effects"] as JArray;
Assert.IsNotNull(effects);
Assert.AreEqual(2, effects.Count);
}
[Test]
public void VolumeCreate_Local_CreatesNonGlobal()
{
AssumeVolumeSystem();
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "volume_create",
["name"] = "GfxTest_LocalVol",
["is_global"] = false
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.IsFalse(result["data"]["isGlobal"].Value<bool>());
}
[Test]
public void VolumeAddEffect_AddsEffect()
{
AssumeVolumeSystem();
CreateTestVolume("GfxTest_AddFx");
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "volume_add_effect",
["target"] = "GfxTest_AddFx",
["effect"] = "Bloom",
["parameters"] = new JObject { ["intensity"] = 3 }
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.AreEqual("Bloom", result["data"]["effect"].ToString());
}
[Test]
public void VolumeAddEffect_Duplicate_ReturnsError()
{
AssumeVolumeSystem();
CreateTestVolume("GfxTest_DupFx");
ManageGraphics.HandleCommand(new JObject
{
["action"] = "volume_add_effect",
["target"] = "GfxTest_DupFx",
["effect"] = "Bloom"
});
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "volume_add_effect",
["target"] = "GfxTest_DupFx",
["effect"] = "Bloom"
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("already exists"));
}
[Test]
public void VolumeAddEffect_InvalidEffect_ReturnsError()
{
AssumeVolumeSystem();
CreateTestVolume("GfxTest_BadFx");
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "volume_add_effect",
["target"] = "GfxTest_BadFx",
["effect"] = "FakeEffect"
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("not found"));
}
[Test]
public void VolumeSetEffect_SetsParameters()
{
AssumeVolumeSystem();
CreateTestVolume("GfxTest_SetFx");
ManageGraphics.HandleCommand(new JObject
{
["action"] = "volume_add_effect",
["target"] = "GfxTest_SetFx",
["effect"] = "Bloom"
});
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "volume_set_effect",
["target"] = "GfxTest_SetFx",
["effect"] = "Bloom",
["parameters"] = new JObject { ["intensity"] = 5, ["scatter"] = 0.8 }
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var setParams = result["data"]["set"] as JArray;
Assert.IsNotNull(setParams);
Assert.That(setParams.Select(t => t.ToString()), Contains.Item("intensity"));
Assert.That(setParams.Select(t => t.ToString()), Contains.Item("scatter"));
}
[Test]
public void VolumeSetEffect_InvalidParam_ReportsFailed()
{
AssumeVolumeSystem();
CreateTestVolume("GfxTest_BadParam");
ManageGraphics.HandleCommand(new JObject
{
["action"] = "volume_add_effect",
["target"] = "GfxTest_BadParam",
["effect"] = "Bloom"
});
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "volume_set_effect",
["target"] = "GfxTest_BadParam",
["effect"] = "Bloom",
["parameters"] = new JObject { ["nonExistent"] = 42 }
}));
Assert.IsTrue(result.Value<bool>("success"));
var failed = result["data"]["failed"] as JArray;
Assert.IsNotNull(failed);
Assert.That(failed.Select(t => t.ToString()), Contains.Item("nonExistent"));
}
[Test]
public void VolumeRemoveEffect_RemovesEffect()
{
AssumeVolumeSystem();
CreateTestVolume("GfxTest_RmFx");
ManageGraphics.HandleCommand(new JObject
{
["action"] = "volume_add_effect",
["target"] = "GfxTest_RmFx",
["effect"] = "Vignette"
});
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "volume_remove_effect",
["target"] = "GfxTest_RmFx",
["effect"] = "Vignette"
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
// Verify it's gone
var info = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "volume_get_info",
["target"] = "GfxTest_RmFx"
}));
var effects = info["data"]["effects"] as JArray;
Assert.IsNotNull(effects);
Assert.AreEqual(0, effects.Count);
}
[Test]
public void VolumeRemoveEffect_NonExistent_ReturnsError()
{
AssumeVolumeSystem();
CreateTestVolume("GfxTest_RmMissing");
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "volume_remove_effect",
["target"] = "GfxTest_RmMissing",
["effect"] = "DepthOfField"
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("not found"));
}
[Test]
public void VolumeGetInfo_ReturnsEffectList()
{
AssumeVolumeSystem();
CreateTestVolume("GfxTest_Info");
ManageGraphics.HandleCommand(new JObject
{
["action"] = "volume_add_effect",
["target"] = "GfxTest_Info",
["effect"] = "Bloom"
});
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "volume_get_info",
["target"] = "GfxTest_Info"
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"];
Assert.AreEqual("GfxTest_Info", data["name"].ToString());
var effects = data["effects"] as JArray;
Assert.IsNotNull(effects);
Assert.AreEqual(1, effects.Count);
Assert.AreEqual("Bloom", effects[0]["type"].ToString());
}
[Test]
public void VolumeGetInfo_NonExistentTarget_ReturnsError()
{
AssumeVolumeSystem();
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "volume_get_info",
["target"] = "NonExistentVolume"
}));
Assert.IsFalse(result.Value<bool>("success"));
}
[Test]
public void VolumeSetProperties_UpdatesWeightAndPriority()
{
AssumeVolumeSystem();
CreateTestVolume("GfxTest_Props");
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "volume_set_properties",
["target"] = "GfxTest_Props",
["properties"] = new JObject { ["weight"] = 0.5, ["priority"] = 20 }
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var changed = result["data"]["changed"] as JArray;
Assert.IsNotNull(changed);
Assert.That(changed.Select(t => t.ToString()), Contains.Item("weight"));
Assert.That(changed.Select(t => t.ToString()), Contains.Item("priority"));
// Verify via get_info
var info = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "volume_get_info",
["target"] = "GfxTest_Props"
}));
Assert.AreEqual(0.5f, info["data"]["weight"].Value<float>(), 0.01f);
Assert.AreEqual(20f, info["data"]["priority"].Value<float>(), 0.01f);
}
[Test]
public void VolumeListEffects_ReturnsAvailableTypes()
{
AssumeVolumeSystem();
var result = ToJObject(ManageGraphics.HandleCommand(
new JObject { ["action"] = "volume_list_effects" }));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var effects = result["data"]["effects"] as JArray;
Assert.IsNotNull(effects);
Assert.Greater(effects.Count, 0);
Assert.IsNotNull(effects[0]["name"]);
}
[Test]
public void VolumeCreateProfile_CreatesAsset()
{
AssumeVolumeSystem();
string path = $"{TempRoot}/TestProfile";
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "volume_create_profile",
["path"] = path
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
string fullPath = result["data"]["path"].ToString();
Assert.IsTrue(fullPath.EndsWith(".asset"));
Assert.IsNotNull(AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(fullPath));
}
// =====================================================================
// Bake Actions
// =====================================================================
[Test]
public void BakeGetSettings_ReturnsLightmapperInfo()
{
var result = ToJObject(ManageGraphics.HandleCommand(
new JObject { ["action"] = "bake_get_settings" }));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"];
Assert.IsNotNull(data["lightmapper"]);
Assert.IsNotNull(data["lightmapResolution"]);
}
[Test]
public void BakeSetSettings_ChangesAndRestores()
{
// Read original
var original = ToJObject(ManageGraphics.HandleCommand(
new JObject { ["action"] = "bake_get_settings" }));
int origResolution = original["data"]["lightmapResolution"].Value<int>();
// Change
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "bake_set_settings",
["settings"] = new JObject { ["lightmapResolution"] = 20 }
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var changed = result["data"]["changed"] as JArray;
Assert.That(changed.Select(t => t.ToString()), Contains.Item("lightmapResolution"));
// Verify
var verify = ToJObject(ManageGraphics.HandleCommand(
new JObject { ["action"] = "bake_get_settings" }));
Assert.AreEqual(20, verify["data"]["lightmapResolution"].Value<int>());
// Restore
ManageGraphics.HandleCommand(new JObject
{
["action"] = "bake_set_settings",
["settings"] = new JObject { ["lightmapResolution"] = origResolution }
});
}
[Test]
public void BakeStatus_ReportsNotRunning()
{
var result = ToJObject(ManageGraphics.HandleCommand(
new JObject { ["action"] = "bake_status" }));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.IsNotNull(result["data"]["isRunning"]);
}
[Test]
public void BakeClear_Succeeds()
{
var result = ToJObject(ManageGraphics.HandleCommand(
new JObject { ["action"] = "bake_clear" }));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
}
[Test]
public void BakeCreateReflectionProbe_CreatesProbe()
{
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "bake_create_reflection_probe",
["name"] = "GfxTest_ReflProbe",
["position"] = new JArray(0, 1, 0),
["size"] = new JArray(10, 10, 10),
["resolution"] = 128
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var go = GameObject.Find("GfxTest_ReflProbe");
Assert.IsNotNull(go);
Assert.IsNotNull(go.GetComponent<ReflectionProbe>());
}
[Test]
public void BakeCreateLightProbeGroup_CreatesGrid()
{
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "bake_create_light_probe_group",
["name"] = "GfxTest_LPGroup",
["position"] = new JArray(0, 0, 0),
["grid_size"] = new JArray(2, 2, 2),
["spacing"] = 2
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.AreEqual(8, result["data"]["probeCount"].Value<int>());
var go = GameObject.Find("GfxTest_LPGroup");
Assert.IsNotNull(go);
Assert.IsNotNull(go.GetComponent<LightProbeGroup>());
}
[Test]
public void BakeSetProbePositions_SetsPositions()
{
ManageGraphics.HandleCommand(new JObject
{
["action"] = "bake_create_light_probe_group",
["name"] = "GfxTest_LPPos",
["grid_size"] = new JArray(1, 1, 1)
});
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "bake_set_probe_positions",
["target"] = "GfxTest_LPPos",
["positions"] = new JArray
{
new JArray(0, 0, 0),
new JArray(1, 0, 0),
new JArray(0, 1, 0)
}
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.AreEqual(3, result["data"]["probeCount"].Value<int>());
}
[Test]
public void BakeSetProbePositions_WrongComponent_ReturnsError()
{
var go = new GameObject("GfxTest_NoProbe");
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "bake_set_probe_positions",
["target"] = "GfxTest_NoProbe",
["positions"] = new JArray { new JArray(0, 0, 0) }
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("LightProbeGroup"));
}
// =====================================================================
// Stats Actions
// =====================================================================
[Test]
public void StatsGet_ReturnsCounters()
{
var result = ToJObject(ManageGraphics.HandleCommand(
new JObject { ["action"] = "stats_get" }));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.IsNotNull(result["data"]["draw_calls"]);
Assert.IsNotNull(result["data"]["batches"]);
Assert.IsNotNull(result["data"]["triangles"]);
}
[Test]
public void StatsListCounters_ReturnsList()
{
var result = ToJObject(ManageGraphics.HandleCommand(
new JObject { ["action"] = "stats_list_counters" }));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var counters = result["data"]["counters"] as JArray;
Assert.IsNotNull(counters);
Assert.Greater(counters.Count, 0);
}
[Test]
public void StatsGetMemory_ReturnsMemoryInfo()
{
var result = ToJObject(ManageGraphics.HandleCommand(
new JObject { ["action"] = "stats_get_memory" }));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.IsNotNull(result["data"]["totalAllocatedMB"]);
Assert.IsNotNull(result["data"]["graphicsDriverMB"]);
}
[Test]
public void StatsSetSceneDebug_ValidMode_Succeeds()
{
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "stats_set_scene_debug",
["mode"] = "Wireframe"
}));
if (_hasSceneView)
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
else
Assert.IsFalse(result.Value<bool>("success"));
}
[Test]
public void StatsSetSceneDebug_InvalidMode_ReturnsError()
{
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "stats_set_scene_debug",
["mode"] = "InvalidMode"
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("Valid:"));
}
// =====================================================================
// Pipeline Actions
// =====================================================================
[Test]
public void PipelineGetInfo_ReturnsPipelineName()
{
var result = ToJObject(ManageGraphics.HandleCommand(
new JObject { ["action"] = "pipeline_get_info" }));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.IsNotNull(result["data"]["pipelineName"]);
Assert.IsNotNull(result["data"]["qualityLevelName"]);
}
[Test]
public void PipelineGetSettings_ReturnsSettings()
{
Assume.That(_hasURP || _hasHDRP, "Built-in pipeline has no settings asset — skipping.");
var result = ToJObject(ManageGraphics.HandleCommand(
new JObject { ["action"] = "pipeline_get_settings" }));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var settings = result["data"]["settings"];
Assert.IsNotNull(settings);
Assert.IsNotNull(settings["renderScale"]);
}
[Test]
public void PipelineSetQuality_InvalidLevel_ReturnsError()
{
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "pipeline_set_quality",
["level"] = "NonExistentLevel"
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("Available:"));
}
// =====================================================================
// Renderer Feature Actions (URP only)
// =====================================================================
private void AssumeURP()
{
Assume.That(_hasURP, "URP not available — skipping.");
}
[Test]
public void FeatureList_ReturnsFeatures()
{
AssumeURP();
var result = ToJObject(ManageGraphics.HandleCommand(
new JObject { ["action"] = "feature_list" }));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.IsNotNull(result["data"]["features"]);
Assert.IsNotNull(result["data"]["rendererDataName"]);
}
[Test]
public void FeatureAdd_InvalidType_ReturnsError()
{
AssumeURP();
var result = ToJObject(ManageGraphics.HandleCommand(new JObject
{
["action"] = "feature_add",
["type"] = "NonExistentFeature"
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("not found"));
Assert.That(result["error"].ToString(), Does.Contain("Available:"));
}
// =====================================================================
// Helpers
// =====================================================================
private void CreateTestVolume(string name)
{
ManageGraphics.HandleCommand(new JObject
{
["action"] = "volume_create",
["name"] = name
});
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9f7dd666649bd48049711444cb73ab7f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,162 @@
using System;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Tools;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
public class ManageMaterialPropertiesTests
{
private const string TempRoot = "Assets/Temp/ManageMaterialPropertiesTests";
private string _matPath;
// Built-in's lit shader (Standard) uses "_Color" as its main color property,
// while URP/HDRP lit shaders use "_BaseColor". The tool aliases shader "Standard"
// to the pipeline-appropriate lit shader, so the color property name must match.
private static string MainColorProperty()
{
return RenderPipelineUtility.GetActivePipeline() == RenderPipelineUtility.PipelineKind.BuiltIn
? "_Color"
: "_BaseColor";
}
[SetUp]
public void SetUp()
{
if (!AssetDatabase.IsValidFolder("Assets/Temp"))
{
AssetDatabase.CreateFolder("Assets", "Temp");
}
if (!AssetDatabase.IsValidFolder(TempRoot))
{
AssetDatabase.CreateFolder("Assets/Temp", "ManageMaterialPropertiesTests");
}
_matPath = $"{TempRoot}/PropTest.mat";
}
[TearDown]
public void TearDown()
{
if (AssetDatabase.IsValidFolder(TempRoot))
{
AssetDatabase.DeleteAsset(TempRoot);
}
// Clean up empty parent folders to avoid debris
CleanupEmptyParentFolders(TempRoot);
}
[Test]
public void CreateMaterial_WithValidJsonStringArray_SetsProperty()
{
string colorProp = MainColorProperty();
string jsonProps = "{\"" + colorProp + "\": [1.0, 0.0, 0.0, 1.0]}";
var paramsObj = new JObject
{
["action"] = "create",
["materialPath"] = _matPath,
["shader"] = "Standard",
["properties"] = jsonProps
};
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var mat = AssetDatabase.LoadAssetAtPath<Material>(_matPath);
Assert.AreEqual(Color.red, mat.GetColor(colorProp));
}
[Test]
public void CreateMaterial_WithJObjectArray_SetsProperty()
{
string colorProp = MainColorProperty();
var props = new JObject();
props[colorProp] = new JArray(0.0f, 1.0f, 0.0f, 1.0f);
var paramsObj = new JObject
{
["action"] = "create",
["materialPath"] = _matPath,
["shader"] = "Standard",
["properties"] = props
};
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var mat = AssetDatabase.LoadAssetAtPath<Material>(_matPath);
Assert.AreEqual(Color.green, mat.GetColor(colorProp));
}
[Test]
public void CreateMaterial_WithEmptyProperties_Succeeds()
{
var paramsObj = new JObject
{
["action"] = "create",
["materialPath"] = _matPath,
["shader"] = "Standard",
["properties"] = new JObject()
};
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
}
[Test]
public void CreateMaterial_WithInvalidJsonSyntax_ReturnsDetailedError()
{
// Missing closing brace
string invalidJson = "{\"_Color\": [1,0,0,1]";
var paramsObj = new JObject
{
["action"] = "create",
["materialPath"] = _matPath,
["shader"] = "Standard",
["properties"] = invalidJson
};
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
Assert.IsFalse(result.Value<bool>("success"));
string msg = result.Value<string>("error");
// Verify we get exception details
Assert.IsTrue(msg.Contains("Invalid JSON"), "Should mention Invalid JSON");
// Verify the message contains more than just the prefix (has exception details)
Assert.IsTrue(msg.Length > "Invalid JSON".Length,
$"Message should contain exception details. Got: {msg}");
}
[Test]
public void CreateMaterial_WithNullProperty_HandlesGracefully()
{
var props = new JObject();
props["_Color"] = null;
var paramsObj = new JObject
{
["action"] = "create",
["materialPath"] = _matPath,
["shader"] = "Standard",
["properties"] = props
};
// Should probably succeed but warn or ignore, or fail gracefully
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
// We accept either success (ignored) or specific error, but not crash
// The new response format uses a bool "success" field
var success = result.Value<bool?>("success");
Assert.IsNotNull(success, "Response should have success field");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ca019b5c6c1ee4e13b77574f2ae53583
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,72 @@
using System;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Tools;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
public class ManageMaterialReproTests
{
private const string TempRoot = "Assets/Temp/ManageMaterialReproTests";
private string _matPath;
[SetUp]
public void SetUp()
{
if (!AssetDatabase.IsValidFolder("Assets/Temp"))
{
AssetDatabase.CreateFolder("Assets", "Temp");
}
if (!AssetDatabase.IsValidFolder(TempRoot))
{
AssetDatabase.CreateFolder("Assets/Temp", "ManageMaterialReproTests");
}
string guid = Guid.NewGuid().ToString("N");
_matPath = $"{TempRoot}/ReproMat_{guid}.mat";
}
[TearDown]
public void TearDown()
{
if (AssetDatabase.IsValidFolder(TempRoot))
{
AssetDatabase.DeleteAsset(TempRoot);
}
// Clean up empty parent folders to avoid debris
CleanupEmptyParentFolders(TempRoot);
}
[Test]
public void CreateMaterial_WithInvalidJsonString_ReturnsGenericError()
{
// Arrange
// Malformed JSON string (missing closing brace)
string invalidJson = "{\"_Color\": [1,0,0,1]";
var paramsObj = new JObject
{
["action"] = "create",
["materialPath"] = _matPath,
["shader"] = "Standard",
["properties"] = invalidJson
};
// Act
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
// Assert
Assert.IsFalse(result.Value<bool>("success"));
// We expect more detailed error message after fix
var message = result.Value<string>("error");
Assert.IsTrue(message.StartsWith("Invalid JSON in properties"), "Message should start with prefix");
Assert.AreNotEqual("Invalid JSON in properties", message, "Message should contain exception details");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c967207bf78c344178484efe6d87dea7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,181 @@
using System;
using System.IO;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Tools;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
public class ManageMaterialStressTests
{
private const string TempRoot = "Assets/Temp/ManageMaterialStressTests";
private string _matPath;
private GameObject _cube;
[SetUp]
public void SetUp()
{
if (!AssetDatabase.IsValidFolder("Assets/Temp"))
{
AssetDatabase.CreateFolder("Assets", "Temp");
}
if (!AssetDatabase.IsValidFolder(TempRoot))
{
AssetDatabase.CreateFolder("Assets/Temp", "ManageMaterialStressTests");
}
string guid = Guid.NewGuid().ToString("N");
_matPath = $"{TempRoot}/StressMat_{guid}.mat";
var material = new Material(Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard"));
material.color = Color.white;
AssetDatabase.CreateAsset(material, _matPath);
AssetDatabase.SaveAssets();
_cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
_cube.name = "StressCube";
}
[TearDown]
public void TearDown()
{
if (_cube != null)
{
UnityEngine.Object.DestroyImmediate(_cube);
}
if (AssetDatabase.IsValidFolder(TempRoot))
{
AssetDatabase.DeleteAsset(TempRoot);
}
// Clean up empty parent folders to avoid debris
CleanupEmptyParentFolders(TempRoot);
}
[Test]
public void HandleInvalidInputs_ReturnsError_NotException()
{
// 1. Bad path
var paramsBadPath = new JObject
{
["action"] = "set_material_color",
["materialPath"] = "Assets/NonExistent/Ghost.mat",
["color"] = new JArray(1f, 0f, 0f, 1f)
};
var resultBadPath = ToJObject(ManageMaterial.HandleCommand(paramsBadPath));
Assert.IsFalse(resultBadPath.Value<bool>("success"));
StringAssert.Contains("Could not find material", resultBadPath.Value<string>("error"));
// 2. Bad color array (too short)
var paramsBadColor = new JObject
{
["action"] = "set_material_color",
["materialPath"] = _matPath,
["color"] = new JArray(1f) // Invalid
};
var resultBadColor = ToJObject(ManageMaterial.HandleCommand(paramsBadColor));
Assert.IsFalse(resultBadColor.Value<bool>("success"));
StringAssert.Contains("Invalid color format", resultBadColor.Value<string>("error"));
// 3. Bad slot index
// Assign material first
var renderer = _cube.GetComponent<Renderer>();
renderer.sharedMaterial = AssetDatabase.LoadAssetAtPath<Material>(_matPath);
var paramsBadSlot = new JObject
{
["action"] = "assign_material_to_renderer",
["target"] = "StressCube",
["searchMethod"] = "by_name",
["materialPath"] = _matPath,
["slot"] = 99
};
var resultBadSlot = ToJObject(ManageMaterial.HandleCommand(paramsBadSlot));
Assert.IsFalse(resultBadSlot.Value<bool>("success"));
StringAssert.Contains("out of bounds", resultBadSlot.Value<string>("error"));
}
[Test]
public void StateIsolation_PropertyBlockDoesNotLeakToSharedMaterial()
{
// Arrange
var renderer = _cube.GetComponent<Renderer>();
var sharedMat = AssetDatabase.LoadAssetAtPath<Material>(_matPath);
renderer.sharedMaterial = sharedMat;
// Initial color
var initialColor = Color.white;
if (sharedMat.HasProperty("_BaseColor")) sharedMat.SetColor("_BaseColor", initialColor);
else if (sharedMat.HasProperty("_Color")) sharedMat.SetColor("_Color", initialColor);
// Act - Set Property Block Color
var blockColor = Color.red;
var paramsObj = new JObject
{
["action"] = "set_renderer_color",
["target"] = "StressCube",
["searchMethod"] = "by_name",
["color"] = new JArray(blockColor.r, blockColor.g, blockColor.b, blockColor.a),
["mode"] = "property_block"
};
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
// Assert
// 1. Renderer has property block with Red
var block = new MaterialPropertyBlock();
renderer.GetPropertyBlock(block, 0);
var propName = sharedMat.HasProperty("_BaseColor") ? "_BaseColor" : "_Color";
Assert.AreEqual(blockColor, block.GetColor(propName));
// 2. Shared material remains White
var sharedColor = sharedMat.GetColor(propName);
Assert.AreEqual(initialColor, sharedColor, "Shared material color should NOT change when using PropertyBlock");
}
[Test]
public void Integration_PureManageMaterial_AssignsMaterialAndModifies()
{
// This simulates a workflow where we create a GO, assign a mat, then tweak it.
// 1. Create GO (already done in Setup, but let's verify)
Assert.IsNotNull(_cube);
// 2. Assign Material using ManageMaterial
var assignParams = new JObject
{
["action"] = "assign_material_to_renderer",
["target"] = "StressCube",
["searchMethod"] = "by_name",
["materialPath"] = _matPath
};
var assignResult = ToJObject(ManageMaterial.HandleCommand(assignParams));
Assert.IsTrue(assignResult.Value<bool>("success"), assignResult.ToString());
// Verify assignment
var renderer = _cube.GetComponent<Renderer>();
Assert.AreEqual(Path.GetFileNameWithoutExtension(_matPath), renderer.sharedMaterial.name);
// 3. Modify Shared Material Color using ManageMaterial
var newColor = Color.blue;
var colorParams = new JObject
{
["action"] = "set_material_color",
["materialPath"] = _matPath,
["color"] = new JArray(newColor.r, newColor.g, newColor.b, newColor.a)
};
var colorResult = ToJObject(ManageMaterial.HandleCommand(colorParams));
Assert.IsTrue(colorResult.Value<bool>("success"), colorResult.ToString());
// Verify color changed on renderer (because it's shared)
var propName = renderer.sharedMaterial.HasProperty("_BaseColor") ? "_BaseColor" : "_Color";
Assert.AreEqual(newColor, renderer.sharedMaterial.GetColor(propName));
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 49ecdd3f43cf54deea7508f317efcb45
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,223 @@
using System;
using System.IO;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Tools;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
public class ManageMaterialTests
{
private const string TempRoot = "Assets/Temp/ManageMaterialTests";
private string _matPath;
[SetUp]
public void SetUp()
{
if (!AssetDatabase.IsValidFolder("Assets/Temp"))
{
AssetDatabase.CreateFolder("Assets", "Temp");
}
if (!AssetDatabase.IsValidFolder(TempRoot))
{
AssetDatabase.CreateFolder("Assets/Temp", "ManageMaterialTests");
}
string guid = Guid.NewGuid().ToString("N");
_matPath = $"{TempRoot}/TestMat_{guid}.mat";
// Create a basic material
var material = new Material(Shader.Find("Universal Render Pipeline/Lit") ?? Shader.Find("Standard"));
AssetDatabase.CreateAsset(material, _matPath);
AssetDatabase.SaveAssets();
}
[TearDown]
public void TearDown()
{
if (AssetDatabase.IsValidFolder(TempRoot))
{
AssetDatabase.DeleteAsset(TempRoot);
}
// Clean up empty parent folders to avoid debris
CleanupEmptyParentFolders(TempRoot);
}
[Test]
public void SetMaterialShaderProperty_SetsColor()
{
// Arrange
var color = new Color(1f, 1f, 0f, 1f); // Yellow
var paramsObj = new JObject
{
["action"] = "set_material_shader_property",
["materialPath"] = _matPath,
["property"] = "_BaseColor", // URP
["value"] = new JArray(color.r, color.g, color.b, color.a)
};
// Check if using Standard shader (fallback)
var mat = AssetDatabase.LoadAssetAtPath<Material>(_matPath);
if (mat.shader.name == "Standard")
{
paramsObj["property"] = "_Color";
}
// Act
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
// Assert
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
mat = AssetDatabase.LoadAssetAtPath<Material>(_matPath); // Reload
var prop = mat.shader.name == "Standard" ? "_Color" : "_BaseColor";
Assert.IsTrue(mat.HasProperty(prop), $"Material should have property {prop}");
Assert.AreEqual(color, mat.GetColor(prop));
}
[Test]
public void SetMaterialColor_SetsColorWithFallback()
{
// Arrange
var color = new Color(0f, 1f, 0f, 1f); // Green
var paramsObj = new JObject
{
["action"] = "set_material_color",
["materialPath"] = _matPath,
["color"] = new JArray(color.r, color.g, color.b, color.a)
};
// Act
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
// Assert
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var mat = AssetDatabase.LoadAssetAtPath<Material>(_matPath);
var prop = mat.HasProperty("_BaseColor") ? "_BaseColor" : "_Color";
Assert.IsTrue(mat.HasProperty(prop), $"Material should have property {prop}");
Assert.AreEqual(color, mat.GetColor(prop));
}
[Test]
public void AssignMaterialToRenderer_Works()
{
// Arrange
var go = GameObject.CreatePrimitive(PrimitiveType.Cube);
go.name = "AssignTestCube";
try
{
var paramsObj = new JObject
{
["action"] = "assign_material_to_renderer",
["target"] = "AssignTestCube",
["searchMethod"] = "by_name",
["materialPath"] = _matPath,
["slot"] = 0
};
// Act
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
// Assert
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var renderer = go.GetComponent<Renderer>();
Assert.IsNotNull(renderer.sharedMaterial);
// Compare names because objects might be different instances (loaded vs scene)
var matName = Path.GetFileNameWithoutExtension(_matPath);
Assert.AreEqual(matName, renderer.sharedMaterial.name);
}
finally
{
UnityEngine.Object.DestroyImmediate(go);
}
}
[Test]
public void SetRendererColor_PropertyBlock_Works()
{
// Arrange
var go = GameObject.CreatePrimitive(PrimitiveType.Cube);
go.name = "BlockTestCube";
// Assign the material first so we have something valid
var mat = AssetDatabase.LoadAssetAtPath<Material>(_matPath);
go.GetComponent<Renderer>().sharedMaterial = mat;
try
{
var color = new Color(1f, 0f, 0f, 1f); // Red
var paramsObj = new JObject
{
["action"] = "set_renderer_color",
["target"] = "BlockTestCube",
["searchMethod"] = "by_name",
["color"] = new JArray(color.r, color.g, color.b, color.a),
["mode"] = "property_block"
};
// Act
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
// Assert
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var renderer = go.GetComponent<Renderer>();
var block = new MaterialPropertyBlock();
renderer.GetPropertyBlock(block, 0);
var prop = mat.HasProperty("_BaseColor") ? "_BaseColor" : "_Color";
Assert.AreEqual(color, block.GetColor(prop));
// Verify material asset didn't change (it was originally white/gray from setup?)
// We didn't check original color, but property block shouldn't affect shared material
// We can check that sharedMaterial color is NOT red if we set it to something else first
// But assuming test isolation, we can just verify the block is set.
}
finally
{
UnityEngine.Object.DestroyImmediate(go);
}
}
[Test]
public void GetMaterialInfo_ReturnsProperties()
{
// Arrange
var paramsObj = new JObject
{
["action"] = "get_material_info",
["materialPath"] = _matPath
};
// Act
var result = ToJObject(ManageMaterial.HandleCommand(paramsObj));
// Assert
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"] as JObject;
Assert.IsNotNull(data, "Response should have data object");
Assert.IsNotNull(data["properties"]);
Assert.IsInstanceOf<JArray>(data["properties"]);
var props = data["properties"] as JArray;
Assert.IsTrue(props.Count > 0);
// Check for standard properties
bool foundColor = false;
foreach(var p in props)
{
var name = p["name"]?.ToString();
if (name == "_Color" || name == "_BaseColor") foundColor = true;
}
Assert.IsTrue(foundColor, "Should find color property");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9f96e01f904e044608d97842c3a3cb43
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,974 @@
using System;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Tools.Physics;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
public class ManagePhysicsTests
{
private const string TempRoot = "Assets/Temp/ManagePhysicsTests";
[SetUp]
public void SetUp()
{
EnsureFolder(TempRoot);
}
[TearDown]
public void TearDown()
{
#if UNITY_2022_2_OR_NEWER
foreach (var go in UnityEngine.Object.FindObjectsByType<GameObject>(FindObjectsSortMode.None))
#else
foreach (var go in UnityEngine.Object.FindObjectsOfType<GameObject>())
#endif
{
if (go.name.StartsWith("PhysTest_"))
UnityEngine.Object.DestroyImmediate(go);
}
if (AssetDatabase.IsValidFolder(TempRoot))
AssetDatabase.DeleteAsset(TempRoot);
CleanupEmptyParentFolders(TempRoot);
}
// =====================================================================
// Dispatch / Error Handling
// =====================================================================
[Test]
public void HandleCommand_NullParams_ReturnsError()
{
var result = ToJObject(ManagePhysics.HandleCommand(null));
Assert.IsFalse(result.Value<bool>("success"));
}
[Test]
public void HandleCommand_MissingAction_ReturnsError()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject()));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("action"));
}
[Test]
public void HandleCommand_UnknownAction_ReturnsError()
{
var result = ToJObject(ManagePhysics.HandleCommand(
new JObject { ["action"] = "bogus_action" }));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("Unknown action"));
}
// =====================================================================
// Ping
// =====================================================================
[Test]
public void Ping_ReturnsPhysicsStatus()
{
var result = ToJObject(ManagePhysics.HandleCommand(
new JObject { ["action"] = "ping" }));
Assert.IsTrue(result.Value<bool>("success"));
Assert.That(result["message"].ToString(), Does.Contain("Physics"));
var data = result["data"];
Assert.IsNotNull(data);
Assert.IsNotNull(data["gravity3d"]);
Assert.IsNotNull(data["gravity2d"]);
Assert.IsNotNull(data["simulationMode"]);
Assert.IsNotNull(data["defaultSolverIterations"]);
Assert.IsNotNull(data["bounceThreshold"]);
Assert.IsNotNull(data["queriesHitTriggers"]);
}
// =====================================================================
// GetSettings
// =====================================================================
[Test]
public void GetSettings_3D_ReturnsGravity()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "get_settings",
["dimension"] = "3d"
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"];
Assert.AreEqual("3d", data["dimension"].ToString());
var gravity = data["gravity"] as JArray;
Assert.IsNotNull(gravity);
Assert.AreEqual(3, gravity.Count);
Assert.IsNotNull(data["defaultContactOffset"]);
Assert.IsNotNull(data["sleepThreshold"]);
Assert.IsNotNull(data["defaultSolverIterations"]);
Assert.IsNotNull(data["simulationMode"]);
}
[Test]
public void GetSettings_DefaultDimension_Returns3D()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "get_settings"
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.AreEqual("3d", result["data"]["dimension"].ToString());
}
[Test]
public void GetSettings_2D_ReturnsGravity()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "get_settings",
["dimension"] = "2d"
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"];
Assert.AreEqual("2d", data["dimension"].ToString());
var gravity = data["gravity"] as JArray;
Assert.IsNotNull(gravity);
Assert.AreEqual(2, gravity.Count);
Assert.IsNotNull(data["velocityIterations"]);
Assert.IsNotNull(data["positionIterations"]);
Assert.IsNotNull(data["queriesHitTriggers"]);
}
[Test]
public void GetSettings_InvalidDimension_ReturnsError()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "get_settings",
["dimension"] = "4d"
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("Invalid dimension"));
}
// =====================================================================
// SetSettings 3D
// =====================================================================
[Test]
public void SetSettings_3D_ChangesGravity()
{
// Save original
var originalGravity = UnityEngine.Physics.gravity;
try
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "set_settings",
["dimension"] = "3d",
["settings"] = new JObject
{
["gravity"] = new JArray(0, -20, 0)
}
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var changed = result["data"]["changed"] as JArray;
Assert.IsNotNull(changed);
Assert.That(changed.ToString(), Does.Contain("gravity"));
// Verify the change took effect
Assert.AreEqual(-20f, UnityEngine.Physics.gravity.y, 0.01f);
}
finally
{
// Restore original
UnityEngine.Physics.gravity = originalGravity;
}
}
[Test]
public void SetSettings_3D_ChangesSolverIterations()
{
var original = UnityEngine.Physics.defaultSolverIterations;
try
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "set_settings",
["dimension"] = "3d",
["settings"] = new JObject
{
["defaultSolverIterations"] = 12
}
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.AreEqual(12, UnityEngine.Physics.defaultSolverIterations);
}
finally
{
UnityEngine.Physics.defaultSolverIterations = original;
}
}
[Test]
public void SetSettings_3D_UnknownKey_ReturnsError()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "set_settings",
["dimension"] = "3d",
["settings"] = new JObject
{
["nonExistentSetting"] = 42
}
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("Unknown"));
Assert.That(result["error"].ToString(), Does.Contain("nonExistentSetting"));
}
[Test]
public void SetSettings_3D_InvalidGravityArray_ReturnsError()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "set_settings",
["dimension"] = "3d",
["settings"] = new JObject
{
["gravity"] = new JArray(0, -10) // Only 2 elements for 3D
}
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("[x, y, z]"));
}
[Test]
public void SetSettings_EmptySettings_ReturnsError()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "set_settings",
["dimension"] = "3d",
["settings"] = new JObject()
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("settings"));
}
[Test]
public void SetSettings_MissingSettings_ReturnsError()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "set_settings",
["dimension"] = "3d"
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("settings"));
}
// =====================================================================
// SetSettings 2D
// =====================================================================
[Test]
public void SetSettings_2D_ChangesGravity()
{
var originalGravity = Physics2D.gravity;
try
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "set_settings",
["dimension"] = "2d",
["settings"] = new JObject
{
["gravity"] = new JArray(0, -20)
}
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var changed = result["data"]["changed"] as JArray;
Assert.IsNotNull(changed);
Assert.That(changed.ToString(), Does.Contain("gravity"));
Assert.AreEqual(-20f, Physics2D.gravity.y, 0.01f);
}
finally
{
Physics2D.gravity = originalGravity;
}
}
[Test]
public void SetSettings_2D_UnknownKey_ReturnsError()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "set_settings",
["dimension"] = "2d",
["settings"] = new JObject
{
["fakeSetting"] = true
}
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("Unknown"));
Assert.That(result["error"].ToString(), Does.Contain("fakeSetting"));
}
[Test]
public void SetSettings_2D_InvalidGravityArray_ReturnsError()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "set_settings",
["dimension"] = "2d",
["settings"] = new JObject
{
["gravity"] = new JArray(0) // Only 1 element for 2D
}
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("[x, y]"));
}
// =====================================================================
// GetCollisionMatrix
// =====================================================================
[Test]
public void CreatePhysicsMaterial_3D_CreatesAsset()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "create_physics_material",
["name"] = "TestMat3D",
["path"] = TempRoot,
["dimension"] = "3d"
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
string expectedPath = TempRoot + "/TestMat3D.physicMaterial";
Assert.AreEqual(expectedPath, result["data"]["path"].ToString());
Assert.IsNotNull(AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(expectedPath),
"Asset should exist on disk.");
}
[Test]
public void CreatePhysicsMaterial_2D_CreatesAsset()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "create_physics_material",
["name"] = "TestMat2D",
["path"] = TempRoot,
["dimension"] = "2d"
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
string expectedPath = TempRoot + "/TestMat2D.physicsMaterial2D";
Assert.AreEqual(expectedPath, result["data"]["path"].ToString());
Assert.IsNotNull(AssetDatabase.LoadAssetAtPath<PhysicsMaterial2D>(expectedPath),
"2D physics material asset should exist on disk.");
}
[Test]
public void CreatePhysicsMaterial_MissingName_ReturnsError()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "create_physics_material",
["path"] = TempRoot,
["dimension"] = "3d"
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("name"));
}
// =====================================================================
// ConfigurePhysicsMaterial
// =====================================================================
[Test]
public void ConfigurePhysicsMaterial_3D_UpdatesProperties()
{
// Create material first
ManagePhysics.HandleCommand(new JObject
{
["action"] = "create_physics_material",
["name"] = "ConfigTest3D",
["path"] = TempRoot,
["dimension"] = "3d",
["bounciness"] = 0
});
string matPath = TempRoot + "/ConfigTest3D.physicMaterial";
// Configure it
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "configure_physics_material",
["path"] = matPath,
["dimension"] = "3d",
["properties"] = new JObject
{
["bounciness"] = 0.75
}
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
// Verify the property changed
#if UNITY_6000_0_OR_NEWER
var mat = AssetDatabase.LoadAssetAtPath<PhysicsMaterial>(matPath);
#else
var mat = AssetDatabase.LoadAssetAtPath<PhysicMaterial>(matPath);
#endif
Assert.IsNotNull(mat);
Assert.AreEqual(0.75f, mat.bounciness, 0.01f);
}
// =====================================================================
// AssignPhysicsMaterial
// =====================================================================
[Test]
public void AssignPhysicsMaterial_ToBoxCollider()
{
// Create a physics material
ManagePhysics.HandleCommand(new JObject
{
["action"] = "create_physics_material",
["name"] = "AssignTest3D",
["path"] = TempRoot,
["dimension"] = "3d"
});
string matPath = TempRoot + "/AssignTest3D.physicMaterial";
// Create a GameObject with a BoxCollider
var go = new GameObject("PhysTest_BoxCollider");
go.AddComponent<BoxCollider>();
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "assign_physics_material",
["target"] = "PhysTest_BoxCollider",
["material_path"] = matPath,
["search_method"] = "by_name"
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var collider = go.GetComponent<BoxCollider>();
Assert.IsNotNull(collider.sharedMaterial,
"BoxCollider.sharedMaterial should be set after assignment.");
}
[Test]
public void AssignPhysicsMaterial_NoCollider_ReturnsError()
{
// Create a physics material
ManagePhysics.HandleCommand(new JObject
{
["action"] = "create_physics_material",
["name"] = "AssignNoCollider",
["path"] = TempRoot,
["dimension"] = "3d"
});
string matPath = TempRoot + "/AssignNoCollider.physicMaterial";
// Create a GameObject WITHOUT any collider
var go = new GameObject("PhysTest_NoCollider");
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "assign_physics_material",
["target"] = "PhysTest_NoCollider",
["material_path"] = matPath,
["search_method"] = "by_name"
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("collider").IgnoreCase);
}
// =====================================================================
// AddJoint
// =====================================================================
[Test]
public void AddJoint_Hinge_RequiresRigidbody()
{
var go = new GameObject("PhysTest_NoRB");
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "add_joint",
["target"] = "PhysTest_NoRB",
["joint_type"] = "hinge"
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("Rigidbody"));
}
[Test]
public void AddJoint_Hinge_Success()
{
var go = new GameObject("PhysTest_HingeAdd");
go.AddComponent<Rigidbody>();
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "add_joint",
["target"] = "PhysTest_HingeAdd",
["joint_type"] = "hinge"
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.That(result["data"]["jointType"].ToString(), Is.EqualTo("HingeJoint"));
var hinge = go.GetComponent<HingeJoint>();
Assert.IsNotNull(hinge, "HingeJoint should be present on the GameObject.");
}
[Test]
public void AddJoint_UnknownType_ReturnsError()
{
var go = new GameObject("PhysTest_UnknownJoint");
go.AddComponent<Rigidbody>();
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "add_joint",
["target"] = "PhysTest_UnknownJoint",
["joint_type"] = "rubber_band"
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("Unknown"));
Assert.That(result["error"].ToString(), Does.Contain("rubber_band"));
}
// =====================================================================
// ConfigureJoint
// =====================================================================
[Test]
public void ConfigureJoint_Hinge_Motor()
{
var go = new GameObject("PhysTest_HingeMotor");
go.AddComponent<Rigidbody>();
go.AddComponent<HingeJoint>();
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "configure_joint",
["target"] = "PhysTest_HingeMotor",
["joint_type"] = "hinge",
["motor"] = new JObject
{
["targetVelocity"] = 90f,
["force"] = 50f,
["freeSpin"] = false
}
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var hinge = go.GetComponent<HingeJoint>();
Assert.IsTrue(hinge.useMotor, "useMotor should be true after motor config.");
Assert.AreEqual(90f, hinge.motor.targetVelocity, 0.01f);
Assert.AreEqual(50f, hinge.motor.force, 0.01f);
}
[Test]
public void ConfigureJoint_Hinge_Limits()
{
var go = new GameObject("PhysTest_HingeLimits");
go.AddComponent<Rigidbody>();
go.AddComponent<HingeJoint>();
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "configure_joint",
["target"] = "PhysTest_HingeLimits",
["joint_type"] = "hinge",
["limits"] = new JObject
{
["min"] = -45f,
["max"] = 45f,
["bounciness"] = 0.5f
}
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var hinge = go.GetComponent<HingeJoint>();
Assert.IsTrue(hinge.useLimits, "useLimits should be true after limits config.");
Assert.AreEqual(-45f, hinge.limits.min, 0.01f);
Assert.AreEqual(45f, hinge.limits.max, 0.01f);
Assert.AreEqual(0.5f, hinge.limits.bounciness, 0.01f);
}
// =====================================================================
// RemoveJoint
// =====================================================================
[Test]
public void RemoveJoint_Specific_Success()
{
var go = new GameObject("PhysTest_RemoveHinge");
go.AddComponent<Rigidbody>();
go.AddComponent<HingeJoint>();
Assert.IsNotNull(go.GetComponent<HingeJoint>());
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "remove_joint",
["target"] = "PhysTest_RemoveHinge",
["joint_type"] = "hinge"
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.AreEqual(1, result["data"]["removedCount"].Value<int>());
Assert.IsNull(go.GetComponent<HingeJoint>(),
"HingeJoint should be removed.");
}
[Test]
public void RemoveJoint_All()
{
var go = new GameObject("PhysTest_RemoveAll");
go.AddComponent<Rigidbody>();
go.AddComponent<HingeJoint>();
go.AddComponent<FixedJoint>();
Assert.IsNotNull(go.GetComponent<HingeJoint>());
Assert.IsNotNull(go.GetComponent<FixedJoint>());
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "remove_joint",
["target"] = "PhysTest_RemoveAll"
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.AreEqual(2, result["data"]["removedCount"].Value<int>());
Assert.IsNull(go.GetComponent<HingeJoint>(),
"HingeJoint should be removed.");
Assert.IsNull(go.GetComponent<FixedJoint>(),
"FixedJoint should be removed.");
}
// =====================================================================
// GetCollisionMatrix
// =====================================================================
[Test]
public void GetCollisionMatrix_3D_ReturnsMatrix()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "get_collision_matrix",
["dimension"] = "3d"
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"];
Assert.IsNotNull(data["layers"]);
Assert.IsTrue(data["layers"] is JArray);
Assert.IsTrue(((JArray)data["layers"]).Count > 0);
Assert.IsNotNull(data["matrix"]);
Assert.IsTrue(data["matrix"].Type == JTokenType.Object);
}
[Test]
public void GetCollisionMatrix_2D_ReturnsMatrix()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "get_collision_matrix",
["dimension"] = "2d"
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"];
Assert.IsNotNull(data["layers"]);
Assert.IsTrue(data["layers"] is JArray);
Assert.IsTrue(((JArray)data["layers"]).Count > 0);
Assert.IsNotNull(data["matrix"]);
Assert.IsTrue(data["matrix"].Type == JTokenType.Object);
}
// =====================================================================
// SetCollisionMatrix
// =====================================================================
[Test]
public void SetCollisionMatrix_3D_ChangesAndRestores()
{
bool original = !UnityEngine.Physics.GetIgnoreLayerCollision(0, 0);
try
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "set_collision_matrix",
["dimension"] = "3d",
["layer_a"] = "Default",
["layer_b"] = "Default",
["collide"] = false
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.IsTrue(UnityEngine.Physics.GetIgnoreLayerCollision(0, 0),
"Layers should be ignoring collision after setting collide=false");
result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "set_collision_matrix",
["dimension"] = "3d",
["layer_a"] = "Default",
["layer_b"] = "Default",
["collide"] = true
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.IsFalse(UnityEngine.Physics.GetIgnoreLayerCollision(0, 0),
"Layers should NOT be ignoring collision after setting collide=true");
}
finally
{
UnityEngine.Physics.IgnoreLayerCollision(0, 0, !original);
}
}
[Test]
public void SetCollisionMatrix_InvalidLayer_ReturnsError()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "set_collision_matrix",
["dimension"] = "3d",
["layer_a"] = "NonExistentLayerXYZ",
["layer_b"] = "Default"
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("layer_a"));
}
[Test]
public void SetCollisionMatrix_MissingParams_ReturnsError()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "set_collision_matrix",
["dimension"] = "3d"
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("layer_a"));
}
// =====================================================================
// Raycast
// =====================================================================
[Test]
public void Raycast_HitsCollider()
{
// Use an offset origin/direction to avoid hitting unrelated scene objects
var go = new GameObject("PhysTest_RayTarget");
go.transform.position = new Vector3(500, 500, 5);
var col = go.AddComponent<BoxCollider>();
col.size = new Vector3(10, 10, 1);
UnityEngine.Physics.SyncTransforms();
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "raycast",
["origin"] = new JArray(500, 500, 0),
["direction"] = new JArray(0, 0, 1),
["max_distance"] = 100
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.IsTrue(result["data"].Value<bool>("hit"));
Assert.AreEqual("PhysTest_RayTarget", result["data"]["gameObject"].ToString());
}
[Test]
public void Raycast_MissesWhenNoTarget()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "raycast",
["origin"] = new JArray(0, 1000, 0),
["direction"] = new JArray(0, 1, 0),
["max_distance"] = 1
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.IsFalse(result["data"].Value<bool>("hit"));
}
[Test]
public void Raycast_MissingOrigin_ReturnsError()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "raycast",
["direction"] = new JArray(0, 0, 1)
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("origin"));
}
// =====================================================================
// Overlap
// =====================================================================
[Test]
public void Overlap_Sphere_FindsColliders()
{
var go = new GameObject("PhysTest_OverlapTarget");
go.transform.position = Vector3.zero;
go.AddComponent<SphereCollider>();
UnityEngine.Physics.SyncTransforms();
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "overlap",
["shape"] = "sphere",
["position"] = new JArray(0, 0, 0),
["size"] = 10
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var colliders = result["data"]["colliders"] as JArray;
Assert.IsNotNull(colliders);
Assert.IsTrue(colliders.Count > 0);
}
[Test]
public void Overlap_MissingShape_ReturnsError()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "overlap",
["position"] = new JArray(0, 0, 0),
["size"] = 10
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("shape"));
}
// =====================================================================
// Validate
// =====================================================================
[Test]
public void Validate_DetectsMeshColliderWithoutConvex()
{
var go = new GameObject("PhysTest_BadMesh");
go.AddComponent<Rigidbody>();
var mc = go.AddComponent<MeshCollider>();
mc.convex = false;
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "validate",
["target"] = "PhysTest_BadMesh"
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var warnings = result["data"]["warnings"] as JArray;
Assert.IsNotNull(warnings);
Assert.IsTrue(warnings.Count > 0);
bool foundConvex = false;
foreach (var w in warnings)
{
if (w.ToString().Contains("Convex"))
{ foundConvex = true; break; }
}
Assert.IsTrue(foundConvex, "Should detect MeshCollider without Convex.");
}
[Test]
public void Validate_DetectsColliderWithoutRigidbody()
{
var go = new GameObject("PhysTest_NoRigidbody");
go.isStatic = false;
go.AddComponent<BoxCollider>();
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "validate",
["target"] = "PhysTest_NoRigidbody"
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var warnings = result["data"]["warnings"] as JArray;
Assert.IsNotNull(warnings);
bool foundWarning = false;
foreach (var w in warnings)
{
if (w.ToString().Contains("Collider") && w.ToString().Contains("Rigidbody"))
{ foundWarning = true; break; }
}
Assert.IsTrue(foundWarning, "Should detect Collider without Rigidbody on non-static object.");
}
[Test]
public void Validate_CleanObject_FewWarnings()
{
var go = new GameObject("PhysTest_Clean");
go.AddComponent<Rigidbody>();
go.AddComponent<BoxCollider>();
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "validate",
["target"] = "PhysTest_Clean"
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.AreEqual(1, result["data"].Value<int>("objects_scanned"));
}
[Test]
public void Validate_TargetNotFound_ReturnsError()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "validate",
["target"] = "PhysTest_DoesNotExistXYZ"
}));
Assert.IsFalse(result.Value<bool>("success"));
}
// =====================================================================
// SimulateStep
// =====================================================================
[Test]
public void SimulateStep_ExecutesWithoutError()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "simulate_step",
["steps"] = 1
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.AreEqual(1, result["data"].Value<int>("steps_executed"));
}
[Test]
public void SimulateStep_ClampsMax()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "simulate_step",
["steps"] = 999
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.AreEqual(100, result["data"].Value<int>("steps_executed"));
}
[Test]
public void SimulateStep_InvalidDimension_ReturnsError()
{
var result = ToJObject(ManagePhysics.HandleCommand(new JObject
{
["action"] = "simulate_step",
["dimension"] = "4d"
}));
Assert.IsFalse(result.Value<bool>("success"));
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2f48e1b7be1ed4a29b698127aac0e55e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7a8d9f0e1b2c3d4e5f6a7b8c9d0e1f2a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,251 @@
using System.IO;
using MCPForUnity.Editor.Tools.Prefabs;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
public class ManagePrefabsStageTests
{
private const string TempDirectory = "Assets/Temp/ManagePrefabsStageTests";
[SetUp]
public void SetUp()
{
StageUtility.GoToMainStage();
EnsureFolder(TempDirectory);
}
[TearDown]
public void TearDown()
{
StageUtility.GoToMainStage();
if (AssetDatabase.IsValidFolder(TempDirectory))
{
AssetDatabase.DeleteAsset(TempDirectory);
}
CleanupEmptyParentFolders(TempDirectory);
}
[Test]
public void OpenPrefabStage_RequiresPrefabPath()
{
var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
{
["action"] = "open_prefab_stage"
}));
Assert.IsFalse(result.Value<bool>("success"));
StringAssert.Contains("prefabPath", result.Value<string>("error"));
}
[Test]
public void OpenPrefabStage_RejectsNonPrefabPath()
{
var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
{
["action"] = "open_prefab_stage",
["prefabPath"] = "Assets/Temp/NotPrefab.txt"
}));
Assert.IsFalse(result.Value<bool>("success"));
StringAssert.Contains(".prefab", result.Value<string>("error"));
}
[Test]
public void OpenPrefabStage_OpensPrefabStageAndReturnsStageData()
{
string prefabPath = CreateTestPrefab("OpenStageRoot");
try
{
var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
{
["action"] = "open_prefab_stage",
["prefabPath"] = prefabPath
}));
Assert.IsTrue(result.Value<bool>("success"));
Assert.AreEqual(prefabPath, result["data"].Value<string>("prefabPath"));
Assert.AreEqual(prefabPath, result["data"].Value<string>("openedPrefabPath"));
Assert.AreEqual("OpenStageRoot", result["data"].Value<string>("rootName"));
var stage = PrefabStageUtility.GetCurrentPrefabStage();
Assert.IsNotNull(stage);
Assert.AreEqual(prefabPath, stage.assetPath);
var closeResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
{
["action"] = "close_prefab_stage"
}));
Assert.IsTrue(closeResult.Value<bool>("success"));
Assert.IsNull(PrefabStageUtility.GetCurrentPrefabStage());
}
finally
{
StageUtility.GoToMainStage();
SafeDeleteAsset(prefabPath);
}
}
[Test]
public void OpenPrefabStage_AcceptsPathAlias()
{
string prefabPath = CreateTestPrefab("AliasRoot");
try
{
var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
{
["action"] = "open_prefab_stage",
["path"] = prefabPath
}));
Assert.IsTrue(result.Value<bool>("success"));
Assert.AreEqual(prefabPath, result["data"].Value<string>("openedPrefabPath"));
Assert.IsNotNull(PrefabStageUtility.GetCurrentPrefabStage());
}
finally
{
StageUtility.GoToMainStage();
SafeDeleteAsset(prefabPath);
}
}
[Test]
public void OpenPrefabStage_PrefabPathTakesPrecedenceOverPath()
{
string prefabPath = CreateTestPrefab("PrefabPathRoot");
string aliasPath = CreateTestPrefab("AliasPathRoot");
try
{
var result = ToJObject(ManagePrefabs.HandleCommand(new JObject
{
["action"] = "open_prefab_stage",
["prefabPath"] = prefabPath,
["path"] = aliasPath
}));
Assert.IsTrue(result.Value<bool>("success"));
var currentStage = PrefabStageUtility.GetCurrentPrefabStage();
Assert.IsNotNull(currentStage, "Expected a prefab stage to be open.");
Assert.AreEqual(
prefabPath,
currentStage.assetPath,
"prefabPath should take precedence over path when both are provided."
);
}
finally
{
StageUtility.GoToMainStage();
SafeDeleteAsset(prefabPath);
SafeDeleteAsset(aliasPath);
}
}
[Test]
public void SavePrefabStage_PersistsPrefabContentChanges()
{
string prefabPath = CreateTestPrefab("SaveStageRoot");
try
{
AssertOpenPrefabStage(prefabPath);
var stage = PrefabStageUtility.GetCurrentPrefabStage();
Assert.IsNotNull(stage, "Expected prefab stage to be open.");
var child = new GameObject("SavedChild");
child.transform.SetParent(stage.prefabContentsRoot.transform, false);
var saveResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
{
["action"] = "save_prefab_stage"
}));
Assert.IsTrue(saveResult.Value<bool>("success"), $"Expected save to succeed but got: {saveResult}");
Assert.AreEqual(prefabPath, saveResult["data"].Value<string>("prefabPath"));
StageUtility.GoToMainStage();
GameObject reloaded = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
Assert.IsNotNull(reloaded.transform.Find("SavedChild"), "Saved prefab should contain the new child after save_prefab_stage.");
}
finally
{
StageUtility.GoToMainStage();
SafeDeleteAsset(prefabPath);
}
}
[Test]
public void ClosePrefabStage_SaveBeforeClose_PersistsChanges()
{
string prefabPath = CreateTestPrefab("CloseSaveRoot");
try
{
AssertOpenPrefabStage(prefabPath);
var stage = PrefabStageUtility.GetCurrentPrefabStage();
Assert.IsNotNull(stage, "Expected prefab stage to be open.");
var child = new GameObject("CloseSavedChild");
child.transform.SetParent(stage.prefabContentsRoot.transform, false);
var closeResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
{
["action"] = "close_prefab_stage",
["saveBeforeClose"] = true
}));
Assert.IsTrue(closeResult.Value<bool>("success"), $"Expected close with save to succeed but got: {closeResult}");
Assert.IsNull(PrefabStageUtility.GetCurrentPrefabStage(), "Prefab stage should be closed after close_prefab_stage.");
GameObject reloaded = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
Assert.IsNotNull(reloaded.transform.Find("CloseSavedChild"), "Saved prefab should contain the new child after close_prefab_stage(saveBeforeClose: true).");
}
finally
{
StageUtility.GoToMainStage();
SafeDeleteAsset(prefabPath);
}
}
private static void AssertOpenPrefabStage(string prefabPath)
{
var openResult = ToJObject(ManagePrefabs.HandleCommand(new JObject
{
["action"] = "open_prefab_stage",
["prefabPath"] = prefabPath
}));
Assert.IsTrue(openResult.Value<bool>("success"), $"Expected open to succeed but got: {openResult}");
}
private static string CreateTestPrefab(string rootName)
{
string prefabPath = Path.Combine(TempDirectory, $"{rootName}.prefab").Replace('\\', '/');
var root = new GameObject(rootName);
try
{
PrefabUtility.SaveAsPrefabAsset(root, prefabPath);
}
finally
{
Object.DestroyImmediate(root, true);
}
return prefabPath;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 421e522d607414e7ba1339af74dab899
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,861 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Tools.ProBuilder;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
public class ManageProBuilderTests
{
private readonly List<GameObject> _createdObjects = new List<GameObject>();
private bool _proBuilderInstalled;
[OneTimeSetUp]
public void OneTimeSetUp()
{
_proBuilderInstalled = Type.GetType(
"UnityEngine.ProBuilder.ProBuilderMesh, Unity.ProBuilder"
) != null;
}
[TearDown]
public void TearDown()
{
foreach (var go in _createdObjects)
{
if (go != null)
UnityEngine.Object.DestroyImmediate(go);
}
_createdObjects.Clear();
}
// =====================================================================
// Basic action validation (works regardless of ProBuilder installation)
// =====================================================================
[Test]
public void HandleCommand_MissingAction_ReturnsError()
{
var paramsObj = new JObject();
var result = ToJObject(ManageProBuilder.HandleCommand(paramsObj));
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
}
[Test]
public void HandleCommand_UnknownAction_ReturnsError()
{
var paramsObj = new JObject { ["action"] = "nonexistent_action" };
var result = ToJObject(ManageProBuilder.HandleCommand(paramsObj));
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
var errorMsg = result["error"]?.ToString() ?? result["message"]?.ToString();
Assert.That(errorMsg, Does.Contain("Unknown action").Or.Contain("not installed"));
}
[Test]
public void HandleCommand_Ping_ReturnsSuccess()
{
var paramsObj = new JObject { ["action"] = "ping" };
var result = ToJObject(ManageProBuilder.HandleCommand(paramsObj));
if (!_proBuilderInstalled)
{
// Without ProBuilder, should return error about missing package
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
Assert.That(result["error"]?.ToString(),
Does.Contain("ProBuilder").IgnoreCase);
return;
}
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
}
// =====================================================================
// Shape creation (requires ProBuilder)
// =====================================================================
[Test]
public void CreateShape_MissingShapeType_ReturnsError()
{
if (!_proBuilderInstalled)
{
Assert.Pass("ProBuilder not installed - skipping.");
return;
}
var paramsObj = new JObject
{
["action"] = "create_shape",
};
var result = ToJObject(ManageProBuilder.HandleCommand(paramsObj));
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
}
[Test]
public void CreateShape_InvalidShapeType_ReturnsError()
{
if (!_proBuilderInstalled)
{
Assert.Pass("ProBuilder not installed - skipping.");
return;
}
var paramsObj = new JObject
{
["action"] = "create_shape",
["properties"] = new JObject { ["shapeType"] = "InvalidShape" },
};
var result = ToJObject(ManageProBuilder.HandleCommand(paramsObj));
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
}
[Test]
public void CreateShape_Cube_CreatesGameObject()
{
if (!_proBuilderInstalled)
{
Assert.Pass("ProBuilder not installed - skipping.");
return;
}
var paramsObj = new JObject
{
["action"] = "create_shape",
["properties"] = new JObject
{
["shapeType"] = "Cube",
["name"] = "PBTestCube",
},
};
var result = ToJObject(ManageProBuilder.HandleCommand(paramsObj));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"] as JObject;
Assert.IsNotNull(data, "Result should contain data");
Assert.AreEqual("PBTestCube", data.Value<string>("gameObjectName"));
Assert.Greater(data.Value<int>("faceCount"), 0);
Assert.Greater(data.Value<int>("vertexCount"), 0);
// Track for cleanup
var go = GameObject.Find("PBTestCube");
if (go != null) _createdObjects.Add(go);
}
[Test]
public void CreateShape_WithPosition_SetsTransform()
{
if (!_proBuilderInstalled)
{
Assert.Pass("ProBuilder not installed - skipping.");
return;
}
var paramsObj = new JObject
{
["action"] = "create_shape",
["properties"] = new JObject
{
["shapeType"] = "Cube",
["name"] = "PBTestCubePos",
["position"] = new JArray(5f, 10f, 15f),
},
};
var result = ToJObject(ManageProBuilder.HandleCommand(paramsObj));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var go = GameObject.Find("PBTestCubePos");
Assert.IsNotNull(go, "Created GameObject should exist");
Assert.AreEqual(new Vector3(5f, 10f, 15f), go.transform.position);
_createdObjects.Add(go);
}
[Test]
public void CreatePolyShape_CreatesFromPoints()
{
if (!_proBuilderInstalled)
{
Assert.Pass("ProBuilder not installed - skipping.");
return;
}
var paramsObj = new JObject
{
["action"] = "create_poly_shape",
["properties"] = new JObject
{
["points"] = new JArray(
new JArray(0f, 0f, 0f),
new JArray(5f, 0f, 0f),
new JArray(5f, 0f, 5f),
new JArray(0f, 0f, 5f)
),
["extrudeHeight"] = 3f,
["name"] = "PBTestPoly",
},
};
var result = ToJObject(ManageProBuilder.HandleCommand(paramsObj));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var go = GameObject.Find("PBTestPoly");
if (go != null) _createdObjects.Add(go);
}
// =====================================================================
// Mesh editing (requires ProBuilder + created shape)
// =====================================================================
[Test]
public void GetMeshInfo_ReturnsDetails()
{
if (!_proBuilderInstalled)
{
Assert.Pass("ProBuilder not installed - skipping.");
return;
}
// First create a shape
var createParams = new JObject
{
["action"] = "create_shape",
["properties"] = new JObject
{
["shapeType"] = "Cube",
["name"] = "PBTestInfoCube",
},
};
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
var go = GameObject.Find("PBTestInfoCube");
Assert.IsNotNull(go);
_createdObjects.Add(go);
// Now get mesh info
var infoParams = new JObject
{
["action"] = "get_mesh_info",
["target"] = "PBTestInfoCube",
["properties"] = new JObject { ["include"] = "faces" },
};
var result = ToJObject(ManageProBuilder.HandleCommand(infoParams));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"] as JObject;
Assert.IsNotNull(data);
Assert.Greater(data.Value<int>("faceCount"), 0);
Assert.Greater(data.Value<int>("vertexCount"), 0);
Assert.IsNotNull(data["bounds"]);
Assert.IsNotNull(data["faces"]);
}
[Test]
public void ExtrudeFaces_IncreaseFaceCount()
{
if (!_proBuilderInstalled)
{
Assert.Pass("ProBuilder not installed - skipping.");
return;
}
// Create a cube
var createParams = new JObject
{
["action"] = "create_shape",
["properties"] = new JObject
{
["shapeType"] = "Cube",
["name"] = "PBTestExtrudeCube",
},
};
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
var go = GameObject.Find("PBTestExtrudeCube");
_createdObjects.Add(go);
int initialFaceCount = createResult["data"].Value<int>("faceCount");
// Extrude face 0
var extrudeParams = new JObject
{
["action"] = "extrude_faces",
["target"] = "PBTestExtrudeCube",
["properties"] = new JObject
{
["faceIndices"] = new JArray(0),
["distance"] = 1.0f,
},
};
var result = ToJObject(ManageProBuilder.HandleCommand(extrudeParams));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
int newFaceCount = result["data"].Value<int>("faceCount");
Assert.Greater(newFaceCount, initialFaceCount,
"Face count should increase after extrusion");
}
[Test]
public void DeleteFaces_DecreasesFaceCount()
{
if (!_proBuilderInstalled)
{
Assert.Pass("ProBuilder not installed - skipping.");
return;
}
var createParams = new JObject
{
["action"] = "create_shape",
["properties"] = new JObject
{
["shapeType"] = "Cube",
["name"] = "PBTestDeleteCube",
},
};
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
var go = GameObject.Find("PBTestDeleteCube");
_createdObjects.Add(go);
int initialFaceCount = createResult["data"].Value<int>("faceCount");
var deleteParams = new JObject
{
["action"] = "delete_faces",
["target"] = "PBTestDeleteCube",
["properties"] = new JObject
{
["faceIndices"] = new JArray(0),
},
};
var result = ToJObject(ManageProBuilder.HandleCommand(deleteParams));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
int newFaceCount = result["data"].Value<int>("faceCount");
Assert.Less(newFaceCount, initialFaceCount,
"Face count should decrease after deletion");
}
[Test]
public void SetFaceMaterial_WithMissingTarget_ReturnsError()
{
if (!_proBuilderInstalled)
{
Assert.Pass("ProBuilder not installed - skipping.");
return;
}
var paramsObj = new JObject
{
["action"] = "set_face_material",
["target"] = "NonExistentObject999",
["properties"] = new JObject
{
["faceIndices"] = new JArray(0),
["materialPath"] = "Assets/Materials/Test.mat",
},
};
var result = ToJObject(ManageProBuilder.HandleCommand(paramsObj));
Assert.IsFalse(result.Value<bool>("success"), result.ToString());
}
[Test]
public void FlipNormals_SucceedsOnValidMesh()
{
if (!_proBuilderInstalled)
{
Assert.Pass("ProBuilder not installed - skipping.");
return;
}
var createParams = new JObject
{
["action"] = "create_shape",
["properties"] = new JObject
{
["shapeType"] = "Cube",
["name"] = "PBTestFlipCube",
},
};
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
var go = GameObject.Find("PBTestFlipCube");
_createdObjects.Add(go);
var flipParams = new JObject
{
["action"] = "flip_normals",
["target"] = "PBTestFlipCube",
["properties"] = new JObject
{
["faceIndices"] = new JArray(0, 1),
},
};
var result = ToJObject(ManageProBuilder.HandleCommand(flipParams));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
}
// =====================================================================
// Enhanced get_mesh_info with include parameter
// =====================================================================
[Test]
public void GetMeshInfo_DefaultInclude_ReturnsSummaryOnly()
{
if (!_proBuilderInstalled)
{
Assert.Pass("ProBuilder not installed - skipping.");
return;
}
var createParams = new JObject
{
["action"] = "create_shape",
["properties"] = new JObject
{
["shapeType"] = "Cube",
["name"] = "PBTestSummaryCube",
},
};
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
var go = GameObject.Find("PBTestSummaryCube");
Assert.IsNotNull(go);
_createdObjects.Add(go);
var infoParams = new JObject
{
["action"] = "get_mesh_info",
["target"] = "PBTestSummaryCube",
};
var result = ToJObject(ManageProBuilder.HandleCommand(infoParams));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"] as JObject;
Assert.IsNotNull(data);
Assert.Greater(data.Value<int>("faceCount"), 0);
// Default "summary" should NOT include faces array
Assert.IsNull(data["faces"], "Summary mode should not include faces array");
}
[Test]
public void GetMeshInfo_IncludeFaces_ReturnsFaceNormalsAndDirections()
{
if (!_proBuilderInstalled)
{
Assert.Pass("ProBuilder not installed - skipping.");
return;
}
var createParams = new JObject
{
["action"] = "create_shape",
["properties"] = new JObject
{
["shapeType"] = "Cube",
["name"] = "PBTestFacesCube",
},
};
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
var go = GameObject.Find("PBTestFacesCube");
Assert.IsNotNull(go);
_createdObjects.Add(go);
var infoParams = new JObject
{
["action"] = "get_mesh_info",
["target"] = "PBTestFacesCube",
["properties"] = new JObject { ["include"] = "faces" },
};
var result = ToJObject(ManageProBuilder.HandleCommand(infoParams));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"] as JObject;
Assert.IsNotNull(data);
var faces = data["faces"] as JArray;
Assert.IsNotNull(faces, "Faces mode should include faces array");
Assert.Greater(faces.Count, 0);
// Each face should have normal, center, direction
var firstFace = faces[0] as JObject;
Assert.IsNotNull(firstFace);
Assert.IsNotNull(firstFace["normal"], "Face should have normal");
Assert.IsNotNull(firstFace["center"], "Face should have center");
// direction may be null for angled faces, but should exist as key
Assert.IsTrue(firstFace.ContainsKey("direction"), "Face should have direction key");
}
[Test]
public void GetMeshInfo_IncludeEdges_ReturnsEdgeData()
{
if (!_proBuilderInstalled)
{
Assert.Pass("ProBuilder not installed - skipping.");
return;
}
var createParams = new JObject
{
["action"] = "create_shape",
["properties"] = new JObject
{
["shapeType"] = "Cube",
["name"] = "PBTestEdgesCube",
},
};
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
var go = GameObject.Find("PBTestEdgesCube");
Assert.IsNotNull(go);
_createdObjects.Add(go);
var infoParams = new JObject
{
["action"] = "get_mesh_info",
["target"] = "PBTestEdgesCube",
["properties"] = new JObject { ["include"] = "edges" },
};
var result = ToJObject(ManageProBuilder.HandleCommand(infoParams));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"] as JObject;
Assert.IsNotNull(data);
var edges = data["edges"] as JArray;
Assert.IsNotNull(edges, "Edges mode should include edges array");
Assert.Greater(edges.Count, 0);
var firstEdge = edges[0] as JObject;
Assert.IsNotNull(firstEdge);
Assert.IsTrue(firstEdge.ContainsKey("vertexA"), "Edge should have vertexA");
Assert.IsTrue(firstEdge.ContainsKey("vertexB"), "Edge should have vertexB");
}
[Test]
public void GetMeshInfo_CubeTopFace_HasUpNormal()
{
if (!_proBuilderInstalled)
{
Assert.Pass("ProBuilder not installed - skipping.");
return;
}
var createParams = new JObject
{
["action"] = "create_shape",
["properties"] = new JObject
{
["shapeType"] = "Cube",
["name"] = "PBTestTopNormalCube",
},
};
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
var go = GameObject.Find("PBTestTopNormalCube");
Assert.IsNotNull(go);
_createdObjects.Add(go);
var infoParams = new JObject
{
["action"] = "get_mesh_info",
["target"] = "PBTestTopNormalCube",
["properties"] = new JObject { ["include"] = "faces" },
};
var result = ToJObject(ManageProBuilder.HandleCommand(infoParams));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var faces = result["data"]["faces"] as JArray;
Assert.IsNotNull(faces);
// At least one face should have direction "top"
bool hasTop = false;
foreach (JObject face in faces)
{
if (face["direction"]?.ToString() == "top")
{
hasTop = true;
break;
}
}
Assert.IsTrue(hasTop, "A cube should have at least one face with direction 'top'");
}
// =====================================================================
// Smoothing
// =====================================================================
[Test]
public void AutoSmooth_DefaultAngle_AssignsGroups()
{
if (!_proBuilderInstalled)
{
Assert.Pass("ProBuilder not installed - skipping.");
return;
}
var createParams = new JObject
{
["action"] = "create_shape",
["properties"] = new JObject
{
["shapeType"] = "Cube",
["name"] = "PBTestAutoSmoothCube",
},
};
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
var go = GameObject.Find("PBTestAutoSmoothCube");
Assert.IsNotNull(go);
_createdObjects.Add(go);
var smoothParams = new JObject
{
["action"] = "auto_smooth",
["target"] = "PBTestAutoSmoothCube",
["properties"] = new JObject { ["angleThreshold"] = 30 },
};
var result = ToJObject(ManageProBuilder.HandleCommand(smoothParams));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
}
[Test]
public void SetSmoothing_OnSpecificFaces_SetsGroup()
{
if (!_proBuilderInstalled)
{
Assert.Pass("ProBuilder not installed - skipping.");
return;
}
var createParams = new JObject
{
["action"] = "create_shape",
["properties"] = new JObject
{
["shapeType"] = "Cube",
["name"] = "PBTestSetSmoothCube",
},
};
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
var go = GameObject.Find("PBTestSetSmoothCube");
Assert.IsNotNull(go);
_createdObjects.Add(go);
var smoothParams = new JObject
{
["action"] = "set_smoothing",
["target"] = "PBTestSetSmoothCube",
["properties"] = new JObject
{
["faceIndices"] = new JArray(0, 1),
["smoothingGroup"] = 1,
},
};
var result = ToJObject(ManageProBuilder.HandleCommand(smoothParams));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"] as JObject;
Assert.IsNotNull(data);
Assert.AreEqual(2, data.Value<int>("facesModified"));
}
// =====================================================================
// Mesh Utilities
// =====================================================================
[Test]
public void CenterPivot_MovesPivotToCenter()
{
if (!_proBuilderInstalled)
{
Assert.Pass("ProBuilder not installed - skipping.");
return;
}
var createParams = new JObject
{
["action"] = "create_shape",
["properties"] = new JObject
{
["shapeType"] = "Cube",
["name"] = "PBTestCenterPivot",
},
};
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
var go = GameObject.Find("PBTestCenterPivot");
Assert.IsNotNull(go);
_createdObjects.Add(go);
var pivotParams = new JObject
{
["action"] = "center_pivot",
["target"] = "PBTestCenterPivot",
};
var result = ToJObject(ManageProBuilder.HandleCommand(pivotParams));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
}
[Test]
public void FreezeTransform_ResetsTransformKeepsShape()
{
if (!_proBuilderInstalled)
{
Assert.Pass("ProBuilder not installed - skipping.");
return;
}
var createParams = new JObject
{
["action"] = "create_shape",
["properties"] = new JObject
{
["shapeType"] = "Cube",
["name"] = "PBTestFreeze",
["position"] = new JArray(5f, 3f, 2f),
},
};
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
var go = GameObject.Find("PBTestFreeze");
Assert.IsNotNull(go);
_createdObjects.Add(go);
var freezeParams = new JObject
{
["action"] = "freeze_transform",
["target"] = "PBTestFreeze",
};
var result = ToJObject(ManageProBuilder.HandleCommand(freezeParams));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
// Transform should be reset to identity
Assert.AreEqual(Vector3.zero, go.transform.position);
Assert.AreEqual(Quaternion.identity, go.transform.rotation);
Assert.AreEqual(Vector3.one, go.transform.localScale);
}
[Test]
public void ValidateMesh_CleanMesh_ReturnsNoIssues()
{
if (!_proBuilderInstalled)
{
Assert.Pass("ProBuilder not installed - skipping.");
return;
}
var createParams = new JObject
{
["action"] = "create_shape",
["properties"] = new JObject
{
["shapeType"] = "Cube",
["name"] = "PBTestValidate",
},
};
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
var go = GameObject.Find("PBTestValidate");
Assert.IsNotNull(go);
_createdObjects.Add(go);
var validateParams = new JObject
{
["action"] = "validate_mesh",
["target"] = "PBTestValidate",
};
var result = ToJObject(ManageProBuilder.HandleCommand(validateParams));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"] as JObject;
Assert.IsNotNull(data);
Assert.IsTrue(data.Value<bool>("healthy"), "Fresh cube should be healthy");
Assert.AreEqual(0, data.Value<int>("degenerateTriangles"));
}
[Test]
public void RepairMesh_OnCleanMesh_ReportsNoChanges()
{
if (!_proBuilderInstalled)
{
Assert.Pass("ProBuilder not installed - skipping.");
return;
}
var createParams = new JObject
{
["action"] = "create_shape",
["properties"] = new JObject
{
["shapeType"] = "Cube",
["name"] = "PBTestRepair",
},
};
var createResult = ToJObject(ManageProBuilder.HandleCommand(createParams));
Assert.IsTrue(createResult.Value<bool>("success"), createResult.ToString());
var go = GameObject.Find("PBTestRepair");
Assert.IsNotNull(go);
_createdObjects.Add(go);
var repairParams = new JObject
{
["action"] = "repair_mesh",
["target"] = "PBTestRepair",
};
var result = ToJObject(ManageProBuilder.HandleCommand(repairParams));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"] as JObject;
Assert.IsNotNull(data);
Assert.AreEqual(0, data.Value<int>("degenerateTrianglesRemoved"));
}
// =====================================================================
// ProBuilder not installed fallback
// =====================================================================
[Test]
public void AllActions_WithoutProBuilder_ReturnPackageError()
{
if (_proBuilderInstalled)
{
Assert.Pass("ProBuilder IS installed - this test verifies the not-installed path.");
return;
}
string[] testActions = {
"ping", "create_shape", "get_mesh_info", "extrude_faces",
"auto_smooth", "set_smoothing", "center_pivot", "validate_mesh",
};
foreach (var action in testActions)
{
var paramsObj = new JObject { ["action"] = action };
var result = ToJObject(ManageProBuilder.HandleCommand(paramsObj));
Assert.IsFalse(result.Value<bool>("success"),
$"Action '{action}' should fail without ProBuilder: {result}");
Assert.That(result["error"]?.ToString(),
Does.Contain("ProBuilder").IgnoreCase,
$"Error for '{action}' should mention ProBuilder");
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f2a1efebc27c48258b6ce356fed59a35
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,205 @@
using NUnit.Framework;
using System.Reflection;
using UnityEngine;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Tools;
namespace MCPForUnityTests.Editor.Tools
{
public class ManageSceneHierarchyPagingTests
{
private GameObject _root;
private readonly System.Collections.Generic.List<GameObject> _created = new System.Collections.Generic.List<GameObject>();
[TearDown]
public void TearDown()
{
for (int i = 0; i < _created.Count; i++)
{
if (_created[i] != null) Object.DestroyImmediate(_created[i]);
}
_created.Clear();
if (_root != null)
{
Object.DestroyImmediate(_root);
_root = null;
}
}
[Test]
public void GetHierarchy_PaginatesRoots_AndSupportsChildrenPaging()
{
// Arrange: create many roots so paging must occur.
// Note: Keep counts modest to avoid slowing EditMode tests.
const int rootCount = 40;
for (int i = 0; i < rootCount; i++)
{
_created.Add(new GameObject($"HS_Root_{i:D2}"));
}
_root = new GameObject("HS_Parent");
for (int i = 0; i < 15; i++)
{
var child = new GameObject($"HS_Child_{i:D2}");
child.transform.SetParent(_root.transform);
}
// Act: request a small page to force truncation
var p1 = new JObject
{
["action"] = "get_hierarchy",
["pageSize"] = 10,
};
var raw1 = ManageScene.HandleCommand(p1);
var res1 = raw1 as JObject ?? JObject.FromObject(raw1);
// Assert: envelope success + payload shape
Assert.IsTrue(res1.Value<bool>("success"), res1.ToString());
var data1 = res1["data"] as JObject;
Assert.IsNotNull(data1, "Expected data payload to be an object.");
Assert.AreEqual("roots", data1.Value<string>("scope"));
Assert.AreEqual(true, data1.Value<bool>("truncated"), "Expected truncation when pageSize < root count.");
Assert.IsNotNull(data1["next_cursor"], "Expected next_cursor when truncated.");
var items1 = data1["items"] as JArray;
Assert.IsNotNull(items1, "Expected items array.");
Assert.AreEqual(10, items1.Count, "Expected exactly pageSize items returned.");
// Act: fetch next page of roots using next_cursor
var cursor = data1.Value<string>("next_cursor");
var p2 = new JObject
{
["action"] = "get_hierarchy",
["pageSize"] = 10,
["cursor"] = cursor,
};
var raw2 = ManageScene.HandleCommand(p2);
var res2 = raw2 as JObject ?? JObject.FromObject(raw2);
Assert.IsTrue(res2.Value<bool>("success"), res2.ToString());
var data2 = res2["data"] as JObject;
Assert.IsNotNull(data2);
var items2 = data2["items"] as JArray;
Assert.IsNotNull(items2);
Assert.AreEqual(10, items2.Count);
// Act: page children of a specific parent via 'parent' param (instance ID)
var pChildren = new JObject
{
["action"] = "get_hierarchy",
["parent"] = _root.GetInstanceID(),
["pageSize"] = 7,
};
var rawChildren = ManageScene.HandleCommand(pChildren);
var resChildren = rawChildren as JObject ?? JObject.FromObject(rawChildren);
Assert.IsTrue(resChildren.Value<bool>("success"), resChildren.ToString());
var dataChildren = resChildren["data"] as JObject;
Assert.IsNotNull(dataChildren);
Assert.AreEqual("children", dataChildren.Value<string>("scope"));
Assert.AreEqual(true, dataChildren.Value<bool>("truncated"));
Assert.IsNotNull(dataChildren["next_cursor"]);
var childItems = dataChildren["items"] as JArray;
Assert.IsNotNull(childItems);
Assert.AreEqual(7, childItems.Count);
}
[Test]
public void Screenshot_SceneViewRejectsSupersizeAboveOne()
{
var raw = ManageScene.HandleCommand(new JObject
{
["action"] = "screenshot",
["captureSource"] = "scene_view",
["superSize"] = 2,
});
var response = raw as JObject ?? JObject.FromObject(raw);
Assert.IsFalse(response.Value<bool>("success"), response.ToString());
StringAssert.Contains("does not support super_size above 1", response.Value<string>("error"));
}
[Test]
public void EditorWindowScreenshotUtility_SanitizesFileName()
{
var helperType = typeof(ManageScene).Assembly.GetType("MCPForUnity.Editor.Helpers.EditorWindowScreenshotUtility");
Assert.IsNotNull(helperType, "Expected EditorWindowScreenshotUtility type.");
var sanitizeMethod = helperType.GetMethod("SanitizeFileName", BindingFlags.NonPublic | BindingFlags.Static);
Assert.IsNotNull(sanitizeMethod, "Expected SanitizeFileName helper.");
string sanitized = (string)sanitizeMethod.Invoke(null, new object[] { "../evil/path/shot" });
Assert.AreEqual("shot", sanitized);
Assert.IsFalse(sanitized.Contains("/"));
Assert.IsFalse(sanitized.Contains("\\"));
Assert.IsFalse(sanitized.Contains(".."));
string[] reservedInputs = { "CON", "NUL", "PRN", "AUX", "../CON.txt", "folder/COM1.log", "nested\\LPT9", "CON ", "NUL." };
foreach (string input in reservedInputs)
{
sanitized = (string)sanitizeMethod.Invoke(null, new object[] { input });
string sanitizedStem = System.IO.Path.GetFileNameWithoutExtension(sanitized);
Assert.IsFalse(
string.Equals(sanitizedStem, "CON", System.StringComparison.OrdinalIgnoreCase) ||
string.Equals(sanitizedStem, "NUL", System.StringComparison.OrdinalIgnoreCase) ||
string.Equals(sanitizedStem, "PRN", System.StringComparison.OrdinalIgnoreCase) ||
string.Equals(sanitizedStem, "AUX", System.StringComparison.OrdinalIgnoreCase) ||
string.Equals(sanitizedStem, "COM1", System.StringComparison.OrdinalIgnoreCase) ||
string.Equals(sanitizedStem, "LPT9", System.StringComparison.OrdinalIgnoreCase),
$"Expected reserved device name to be sanitized for input '{input}', got '{sanitized}'.");
Assert.IsFalse(sanitized.Contains("/"));
Assert.IsFalse(sanitized.Contains("\\"));
Assert.IsFalse(sanitized.Contains(".."));
}
}
[Test]
public void EditorWindowScreenshotUtility_ClampsSceneViewSupersizeToOne()
{
var helperType = typeof(ManageScene).Assembly.GetType("MCPForUnity.Editor.Helpers.EditorWindowScreenshotUtility");
Assert.IsNotNull(helperType, "Expected EditorWindowScreenshotUtility type.");
var normalizeMethod = helperType.GetMethod("NormalizeSceneViewSuperSize", BindingFlags.NonPublic | BindingFlags.Static);
Assert.IsNotNull(normalizeMethod, "Expected NormalizeSceneViewSuperSize helper.");
int normalized = (int)normalizeMethod.Invoke(null, new object[] { 4 });
Assert.AreEqual(1, normalized);
normalized = (int)normalizeMethod.Invoke(null, new object[] { 0 });
Assert.AreEqual(1, normalized);
}
[Test]
public void Screenshot_ViewTargetAcceptedForGameView()
{
// view_target should be accepted for game_view (positioned capture path).
// It will fail to resolve a non-existent GO, but should NOT reject the parameter itself.
var raw = ManageScene.HandleCommand(new JObject
{
["action"] = "screenshot",
["viewTarget"] = "NonExistentObject",
});
var response = raw as JObject ?? JObject.FromObject(raw);
// Should attempt positioned capture and fail to resolve the GO — not reject the param
Assert.IsFalse(response.Value<bool>("success"), response.ToString());
StringAssert.Contains("not found", response.Value<string>("error"));
}
[Test]
public void CalculateFrameBounds_UsesCollider2D()
{
var helperType = typeof(ManageScene).GetMethod("CalculateFrameBounds", BindingFlags.NonPublic | BindingFlags.Static);
Assert.IsNotNull(helperType, "Expected CalculateFrameBounds helper.");
var root = new GameObject("HS_2D");
_created.Add(root);
var collider = root.AddComponent<BoxCollider2D>();
collider.size = new Vector2(4f, 2f);
collider.offset = new Vector2(1f, -1f);
Bounds bounds = (Bounds)helperType.Invoke(null, new object[] { root });
Assert.Greater(bounds.size.x, 0.1f);
Assert.Greater(bounds.size.y, 0.1f);
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 95a76a6de453c48abaee108f9029cb65
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,97 @@
using NUnit.Framework;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Tools;
using UnityEngine.SceneManagement;
namespace MCPForUnity.Tests.EditMode.Tools
{
[TestFixture]
public class ManageSceneMultiSceneTests
{
[Test]
public void GetLoadedScenes_ReturnsAtLeastOne()
{
var p = new JObject { ["action"] = "get_loaded_scenes" };
var result = ManageScene.HandleCommand(p);
var r = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(r.Value<bool>("success"), r.ToString());
var scenes = r["data"]?["scenes"] as JArray;
Assert.IsNotNull(scenes);
Assert.GreaterOrEqual(scenes.Count, 1);
}
[Test]
public void CloseScene_LastScene_ReturnsError()
{
if (SceneManager.sceneCount > 1)
{
Assert.Ignore("Test requires a single scene; editor has additive scenes open.");
return;
}
var active = SceneManager.GetActiveScene();
var p = new JObject
{
["action"] = "close_scene",
["sceneName"] = active.name
};
var result = ManageScene.HandleCommand(p);
var r = result as JObject ?? JObject.FromObject(result);
Assert.IsFalse(r.Value<bool>("success"), "Should fail to close last scene");
}
[Test]
public void MoveToScene_MissingTarget_ReturnsError()
{
var p = new JObject
{
["action"] = "move_to_scene",
["sceneName"] = "SomeScene"
};
var result = ManageScene.HandleCommand(p);
var r = result as JObject ?? JObject.FromObject(result);
Assert.IsFalse(r.Value<bool>("success"));
}
[Test]
public void MoveToScene_NonExistentGO_ReturnsError()
{
var p = new JObject
{
["action"] = "move_to_scene",
["target"] = "NonExistentGO_99999",
["sceneName"] = "SomeScene"
};
var result = ManageScene.HandleCommand(p);
var r = result as JObject ?? JObject.FromObject(result);
Assert.IsFalse(r.Value<bool>("success"));
}
[Test]
public void ModifyBuildSettings_RedirectsToManageBuild()
{
var p = new JObject
{
["action"] = "modify_build_settings",
["scenePath"] = "Assets/Scenes/Test.unity",
["operation"] = "add"
};
var result = ManageScene.HandleCommand(p);
var r = result as JObject ?? JObject.FromObject(result);
Assert.IsFalse(r.Value<bool>("success"));
Assert.IsTrue(r.Value<string>("error").Contains("manage_build"));
}
[Test]
public void SetActiveScene_NotFound_ReturnsError()
{
var p = new JObject
{
["action"] = "set_active_scene",
["sceneName"] = "NonExistentScene_99999"
};
var result = ManageScene.HandleCommand(p);
var r = result as JObject ?? JObject.FromObject(result);
Assert.IsFalse(r.Value<bool>("success"));
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 71e2a65280e7c4c7da84b774b639f70a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,25 @@
using NUnit.Framework;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Tools;
namespace MCPForUnity.Tests.EditMode.Tools
{
[TestFixture]
public class ManageSceneTemplateTests
{
[Test]
public void Create_UnknownTemplate_ReturnsError()
{
var p = new JObject
{
["action"] = "create",
["name"] = "TemplateTest",
["path"] = "Assets/Scenes",
["template"] = "nonexistent_template"
};
var result = ManageScene.HandleCommand(p);
var r = result as JObject ?? JObject.FromObject(result);
Assert.IsFalse(r.Value<bool>("success"), "Unknown template should fail");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 575ba801f927b4ad2933d8eb86c8571b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,32 @@
using NUnit.Framework;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Tools;
namespace MCPForUnity.Tests.EditMode.Tools
{
[TestFixture]
public class ManageSceneValidationTests
{
[Test]
public void Validate_CleanScene_ReturnsNoIssues()
{
var p = new JObject { ["action"] = "validate" };
var result = ManageScene.HandleCommand(p);
var r = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(r.Value<bool>("success"), r.ToString());
var data = r["data"];
Assert.IsNotNull(data);
Assert.AreEqual(0, data.Value<int>("totalIssues"));
}
[Test]
public void Validate_WithAutoRepair_CleanScene_RepairsNothing()
{
var p = new JObject { ["action"] = "validate", ["autoRepair"] = true };
var result = ManageScene.HandleCommand(p);
var r = result as JObject ?? JObject.FromObject(result);
Assert.IsTrue(r.Value<bool>("success"), r.ToString());
Assert.AreEqual(0, r["data"].Value<int>("repaired"));
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 929d6112822864391ae2dd7cf289049c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,230 @@
using System;
using System.Reflection;
using NUnit.Framework;
using UnityEngine;
using MCPForUnity.Editor.Tools;
namespace MCPForUnityTests.Editor.Tools
{
/// <summary>
/// Tests for ManageScript delimiter-checking and token-finding logic,
/// specifically covering C# string variants that the old lexer missed:
/// verbatim strings, interpolated strings, raw string literals.
/// </summary>
public class ManageScriptDelimiterTests
{
// ── CheckBalancedDelimiters ──────────────────────────────────────
[Test]
public void CheckBalancedDelimiters_VerbatimString_WithBackslashes()
{
// @"C:\Users\file" — backslashes are NOT escape chars in verbatim strings
string code = "class C { string s = @\"C:\\Users\\file\"; }";
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
"Verbatim string with backslashes should not break delimiter balance");
}
[Test]
public void CheckBalancedDelimiters_VerbatimString_DoubledQuotes()
{
// @"He said ""hello""" — doubled quotes are the escape in verbatim strings
string code = "class C { string s = @\"He said \"\"hello\"\"\"; }";
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
"Verbatim string with doubled quotes should not break delimiter balance");
}
[Test]
public void CheckBalancedDelimiters_InterpolatedString_WithBraces()
{
// $"Value: {x}" — the { } are interpolation holes, not real braces
string code = "class C { void M() { int x = 1; string s = $\"Value: {x}\"; } }";
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
"Interpolated string braces should not be counted as delimiters");
}
[Test]
public void CheckBalancedDelimiters_InterpolatedVerbatim_Combined()
{
// $@"Path: {dir}\file" — interpolated + verbatim combined
string code = "class C { void M() { string dir = \"d\"; string s = $@\"Path: {dir}\\file\"; } }";
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
"Interpolated verbatim string should not break delimiter balance");
}
[Test]
public void CheckBalancedDelimiters_NestedInterpolation()
{
// $"Outer {$"Inner {x}"}" — nested interpolated strings
string code = "class C { void M() { int x = 1; string s = $\"Outer {$\"Inner {x}\"}\"; } }";
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
"Nested interpolated strings should not break delimiter balance");
}
[Test]
public void CheckBalancedDelimiters_RawStringLiteral()
{
// C# 11 raw string literal: """{ }"""
string code = "class C { string s = \"\"\"\n{ }\n\"\"\"; }";
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
"Raw string literal braces should not be counted as delimiters");
}
[Test]
public void CheckBalancedDelimiters_MultilineVerbatimString()
{
// Verbatim string spanning multiple lines with braces
string code = "class C { string s = @\"line1\n{ }\"; }";
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
"Multiline verbatim string with braces should not break balance");
}
[Test]
public void CheckBalancedDelimiters_InterpolatedEscapedBraces()
{
// $"literal {{braces}}" — escaped braces in interpolated string
string code = "class C { string s = $\"literal {{braces}}\"; }";
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
"Escaped braces in interpolated strings should not break balance");
}
[Test]
public void CheckBalancedDelimiters_InterpolatedRawString()
{
// $"""...{expr}...""" — interpolated raw string literal (C# 11)
string code = "class C { void M() { int x = 1; string s = $\"\"\"\n Hello {x}\n \"\"\"; } }";
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
"Interpolated raw string should not break delimiter balance");
}
[Test]
public void CheckBalancedDelimiters_MultiDollarRawString()
{
// $$"""...{{expr}}...""" — multi-dollar interpolated raw string
string code = "class C { void M() { int x = 1; string s = $$\"\"\"\n {literal} {{x}}\n \"\"\"; } }";
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
"Multi-dollar raw string should not break delimiter balance");
}
[Test]
public void CheckBalancedDelimiters_BracesInComments_Ignored()
{
string code = "class C {\n// {\n/* { */\nvoid M() { }\n}";
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
"Braces in comments should be ignored");
}
[Test]
public void CheckBalancedDelimiters_BracesInRegularStrings_Ignored()
{
string code = "class C { string s = \"{ }\"; }";
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
"Braces in regular strings should be ignored");
}
[Test]
public void CheckBalancedDelimiters_ActuallyUnbalanced_ReturnsFalse()
{
string code = "class C { void M() { }";
Assert.IsFalse(CallCheckBalancedDelimiters(code, out _, out _),
"Actually unbalanced code should return false");
}
[Test]
public void CheckBalancedDelimiters_ExtraClosingBrace_ReturnsFalse()
{
string code = "class C { } }";
Assert.IsFalse(CallCheckBalancedDelimiters(code, out _, out _),
"Extra closing brace should return false");
}
[Test]
public void CheckBalancedDelimiters_RealWorldUnityScript()
{
string code = @"using UnityEngine;
public class PlayerHUD : MonoBehaviour
{
private int score;
private string playerName;
void Start()
{
score = 0;
playerName = ""Player"";
}
void OnGUI()
{
string label = $""Score: {score}"";
string path = @""C:\Games\SaveData"";
string msg = $@""Player {playerName} at path {path}"";
Debug.Log($""HUD initialized for {playerName} with score {score}"");
Debug.Log(""Literal {{braces}}"");
}
}";
Assert.IsTrue(CallCheckBalancedDelimiters(code, out _, out _),
"Real-world Unity script with interpolated/verbatim strings should pass");
}
// ── IndexOfClassToken ────────────────────────────────────────────
[Test]
public void IndexOfClassToken_FindsClass_NormalCode()
{
string code = "public class Foo { }";
int idx = CallIndexOfClassToken(code, "Foo");
Assert.GreaterOrEqual(idx, 0, "Should find class Foo in normal code");
}
[Test]
public void IndexOfClassToken_SkipsClassInComment()
{
string code = "// class Foo\npublic class Real { }";
int idx = CallIndexOfClassToken(code, "Foo");
Assert.AreEqual(-1, idx, "Should not find 'class Foo' inside a comment");
}
[Test]
public void IndexOfClassToken_SkipsClassInString()
{
string code = "class Real { string s = \"class Foo { }\"; }";
int idx = CallIndexOfClassToken(code, "Foo");
Assert.AreEqual(-1, idx, "Should not find 'class Foo' inside a string literal");
}
[Test]
public void IndexOfClassToken_FindsSecondClass_WhenFirstInComment()
{
string code = "// class Fake\npublic class Real { }";
int idx = CallIndexOfClassToken(code, "Real");
Assert.GreaterOrEqual(idx, 0, "Should find class Real even when a commented class precedes it");
}
// ── Reflection helpers ───────────────────────────────────────────
private static bool CallCheckBalancedDelimiters(string text, out int line, out char expected)
{
line = 0;
expected = '\0';
var method = typeof(ManageScript).GetMethod("CheckBalancedDelimiters",
BindingFlags.NonPublic | BindingFlags.Static);
Assert.IsNotNull(method, "CheckBalancedDelimiters method should exist");
var parameters = new object[] { text, 0, '\0' };
var result = (bool)method.Invoke(null, parameters);
line = (int)parameters[1];
expected = (char)parameters[2];
return result;
}
private static int CallIndexOfClassToken(string source, string className)
{
var method = typeof(ManageScript).GetMethod("IndexOfClassToken",
BindingFlags.NonPublic | BindingFlags.Static);
Assert.IsNotNull(method, "IndexOfClassToken method should exist");
return (int)method.Invoke(null, new object[] { source, className });
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5cbe58d767a3d4ddf995332459d66830
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,447 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using UnityEngine;
using Newtonsoft.Json.Linq;
using MCPForUnity.Editor.Tools;
using System.Reflection;
namespace MCPForUnityTests.Editor.Tools
{
/// <summary>
/// In-memory tests for ManageScript validation logic.
/// These tests focus on the validation methods directly without creating files.
/// </summary>
public class ManageScriptValidationTests
{
[Test]
public void HandleCommand_NullParams_ReturnsError()
{
var result = ManageScript.HandleCommand(null);
Assert.IsNotNull(result, "Should handle null parameters gracefully");
}
[Test]
public void HandleCommand_InvalidAction_ReturnsError()
{
var paramsObj = new JObject
{
["action"] = "invalid_action",
["name"] = "TestScript",
["path"] = "Assets/Scripts"
};
var result = ManageScript.HandleCommand(paramsObj);
Assert.IsNotNull(result, "Should return error result for invalid action");
}
[Test]
public void CheckBalancedDelimiters_ValidCode_ReturnsTrue()
{
string validCode = "using UnityEngine;\n\npublic class TestClass : MonoBehaviour\n{\n void Start()\n {\n Debug.Log(\"test\");\n }\n}";
bool result = CallCheckBalancedDelimiters(validCode, out int line, out char expected);
Assert.IsTrue(result, "Valid C# code should pass balance check");
}
[Test]
public void CheckBalancedDelimiters_UnbalancedBraces_ReturnsFalse()
{
string unbalancedCode = "using UnityEngine;\n\npublic class TestClass : MonoBehaviour\n{\n void Start()\n {\n Debug.Log(\"test\");\n // Missing closing brace";
bool result = CallCheckBalancedDelimiters(unbalancedCode, out int line, out char expected);
Assert.IsFalse(result, "Unbalanced code should fail balance check");
}
[Test]
public void CheckBalancedDelimiters_StringWithBraces_ReturnsTrue()
{
string codeWithStringBraces = "using UnityEngine;\n\npublic class TestClass : MonoBehaviour\n{\n public string json = \"{key: value}\";\n void Start() { Debug.Log(json); }\n}";
bool result = CallCheckBalancedDelimiters(codeWithStringBraces, out int line, out char expected);
Assert.IsTrue(result, "Code with braces in strings should pass balance check");
}
[Test]
public void TicTacToe3D_ValidationScenario_DoesNotCrash()
{
// Test the scenario that was causing issues without file I/O
string ticTacToeCode = "using UnityEngine;\n\npublic class TicTacToe3D : MonoBehaviour\n{\n public string gameState = \"active\";\n void Start() { Debug.Log(\"Game started\"); }\n public void MakeMove(int position) { if (gameState == \"active\") Debug.Log($\"Move {position}\"); }\n}";
// Test that the validation methods don't crash on this code
bool balanceResult = CallCheckBalancedDelimiters(ticTacToeCode, out int line, out char expected);
Assert.IsTrue(balanceResult, "TicTacToe3D code should pass balance validation");
}
// Helper methods to access private ManageScript methods via reflection
private bool CallCheckBalancedDelimiters(string contents, out int line, out char expected)
{
line = 0;
expected = ' ';
try
{
var method = typeof(ManageScript).GetMethod("CheckBalancedDelimiters",
BindingFlags.NonPublic | BindingFlags.Static);
if (method != null)
{
var parameters = new object[] { contents, line, expected };
var result = (bool)method.Invoke(null, parameters);
line = (int)parameters[1];
expected = (char)parameters[2];
return result;
}
}
catch (Exception ex)
{
Debug.LogWarning($"Could not test CheckBalancedDelimiters directly: {ex.Message}");
}
// Fallback: basic structural check
return BasicBalanceCheck(contents);
}
private bool BasicBalanceCheck(string contents)
{
// Simple fallback balance check
int braceCount = 0;
bool inString = false;
bool escaped = false;
for (int i = 0; i < contents.Length; i++)
{
char c = contents[i];
if (escaped)
{
escaped = false;
continue;
}
if (inString)
{
if (c == '\\') escaped = true;
else if (c == '"') inString = false;
continue;
}
if (c == '"') inString = true;
else if (c == '{') braceCount++;
else if (c == '}') braceCount--;
if (braceCount < 0) return false;
}
return braceCount == 0;
}
/// <summary>
/// Calls ValidateScriptSyntax via reflection and returns the error list.
/// This exercises CheckDuplicateMethodSignatures (called from ValidateScriptSyntax).
/// </summary>
private List<string> CallValidateScriptSyntaxUnity(string contents)
{
var validationLevelType = typeof(ManageScript).GetNestedType("ValidationLevel",
BindingFlags.NonPublic);
Assert.IsNotNull(validationLevelType, "ValidationLevel enum must exist");
var basicLevel = Enum.ToObject(validationLevelType, 0); // ValidationLevel.Basic
var method = typeof(ManageScript).GetMethod("ValidateScriptSyntax",
BindingFlags.NonPublic | BindingFlags.Static, null,
new[] { typeof(string), validationLevelType, typeof(string[]).MakeByRefType() }, null);
Assert.IsNotNull(method, "ValidateScriptSyntax method must exist");
var args = new object[] { contents, basicLevel, null };
method.Invoke(null, args);
var errArray = (string[])args[2];
return errArray != null ? errArray.ToList() : new List<string>();
}
private bool HasDuplicateMethodError(List<string> errors)
{
return errors.Any(e => e.Contains("Duplicate method signature detected"));
}
// --- Duplicate method detection: false positive tests ---
[Test]
public void DuplicateDetection_LineCommentedMethod_NotFlagged()
{
string code = @"using UnityEngine;
public class Foo : MonoBehaviour
{
public void DoStuff(int x) { }
// public void DoStuff(int x) { }
}";
var errors = CallValidateScriptSyntaxUnity(code);
Assert.IsFalse(HasDuplicateMethodError(errors),
"A method in a line comment should not be flagged as duplicate");
}
[Test]
public void DuplicateDetection_BlockCommentedMethod_NotFlagged()
{
string code = @"using UnityEngine;
public class Foo : MonoBehaviour
{
public void DoStuff(int x) { }
/* public void DoStuff(int x) { } */
}";
var errors = CallValidateScriptSyntaxUnity(code);
Assert.IsFalse(HasDuplicateMethodError(errors),
"A method in a block comment should not be flagged as duplicate");
}
[Test]
public void DuplicateDetection_InnerClassSameMethod_NotFlagged()
{
string code = @"using UnityEngine;
public class Outer : MonoBehaviour
{
public void Init(int x) { }
private class Inner
{
public void Init(int x) { }
}
}";
var errors = CallValidateScriptSyntaxUnity(code);
Assert.IsFalse(HasDuplicateMethodError(errors),
"Same method name in outer and inner class should not be flagged");
}
[Test]
public void DuplicateDetection_DifferentTypeOverloads_NotFlagged()
{
string code = @"using UnityEngine;
public class Foo : MonoBehaviour
{
public void Process(int x) { }
public void Process(string x) { }
}";
var errors = CallValidateScriptSyntaxUnity(code);
Assert.IsFalse(HasDuplicateMethodError(errors),
"Overloads with different param types but same count should not be flagged");
}
// --- Duplicate method detection: true positive tests ---
[Test]
public void DuplicateDetection_ExpressionBodiedDuplicate_Flagged()
{
string code = @"using UnityEngine;
public class Foo : MonoBehaviour
{
public int GetValue(int x) => x * 2;
public int GetValue(int x) => x * 3;
}";
var errors = CallValidateScriptSyntaxUnity(code);
Assert.IsTrue(HasDuplicateMethodError(errors),
"Expression-bodied duplicate methods should be flagged");
}
[Test]
public void DuplicateDetection_ExactDuplicate_Flagged()
{
string code = @"using UnityEngine;
public class Foo : MonoBehaviour
{
public void DoStuff(int x) { }
public void DoStuff(int x) { }
}";
var errors = CallValidateScriptSyntaxUnity(code);
Assert.IsTrue(HasDuplicateMethodError(errors),
"Exact duplicate methods should be flagged");
}
[Test]
public void DuplicateDetection_SameTypeDifferentParamName_Flagged()
{
// This is the real anchor_replace corruption pattern
string code = @"using UnityEngine;
public class Foo : MonoBehaviour
{
public void Initialize(string name) { }
public void Initialize(string label) { }
}";
var errors = CallValidateScriptSyntaxUnity(code);
Assert.IsTrue(HasDuplicateMethodError(errors),
"Same-type different-name duplicates (corruption pattern) should be flagged");
}
[Test]
public void DuplicateDetection_GenericParamDuplicate_Flagged()
{
string code = @"using UnityEngine;
using System.Collections.Generic;
public class Foo : MonoBehaviour
{
public void Process(Dictionary<string, int> data) { }
public void Process(Dictionary<string, int> other) { }
}";
var errors = CallValidateScriptSyntaxUnity(code);
Assert.IsTrue(HasDuplicateMethodError(errors),
"Generic param duplicates with different names should be flagged");
}
// --- Keyword false positive tests ---
[Test]
public void DuplicateDetection_CSharpKeywords_NotMatchedAsMethods()
{
string code = @"using UnityEngine;
public class Foo : MonoBehaviour
{
public void Update()
{
if (true) { }
if (true) { }
for (int i = 0; i < 10; i++) { }
for (int j = 0; j < 5; j++) { }
while (true) { break; }
while (false) { break; }
foreach (var x in new int[0]) { }
foreach (var y in new int[0]) { }
switch (0) { default: break; }
switch (1) { default: break; }
lock (this) { }
lock (this) { }
using (var d = new System.IO.MemoryStream()) { }
using (var e = new System.IO.MemoryStream()) { }
typeof(int);
typeof(string);
}
}";
var errors = CallValidateScriptSyntaxUnity(code);
Assert.IsFalse(HasDuplicateMethodError(errors),
"C# keywords (if, for, while, etc.) should not be matched as duplicate methods");
}
[Test]
public void DuplicateMethodCheck_ConstructorInvocations_NotFlagged()
{
string code = @"using UnityEngine;
public class Test : MonoBehaviour
{
void Start()
{
GameObject a = new GameObject(""A"");
GameObject b = new GameObject(""B"");
}
}";
var errors = CallValidateScriptSyntaxUnity(code);
Assert.IsFalse(HasDuplicateMethodError(errors),
"Constructor invocations (new Type(...)) should not be flagged as duplicate methods");
}
[Test]
public void DuplicateMethodCheck_MultipleDistinctConstructors_NotFlagged()
{
string code = @"using UnityEngine;
public class Test : MonoBehaviour
{
void Start()
{
var mpb1 = new MaterialPropertyBlock();
var mpb2 = new MaterialPropertyBlock();
var go1 = new GameObject(""A"");
var go2 = new GameObject(""B"");
}
}";
var errors = CallValidateScriptSyntaxUnity(code);
Assert.IsFalse(HasDuplicateMethodError(errors),
"Multiple constructor invocations of different types should not be flagged");
}
[Test]
public void DuplicateMethodCheck_NewModifierWithConstructors_CorrectBehavior()
{
string code = @"using UnityEngine;
public class Base : MonoBehaviour
{
public virtual void Init() { }
}
public class Derived : Base
{
public new void Init() { }
void Start()
{
var a = new GameObject(""A"");
var b = new GameObject(""B"");
}
}";
var errors = CallValidateScriptSyntaxUnity(code);
Assert.IsFalse(HasDuplicateMethodError(errors),
"new modifier on method should not interfere with constructor invocation filtering");
}
[Test]
public void DuplicateMethodCheck_LocalFunctionsInDifferentMethods_NotFlagged()
{
string code = @"using UnityEngine;
public class Test : MonoBehaviour
{
void Start()
{
int ClampScore(int value) { return Mathf.Clamp(value, 0, 100); }
Debug.Log(ClampScore(10));
}
void Reset()
{
int ClampScore(int value) { return Mathf.Clamp(value, 0, 100); }
Debug.Log(ClampScore(0));
}
}";
var errors = CallValidateScriptSyntaxUnity(code);
Assert.IsFalse(HasDuplicateMethodError(errors),
"Same-signature local functions in different methods are valid C# and should not be flagged as duplicate class methods");
}
[Test]
public void DuplicateMethodCheck_RepeatedMethodInvocations_NotFlagged()
{
string code = @"using UnityEngine;
public class Test : MonoBehaviour
{
int First()
{
return Compute();
}
int Second()
{
return Compute();
}
int Compute() { return 1; }
}";
var errors = CallValidateScriptSyntaxUnity(code);
Assert.IsFalse(HasDuplicateMethodError(errors),
"Repeated method invocations inside method bodies should not be treated as duplicate method declarations");
}
[Test]
public void HandleCommand_PathWithCsExtension_StripsFilename()
{
// When path ends with .cs (full file path instead of directory),
// HandleCommand should strip the filename to avoid doubled paths
// like "Assets/Scripts/Foo.cs/Foo.cs".
var paramsObj = new JObject
{
["action"] = "read",
["name"] = "TestScript",
["path"] = "Assets/Scripts/TestScript.cs"
};
var result = ManageScript.HandleCommand(paramsObj);
// The script won't exist, but the error path should NOT contain doubled filename
string json = Newtonsoft.Json.JsonConvert.SerializeObject(result);
Assert.IsFalse(json.Contains("TestScript.cs/TestScript.cs"),
"Path ending in .cs should be treated as directory, not produce doubled filename");
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b8f7e3d1c4a2b5f8e9d6c3a7b1e4f7d2
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2b53f7e50a801437e83e646cf00effed
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,307 @@
using System;
using System.Collections;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using UnityEngine.TestTools;
using MCPForUnity.Editor.Tools;
using MCPForUnityTests.Editor.Tools.Fixtures;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
public class ManageScriptableObjectTests
{
private const string TempRoot = "Assets/Temp/ManageScriptableObjectTests";
private const double UnityReadyTimeoutSeconds = 180.0;
private string _runRoot;
private string _nestedFolder;
private string _createdAssetPath;
private string _createdGuid;
private string _matAPath;
private string _matBPath;
[UnitySetUp]
public IEnumerator SetUp()
{
yield return WaitForUnityReady(UnityReadyTimeoutSeconds);
EnsureFolder("Assets/Temp");
// Avoid deleting/recreating the entire TempRoot each test (can trigger heavy reimport churn).
// Instead, isolate each test in its own unique subfolder under TempRoot.
EnsureFolder(TempRoot);
_runRoot = $"{TempRoot}/Run_{Guid.NewGuid():N}";
EnsureFolder(_runRoot);
_nestedFolder = _runRoot + "/Nested/Deeper";
_createdAssetPath = null;
_createdGuid = null;
// Create two Materials we can reference by guid/path.
_matAPath = $"{TempRoot}/MatA_{Guid.NewGuid():N}.mat";
_matBPath = $"{TempRoot}/MatB_{Guid.NewGuid():N}.mat";
var shader = FindFallbackShader();
Assert.IsNotNull(shader, "A fallback shader must be available for creating Material assets in tests.");
AssetDatabase.CreateAsset(new Material(shader), _matAPath);
AssetDatabase.CreateAsset(new Material(shader), _matBPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
yield return WaitForUnityReady(UnityReadyTimeoutSeconds);
}
[TearDown]
public void TearDown()
{
// Best-effort cleanup
if (!string.IsNullOrEmpty(_createdAssetPath) && AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(_createdAssetPath) != null)
{
AssetDatabase.DeleteAsset(_createdAssetPath);
}
if (!string.IsNullOrEmpty(_matAPath) && AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(_matAPath) != null)
{
AssetDatabase.DeleteAsset(_matAPath);
}
if (!string.IsNullOrEmpty(_matBPath) && AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(_matBPath) != null)
{
AssetDatabase.DeleteAsset(_matBPath);
}
if (!string.IsNullOrEmpty(_runRoot) && AssetDatabase.IsValidFolder(_runRoot))
{
AssetDatabase.DeleteAsset(_runRoot);
}
// Clean up empty parent folders to avoid debris
CleanupEmptyParentFolders(TempRoot);
AssetDatabase.Refresh();
}
[Test]
public void Create_CreatesNestedFolders_PlacesAssetCorrectly()
{
var create = new JObject
{
["action"] = "create",
["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName,
["folderPath"] = _nestedFolder,
["assetName"] = "My_Test_Def_Placement",
["overwrite"] = true,
};
var raw = ManageScriptableObject.HandleCommand(create);
var result = raw as JObject ?? JObject.FromObject(raw);
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"] as JObject;
Assert.IsNotNull(data, "Expected data payload");
_createdGuid = data!["guid"]?.ToString();
_createdAssetPath = data["path"]?.ToString();
Assert.IsTrue(AssetDatabase.IsValidFolder(_nestedFolder), "Nested folder should be created.");
Assert.IsTrue(_createdAssetPath!.StartsWith(_nestedFolder, StringComparison.Ordinal), $"Asset should be created under {_nestedFolder}: {_createdAssetPath}");
Assert.IsTrue(_createdAssetPath.EndsWith(".asset", StringComparison.OrdinalIgnoreCase), "Asset should have .asset extension.");
Assert.IsFalse(string.IsNullOrWhiteSpace(_createdGuid), "Expected guid in response.");
var asset = AssetDatabase.LoadAssetAtPath<ManageScriptableObjectTestDefinition>(_createdAssetPath);
Assert.IsNotNull(asset, "Created asset should load as TestDefinition.");
}
[Test]
public void Create_AppliesPatches_ToCreatedAsset()
{
var create = new JObject
{
["action"] = "create",
["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName,
// Patching correctness does not depend on nested folder creation; keep this lightweight.
["folderPath"] = _runRoot,
["assetName"] = "My_Test_Def_Patches",
["overwrite"] = true,
["patches"] = new JArray
{
new JObject { ["propertyPath"] = "displayName", ["op"] = "set", ["value"] = "Hello" },
new JObject { ["propertyPath"] = "baseNumber", ["op"] = "set", ["value"] = 42 },
new JObject { ["propertyPath"] = "nested.note", ["op"] = "set", ["value"] = "note!" }
}
};
var raw = ManageScriptableObject.HandleCommand(create);
var result = raw as JObject ?? JObject.FromObject(raw);
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"] as JObject;
Assert.IsNotNull(data, "Expected data payload");
_createdGuid = data!["guid"]?.ToString();
_createdAssetPath = data["path"]?.ToString();
Assert.IsTrue(_createdAssetPath!.StartsWith(_runRoot, StringComparison.Ordinal), $"Asset should be created under {_runRoot}: {_createdAssetPath}");
Assert.IsFalse(string.IsNullOrWhiteSpace(_createdGuid), "Expected guid in response.");
var asset = AssetDatabase.LoadAssetAtPath<ManageScriptableObjectTestDefinition>(_createdAssetPath);
Assert.IsNotNull(asset, "Created asset should load as TestDefinition.");
Assert.AreEqual("Hello", asset!.DisplayName, "Private [SerializeField] string should be set via SerializedProperty.");
Assert.AreEqual(42, asset.BaseNumber, "Inherited serialized field should be set via SerializedProperty.");
Assert.AreEqual("note!", asset.NestedNote, "Nested struct field should be set via SerializedProperty path.");
}
[Test]
public void Modify_ArrayResize_ThenAssignObjectRefs_ByGuidAndByPath()
{
// Create base asset first with no patches.
var create = new JObject
{
["action"] = "create",
["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName,
["folderPath"] = _runRoot,
["assetName"] = "Modify_Target",
["overwrite"] = true
};
var createRes = ToJObject(ManageScriptableObject.HandleCommand(create));
Assert.IsTrue(createRes.Value<bool>("success"), createRes.ToString());
_createdGuid = createRes["data"]?["guid"]?.ToString();
_createdAssetPath = createRes["data"]?["path"]?.ToString();
var matAGuid = AssetDatabase.AssetPathToGUID(_matAPath);
var modify = new JObject
{
["action"] = "modify",
["target"] = new JObject { ["guid"] = _createdGuid },
["patches"] = new JArray
{
// Resize list to 2
new JObject { ["propertyPath"] = "materials.Array.size", ["op"] = "array_resize", ["value"] = 2 },
// Assign element 0 by guid
new JObject
{
["propertyPath"] = "materials.Array.data[0]",
["op"] = "set",
["ref"] = new JObject { ["guid"] = matAGuid }
},
// Assign element 1 by path
new JObject
{
["propertyPath"] = "materials.Array.data[1]",
["op"] = "set",
["ref"] = new JObject { ["path"] = _matBPath }
}
}
};
var modRes = ToJObject(ManageScriptableObject.HandleCommand(modify));
Assert.IsTrue(modRes.Value<bool>("success"), modRes.ToString());
// Assert patch results are ok so failures are visible even if the tool returns success.
var results = modRes["data"]?["results"] as JArray;
Assert.IsNotNull(results, "Expected per-patch results in response.");
foreach (var r in results!)
{
Assert.IsTrue(r.Value<bool>("ok"), $"Patch failed: {r}");
}
var asset = AssetDatabase.LoadAssetAtPath<ManageScriptableObjectTestDefinition>(_createdAssetPath);
Assert.IsNotNull(asset);
Assert.AreEqual(2, asset!.Materials.Count, "List should be resized to 2.");
var matA = AssetDatabase.LoadAssetAtPath<Material>(_matAPath);
var matB = AssetDatabase.LoadAssetAtPath<Material>(_matBPath);
Assert.AreEqual(matA, asset.Materials[0], "Element 0 should be set by GUID ref.");
Assert.AreEqual(matB, asset.Materials[1], "Element 1 should be set by path ref.");
}
[Test]
public void Errors_InvalidAction_TypeNotFound_TargetNotFound()
{
// invalid action
var badAction = ToJObject(ManageScriptableObject.HandleCommand(new JObject { ["action"] = "nope" }));
Assert.IsFalse(badAction.Value<bool>("success"));
Assert.AreEqual("invalid_params", badAction.Value<string>("error"));
// type not found
var badType = ToJObject(ManageScriptableObject.HandleCommand(new JObject
{
["action"] = "create",
["typeName"] = "Nope.MissingType",
["folderPath"] = TempRoot,
["assetName"] = "X",
}));
Assert.IsFalse(badType.Value<bool>("success"));
Assert.AreEqual("type_not_found", badType.Value<string>("error"));
// target not found
var badTarget = ToJObject(ManageScriptableObject.HandleCommand(new JObject
{
["action"] = "modify",
["target"] = new JObject { ["guid"] = "00000000000000000000000000000000" },
["patches"] = new JArray(),
}));
Assert.IsFalse(badTarget.Value<bool>("success"));
Assert.AreEqual("target_not_found", badTarget.Value<string>("error"));
}
[Test]
public void Create_RejectsNonAssetsRootFolders()
{
var badPackages = ToJObject(ManageScriptableObject.HandleCommand(new JObject
{
["action"] = "create",
["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName,
["folderPath"] = "Packages/NotAllowed",
["assetName"] = "BadFolder",
["overwrite"] = true,
}));
Assert.IsFalse(badPackages.Value<bool>("success"));
Assert.AreEqual("invalid_folder_path", badPackages.Value<string>("error"));
var badAbsolute = ToJObject(ManageScriptableObject.HandleCommand(new JObject
{
["action"] = "create",
["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName,
["folderPath"] = "/tmp/not_allowed",
["assetName"] = "BadFolder2",
["overwrite"] = true,
}));
Assert.IsFalse(badAbsolute.Value<bool>("success"));
Assert.AreEqual("invalid_folder_path", badAbsolute.Value<string>("error"));
var badFileUri = ToJObject(ManageScriptableObject.HandleCommand(new JObject
{
["action"] = "create",
["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName,
["folderPath"] = "file:///tmp/not_allowed",
["assetName"] = "BadFolder3",
["overwrite"] = true,
}));
Assert.IsFalse(badFileUri.Value<bool>("success"));
Assert.AreEqual("invalid_folder_path", badFileUri.Value<string>("error"));
}
[Test]
public void Create_NormalizesRelativeAndBackslashPaths_AndAvoidsDoubleSlashesInResult()
{
var create = new JObject
{
["action"] = "create",
["typeName"] = typeof(ManageScriptableObjectTestDefinition).FullName,
["folderPath"] = @"Temp\ManageScriptableObjectTests\SlashProbe\\Deep",
["assetName"] = "SlashProbe",
["overwrite"] = true,
};
var res = ToJObject(ManageScriptableObject.HandleCommand(create));
Assert.IsTrue(res.Value<bool>("success"), res.ToString());
var path = res["data"]?["path"]?.ToString();
Assert.IsNotNull(path, "Expected path in response.");
Assert.IsTrue(path!.StartsWith("Assets/Temp/ManageScriptableObjectTests/SlashProbe/Deep", StringComparison.Ordinal),
$"Expected sanitized Assets-rooted path, got: {path}");
Assert.IsFalse(path.Contains("//", StringComparison.Ordinal), $"Path should not contain double slashes: {path}");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1aac13ba83f134fc2ae264408d048c9b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,903 @@
using System;
using System.IO;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using UnityEngine.TestTools;
using UnityEngine.UIElements;
using MCPForUnity.Editor.Tools;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
public class ManageUITests
{
private const string TempRoot = "Assets/Temp/ManageUITests";
[SetUp]
public void SetUp()
{
EnsureFolder(TempRoot);
}
[TearDown]
public void TearDown()
{
if (AssetDatabase.IsValidFolder(TempRoot))
{
AssetDatabase.DeleteAsset(TempRoot);
}
CleanupEmptyParentFolders(TempRoot);
}
// ---- Action validation ----
[Test]
public void HandleCommand_MissingAction_ReturnsError()
{
var result = ToJObject(ManageUI.HandleCommand(new JObject()));
Assert.IsFalse(result.Value<bool>("success"));
}
[Test]
public void HandleCommand_UnknownAction_ReturnsError()
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "explode"
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("Unknown action"));
}
[Test]
public void Ping_ReturnsPong()
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "ping"
}));
Assert.IsTrue(result.Value<bool>("success"));
Assert.AreEqual("pong", result.Value<string>("message"));
}
// ---- Create file ----
[Test]
public void Create_Uxml_CreatesFile()
{
string path = $"{TempRoot}/Test_{Guid.NewGuid():N}.uxml";
string content = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\"><ui:Label text=\"Hi\" /></ui:UXML>";
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = path,
["contents"] = content,
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
// Verify file was created on disk
string fullPath = Path.Combine(Application.dataPath,
path.Substring("Assets/".Length)).Replace('/', Path.DirectorySeparatorChar);
Assert.IsTrue(File.Exists(fullPath), $"File should exist at {fullPath}");
// EnsureEditorExtensionMode may inject editor-extension-mode attribute
string actual = File.ReadAllText(fullPath);
Assert.That(actual, Does.Contain("ui:UXML"));
Assert.That(actual, Does.Contain("ui:Label"));
}
[Test]
public void Create_Uss_CreatesFile()
{
string path = $"{TempRoot}/Test_{Guid.NewGuid():N}.uss";
string content = ".root { background-color: red; }";
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = path,
["contents"] = content,
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
}
[Test]
public void Create_InvalidExtension_ReturnsError()
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = $"{TempRoot}/Test.txt",
["contents"] = "hello",
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain(".uxml or .uss"));
}
[Test]
public void Create_MissingContents_ReturnsError()
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = $"{TempRoot}/Test.uxml",
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("contents"));
}
[Test]
public void Create_AlreadyExists_ReturnsError()
{
string path = $"{TempRoot}/Exists_{Guid.NewGuid():N}.uxml";
string content = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\" />";
// Create first time
ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = path,
["contents"] = content,
});
// Try to create again
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = path,
["contents"] = content,
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("already exists"));
}
[Test]
public void Create_WithBase64EncodedContents_Decodes()
{
string path = $"{TempRoot}/Encoded_{Guid.NewGuid():N}.uxml";
string content = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\" />";
string encoded = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(content));
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = path,
["encodedContents"] = encoded,
["contentsEncoded"] = true,
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
string fullPath = Path.Combine(Application.dataPath,
path.Substring("Assets/".Length)).Replace('/', Path.DirectorySeparatorChar);
string actual = File.ReadAllText(fullPath);
// EnsureEditorExtensionMode may inject editor-extension-mode attribute
Assert.That(actual, Does.Contain("ui:UXML"));
Assert.That(actual, Does.Contain("UnityEngine.UIElements"));
}
// ---- Read file ----
[Test]
public void Read_ExistingFile_ReturnsContents()
{
string path = $"{TempRoot}/ReadTest_{Guid.NewGuid():N}.uxml";
string content = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\" />";
ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = path,
["contents"] = content,
});
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "read",
["path"] = path,
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"] as JObject;
Assert.IsNotNull(data);
// EnsureEditorExtensionMode may inject editor-extension-mode attribute
Assert.That(data.Value<string>("contents"), Does.Contain("ui:UXML"));
}
[Test]
public void Read_NonExistentFile_ReturnsError()
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "read",
["path"] = $"{TempRoot}/DoesNotExist.uxml",
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("not found"));
}
// ---- Update file ----
[Test]
public void Update_ExistingFile_OverwritesContents()
{
string path = $"{TempRoot}/UpdateTest_{Guid.NewGuid():N}.uss";
string original = ".root { color: red; }";
string updated = ".root { color: blue; font-size: 20px; }";
ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = path,
["contents"] = original,
});
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "update",
["path"] = path,
["contents"] = updated,
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
// Verify content was updated
var readResult = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "read",
["path"] = path,
}));
Assert.AreEqual(updated, readResult["data"].Value<string>("contents"));
}
[Test]
public void Update_NonExistentFile_ReturnsError()
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "update",
["path"] = $"{TempRoot}/Missing.uxml",
["contents"] = "<ui:UXML />",
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("not found"));
}
// ---- Create PanelSettings ----
[Test]
public void CreatePanelSettings_CreatesAsset()
{
string path = $"{TempRoot}/TestPanel_{Guid.NewGuid():N}.asset";
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "create_panel_settings",
["path"] = path,
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var ps = AssetDatabase.LoadAssetAtPath<PanelSettings>(path);
Assert.IsNotNull(ps, "PanelSettings should exist at the path");
}
[Test]
public void CreatePanelSettings_AlreadyExists_ReturnsError()
{
string path = $"{TempRoot}/ExistingPanel_{Guid.NewGuid():N}.asset";
ManageUI.HandleCommand(new JObject
{
["action"] = "create_panel_settings",
["path"] = path,
});
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "create_panel_settings",
["path"] = path,
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("already exists"));
}
// ---- Attach UIDocument ----
[Test]
public void AttachUIDocument_AddsComponent()
{
// Create a UXML file first
string uxmlPath = $"{TempRoot}/Attach_{Guid.NewGuid():N}.uxml";
ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = uxmlPath,
["contents"] = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\"><ui:Label text=\"Test\" /></ui:UXML>",
});
AssetDatabase.Refresh();
// Create a test GameObject
var go = new GameObject("UITestObject_Attach");
try
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "attach_ui_document",
["target"] = go.name,
["source_asset"] = uxmlPath,
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var uiDoc = go.GetComponent<UIDocument>();
Assert.IsNotNull(uiDoc, "UIDocument component should be attached");
Assert.IsNotNull(uiDoc.visualTreeAsset, "VisualTreeAsset should be assigned");
Assert.IsNotNull(uiDoc.panelSettings, "PanelSettings should be assigned (auto-created)");
}
finally
{
UnityEngine.Object.DestroyImmediate(go);
}
}
[Test]
public void AttachUIDocument_MissingTarget_ReturnsError()
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "attach_ui_document",
["source_asset"] = "Assets/UI/Test.uxml",
}));
Assert.IsFalse(result.Value<bool>("success"));
}
[Test]
public void AttachUIDocument_MissingSourceAsset_ReturnsError()
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "attach_ui_document",
["target"] = "SomeObject",
}));
Assert.IsFalse(result.Value<bool>("success"));
}
// ---- Get Visual Tree ----
[Test]
public void GetVisualTree_MissingTarget_ReturnsError()
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "get_visual_tree",
}));
Assert.IsFalse(result.Value<bool>("success"));
}
[Test]
public void GetVisualTree_NoUIDocument_ReturnsError()
{
var go = new GameObject("UITestObject_NoDoc");
try
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "get_visual_tree",
["target"] = go.name,
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("UIDocument"));
}
finally
{
UnityEngine.Object.DestroyImmediate(go);
}
}
// ---- Delete file ----
[Test]
public void Delete_ExistingFile_DeletesFile()
{
string path = $"{TempRoot}/Delete_{Guid.NewGuid():N}.uss";
ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = path,
["contents"] = ".root { color: red; }",
});
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "delete",
["path"] = path,
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
string fullPath = Path.Combine(Application.dataPath,
path.Substring("Assets/".Length)).Replace('/', Path.DirectorySeparatorChar);
Assert.IsFalse(File.Exists(fullPath), "File should be deleted");
}
[Test]
public void Delete_NonExistentFile_ReturnsError()
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "delete",
["path"] = $"{TempRoot}/Missing.uxml",
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("not found"));
}
[Test]
public void Delete_InvalidExtension_ReturnsError()
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "delete",
["path"] = $"{TempRoot}/File.txt",
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain(".uxml or .uss"));
}
// ---- List UI assets ----
[Test]
public void List_ReturnsUIAssets()
{
string uxmlPath = $"{TempRoot}/ListTest_{Guid.NewGuid():N}.uxml";
string ussPath = $"{TempRoot}/ListTest_{Guid.NewGuid():N}.uss";
ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = uxmlPath,
["contents"] = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\" />",
});
ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = ussPath,
["contents"] = ".root { }",
});
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "list",
["path"] = TempRoot,
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"] as JObject;
Assert.IsNotNull(data);
int total = data.Value<int>("total");
Assert.GreaterOrEqual(total, 2, "Should find at least 2 UI assets");
}
[Test]
public void List_WithFilterType_FiltersResults()
{
string uxmlPath = $"{TempRoot}/FilterTest_{Guid.NewGuid():N}.uxml";
ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = uxmlPath,
["contents"] = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\" />",
});
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "list",
["path"] = TempRoot,
["filterType"] = "uxml",
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var assets = result["data"]["assets"] as JArray;
Assert.IsNotNull(assets);
foreach (var asset in assets)
{
Assert.AreEqual("uxml", asset.Value<string>("type"));
}
}
// ---- Detach UIDocument ----
[Test]
public void DetachUIDocument_RemovesComponent()
{
string uxmlPath = $"{TempRoot}/Detach_{Guid.NewGuid():N}.uxml";
ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = uxmlPath,
["contents"] = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\"><ui:Label text=\"Test\" /></ui:UXML>",
});
AssetDatabase.Refresh();
var go = new GameObject("UITestObject_Detach");
try
{
ManageUI.HandleCommand(new JObject
{
["action"] = "attach_ui_document",
["target"] = go.name,
["source_asset"] = uxmlPath,
});
Assert.IsNotNull(go.GetComponent<UIDocument>(), "UIDocument should be attached");
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "detach_ui_document",
["target"] = go.name,
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
Assert.IsNull(go.GetComponent<UIDocument>(), "UIDocument should be removed");
}
finally
{
UnityEngine.Object.DestroyImmediate(go);
}
}
[Test]
public void DetachUIDocument_NoUIDocument_ReturnsError()
{
var go = new GameObject("UITestObject_DetachNoDoc");
try
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "detach_ui_document",
["target"] = go.name,
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("UIDocument"));
}
finally
{
UnityEngine.Object.DestroyImmediate(go);
}
}
[Test]
public void DetachUIDocument_MissingTarget_ReturnsError()
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "detach_ui_document",
}));
Assert.IsFalse(result.Value<bool>("success"));
}
// ---- Modify visual element ----
[Test]
public void ModifyVisualElement_MissingTarget_ReturnsError()
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "modify_visual_element",
["elementName"] = "test",
}));
Assert.IsFalse(result.Value<bool>("success"));
}
[Test]
public void ModifyVisualElement_MissingElementName_ReturnsError()
{
var go = new GameObject("UITestObject_ModifyNoName");
try
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "modify_visual_element",
["target"] = go.name,
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("element_name"));
}
finally
{
UnityEngine.Object.DestroyImmediate(go);
}
}
[Test]
public void ModifyVisualElement_NoUIDocument_ReturnsError()
{
var go = new GameObject("UITestObject_ModifyNoDoc");
try
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "modify_visual_element",
["target"] = go.name,
["elementName"] = "test",
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("UIDocument"));
}
finally
{
UnityEngine.Object.DestroyImmediate(go);
}
}
// ---- UXML validation ----
[Test]
public void Create_MalformedXml_ReturnsError_FileNotWritten()
{
string path = $"{TempRoot}/Malformed_{Guid.NewGuid():N}.uxml";
string badContent = "<ui:UXML><ui:Label text=\"unclosed\">";
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = path,
["contents"] = badContent,
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("Malformed XML"));
// Verify file was NOT written
string fullPath = Path.Combine(Application.dataPath,
path.Substring("Assets/".Length)).Replace('/', Path.DirectorySeparatorChar);
Assert.IsFalse(File.Exists(fullPath), "Malformed UXML should not be written to disk");
}
[Test]
public void Create_MissingNamespace_WritesWithWarning()
{
string path = $"{TempRoot}/NoNs_{Guid.NewGuid():N}.uxml";
string content = "<ui:UXML><ui:Label text=\"hi\" /></ui:UXML>";
// Unity's UXML importer logs an error for undeclared 'ui' prefix
LogAssert.ignoreFailingMessages = true;
try
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = path,
["contents"] = content,
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"] as JObject;
Assert.IsNotNull(data);
var warnings = data["validationWarnings"] as JArray;
Assert.IsNotNull(warnings, "Should have validationWarnings");
Assert.That(warnings.ToString(), Does.Contain("Missing namespace"));
}
finally
{
LogAssert.ignoreFailingMessages = false;
}
}
[Test]
public void Create_ValidUxml_NoWarnings()
{
string path = $"{TempRoot}/Valid_{Guid.NewGuid():N}.uxml";
string content = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\"><ui:Label text=\"ok\" /></ui:UXML>";
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = path,
["contents"] = content,
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"] as JObject;
Assert.IsNull(data?["validationWarnings"],
"Fully valid UXML should not have validationWarnings");
}
[Test]
public void Create_WrongRootElement_WritesWithWarning()
{
string path = $"{TempRoot}/WrongRoot_{Guid.NewGuid():N}.uxml";
string content = "<div xmlns:ui=\"UnityEngine.UIElements\"><ui:Label text=\"hi\" /></div>";
// Unity's UXML importer logs an error about expected root element
LogAssert.ignoreFailingMessages = true;
try
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = path,
["contents"] = content,
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"] as JObject;
var warnings = data["validationWarnings"] as JArray;
Assert.IsNotNull(warnings, "Should have validationWarnings");
Assert.That(warnings.ToString(), Does.Contain("Root element"));
}
finally
{
LogAssert.ignoreFailingMessages = false;
}
}
[Test]
public void Create_EmptyContent_ReturnsError()
{
string path = $"{TempRoot}/Empty_{Guid.NewGuid():N}.uxml";
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = path,
["contents"] = " ",
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("empty"));
}
[Test]
public void Update_MalformedXml_ReturnsError_FileNotChanged()
{
string path = $"{TempRoot}/UpdateMalformed_{Guid.NewGuid():N}.uxml";
string original = "<ui:UXML xmlns:ui=\"UnityEngine.UIElements\" />";
ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = path,
["contents"] = original,
});
string badContent = "<ui:UXML><broken>";
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "update",
["path"] = path,
["contents"] = badContent,
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("Malformed XML"));
// Verify original content was preserved (EnsureEditorExtensionMode may have injected attribute)
string fullPath = Path.Combine(Application.dataPath,
path.Substring("Assets/".Length)).Replace('/', Path.DirectorySeparatorChar);
string actual = File.ReadAllText(fullPath);
Assert.That(actual, Does.Contain("ui:UXML"), "Original file content should be preserved");
Assert.That(actual, Does.Not.Contain("<broken>"), "Malformed content should not be written");
}
[Test]
public void Create_Uss_SkipsUxmlValidation()
{
string path = $"{TempRoot}/NoValidation_{Guid.NewGuid():N}.uss";
// USS is CSS-like, not XML — validation should be skipped
string content = "This is not valid XML <broken>";
// Unity 6000.4+'s USS importer logs an error on this content; the test only
// verifies that ManageUI itself doesn't pre-validate .uss as UXML, not the
// downstream importer's behavior.
LogAssert.ignoreFailingMessages = true;
try
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = path,
["contents"] = content,
}));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
}
finally
{
LogAssert.ignoreFailingMessages = false;
}
}
// ---- Path traversal validation ----
[Test]
public void Create_TraversalPath_ReturnsError()
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = "Assets/../etc/evil.uxml",
["contents"] = "<ui:UXML />",
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("traversal"));
}
[Test]
public void Create_DotDotInMiddle_ReturnsError()
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "create",
["path"] = "Assets/UI/../../secret.uxml",
["contents"] = "<ui:UXML />",
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("traversal"));
}
[Test]
public void Read_TraversalPath_ReturnsError()
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "read",
["path"] = "Assets/../secret.uxml",
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("traversal"));
}
[Test]
public void Update_TraversalPath_ReturnsError()
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "update",
["path"] = "Assets/../../etc/passwd.uxml",
["contents"] = "overwrite",
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("traversal"));
}
[Test]
public void Delete_TraversalPath_ReturnsError()
{
var result = ToJObject(ManageUI.HandleCommand(new JObject
{
["action"] = "delete",
["path"] = "Assets/../outside.uxml",
}));
Assert.IsFalse(result.Value<bool>("success"));
Assert.That(result["error"].ToString(), Does.Contain("traversal"));
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7b16ad7850014de1afb1696eb1d09bd1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,164 @@
using System;
using System.IO;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Tools;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
public class MaterialDirectPropertiesTests
{
private const string TempRoot = "Assets/Temp/MaterialDirectPropertiesTests";
private string _matPath;
private string _baseMapPath;
private string _normalMapPath;
private string _occlusionMapPath;
[SetUp]
public void SetUp()
{
if (!AssetDatabase.IsValidFolder("Assets/Temp"))
{
AssetDatabase.CreateFolder("Assets", "Temp");
}
if (!AssetDatabase.IsValidFolder(TempRoot))
{
AssetDatabase.CreateFolder("Assets/Temp", "MaterialDirectPropertiesTests");
}
string guid = Guid.NewGuid().ToString("N");
_matPath = $"{TempRoot}/DirectProps_{guid}.mat";
_baseMapPath = $"{TempRoot}/TexBase_{guid}.asset";
_normalMapPath = $"{TempRoot}/TexNormal_{guid}.asset";
_occlusionMapPath = $"{TempRoot}/TexOcc_{guid}.asset";
// Clean any leftovers just in case
TryDeleteAsset(_matPath);
TryDeleteAsset(_baseMapPath);
TryDeleteAsset(_normalMapPath);
TryDeleteAsset(_occlusionMapPath);
AssetDatabase.Refresh();
}
[TearDown]
public void TearDown()
{
TryDeleteAsset(_matPath);
TryDeleteAsset(_baseMapPath);
TryDeleteAsset(_normalMapPath);
TryDeleteAsset(_occlusionMapPath);
// Clean up temp directory after each test
if (AssetDatabase.IsValidFolder(TempRoot))
{
AssetDatabase.DeleteAsset(TempRoot);
}
// Clean up empty parent folders to avoid debris
CleanupEmptyParentFolders(TempRoot);
AssetDatabase.Refresh();
}
private static void TryDeleteAsset(string path)
{
if (string.IsNullOrEmpty(path)) return;
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path) != null)
{
AssetDatabase.DeleteAsset(path);
}
var abs = Path.Combine(Directory.GetCurrentDirectory(), path);
try
{
if (File.Exists(abs)) File.Delete(abs);
if (File.Exists(abs + ".meta")) File.Delete(abs + ".meta");
}
catch { }
}
private static Texture2D CreateSolidTextureAsset(string path, Color color)
{
var tex = new Texture2D(4, 4, TextureFormat.RGBA32, false);
var pixels = new Color[16];
for (int i = 0; i < pixels.Length; i++) pixels[i] = color;
tex.SetPixels(pixels);
tex.Apply();
AssetDatabase.CreateAsset(tex, path);
AssetDatabase.SaveAssets();
return tex;
}
[Test]
public void CreateAndModifyMaterial_WithDirectPropertyKeys_Works()
{
// Arrange: create textures as assets
CreateSolidTextureAsset(_baseMapPath, Color.white);
CreateSolidTextureAsset(_normalMapPath, new Color(0.5f, 0.5f, 1f));
CreateSolidTextureAsset(_occlusionMapPath, Color.gray);
// Create material using direct keys via JSON string
var createParams = new JObject
{
["action"] = "create",
["path"] = _matPath,
["assetType"] = "Material",
["properties"] = new JObject
{
["shader"] = "Universal Render Pipeline/Lit",
["_Color"] = new JArray(0f, 1f, 0f, 1f),
["_Glossiness"] = 0.25f
}
};
var createRes = ToJObject(ManageAsset.HandleCommand(createParams));
Assert.IsTrue(createRes.Value<bool>("success"), createRes.ToString());
// Modify with aliases and textures
var modifyParams = new JObject
{
["action"] = "modify",
["path"] = _matPath,
["properties"] = new JObject
{
["_BaseColor"] = new JArray(0f, 0f, 1f, 1f),
["_Smoothness"] = 0.5f,
["_BaseMap"] = _baseMapPath,
["_BumpMap"] = _normalMapPath,
["_OcclusionMap"] = _occlusionMapPath
}
};
var modifyRes = ToJObject(ManageAsset.HandleCommand(modifyParams));
Assert.IsTrue(modifyRes.Value<bool>("success"), modifyRes.ToString());
var mat = AssetDatabase.LoadAssetAtPath<Material>(_matPath);
Assert.IsNotNull(mat, "Material should exist at path.");
// Verify color alias applied
if (mat.HasProperty("_BaseColor"))
{
Assert.AreEqual(Color.blue, mat.GetColor("_BaseColor"));
}
else if (mat.HasProperty("_Color"))
{
Assert.AreEqual(Color.blue, mat.GetColor("_Color"));
}
// Verify float
string smoothProp = mat.HasProperty("_Smoothness") ? "_Smoothness" : (mat.HasProperty("_Glossiness") ? "_Glossiness" : null);
Assert.IsNotNull(smoothProp, "Material should expose Smoothness/Glossiness.");
Assert.That(Mathf.Abs(mat.GetFloat(smoothProp) - 0.5f) < 1e-4f);
// Verify textures
string baseMapProp = mat.HasProperty("_BaseMap") ? "_BaseMap" : (mat.HasProperty("_MainTex") ? "_MainTex" : null);
Assert.IsNotNull(baseMapProp, "Material should expose BaseMap/MainTex.");
Assert.IsNotNull(mat.GetTexture(baseMapProp), "BaseMap/MainTex should be assigned.");
if (mat.HasProperty("_BumpMap")) Assert.IsNotNull(mat.GetTexture("_BumpMap"));
if (mat.HasProperty("_OcclusionMap")) Assert.IsNotNull(mat.GetTexture("_OcclusionMap"));
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3623619548eef40568ac5e3cef4c22a5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,202 @@
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using MCPForUnity.Editor.Helpers;
namespace MCPForUnityTests.Editor.Tools
{
/// <summary>
/// Tests specifically for the material and mesh instantiation warnings fix.
/// These tests verify that the GameObjectSerializer uses sharedMaterial/sharedMesh
/// in edit mode to prevent Unity's instantiation warnings.
/// </summary>
public class MaterialMeshInstantiationTests
{
private GameObject testGameObject;
private Material testMaterial;
private Mesh testMesh;
[SetUp]
public void SetUp()
{
// Create a test GameObject for each test
testGameObject = new GameObject("MaterialMeshTestObject");
// Create test material and mesh
testMaterial = new Material(Shader.Find("Standard"));
testMaterial.name = "TestMaterial";
var temp = GameObject.CreatePrimitive(PrimitiveType.Cube);
testMesh = temp.GetComponent<MeshFilter>().sharedMesh;
UnityEngine.Object.DestroyImmediate(temp);
testMesh.name = "TestMesh";
}
[TearDown]
public void TearDown()
{
// Clean up test objects
if (testMaterial != null)
{
UnityEngine.Object.DestroyImmediate(testMaterial);
}
if (testGameObject != null)
{
UnityEngine.Object.DestroyImmediate(testGameObject);
}
}
[Test]
public void GetComponentData_UsesSharedMaterialInsteadOfMaterial()
{
var meshRenderer = testGameObject.AddComponent<MeshRenderer>();
meshRenderer.sharedMaterial = testMaterial;
int beforeId = meshRenderer.sharedMaterial != null ? meshRenderer.sharedMaterial.GetInstanceID() : 0;
var result = GameObjectSerializer.GetComponentData(meshRenderer);
int afterId = meshRenderer.sharedMaterial != null ? meshRenderer.sharedMaterial.GetInstanceID() : 0;
Assert.AreEqual(beforeId, afterId, "sharedMaterial instanceID must not change during edit-mode serialization (no instantiation)");
Assert.IsNotNull(result, "GetComponentData should return a result");
var propsObj = (result as Dictionary<string, object>) != null && ((Dictionary<string, object>)result).TryGetValue("properties", out var p)
? p as Dictionary<string, object>
: null;
if (propsObj != null)
{
long? foundInstanceId = null;
if (propsObj.TryGetValue("material", out var materialObj) && materialObj is Dictionary<string, object> matDict && matDict.TryGetValue("instanceID", out var idObj1) && idObj1 is long id1)
{
foundInstanceId = id1;
}
else if (propsObj.TryGetValue("sharedMaterial", out var sharedMatObj) && sharedMatObj is Dictionary<string, object> sharedMatDict && sharedMatDict.TryGetValue("instanceID", out var idObj2) && idObj2 is long id2)
{
foundInstanceId = id2;
}
else if (propsObj.TryGetValue("materials", out var materialsObj) && materialsObj is List<object> mats && mats.Count > 0 && mats[0] is Dictionary<string, object> firstMat && firstMat.TryGetValue("instanceID", out var idObj3) && idObj3 is long id3)
{
foundInstanceId = id3;
}
else if (propsObj.TryGetValue("sharedMaterials", out var sharedMaterialsObj) && sharedMaterialsObj is List<object> smats && smats.Count > 0 && smats[0] is Dictionary<string, object> firstSMat && firstSMat.TryGetValue("instanceID", out var idObj4) && idObj4 is long id4)
{
foundInstanceId = id4;
}
if (foundInstanceId.HasValue)
{
Assert.AreEqual(beforeId, (int)foundInstanceId.Value, "Serialized material must reference the sharedMaterial instance");
}
}
}
[Test]
public void GetComponentData_UsesSharedMeshInsteadOfMesh()
{
var meshFilter = testGameObject.AddComponent<MeshFilter>();
var uniqueMesh = UnityEngine.Object.Instantiate(testMesh);
meshFilter.sharedMesh = uniqueMesh;
int beforeId = meshFilter.sharedMesh != null ? meshFilter.sharedMesh.GetInstanceID() : 0;
var result = GameObjectSerializer.GetComponentData(meshFilter);
int afterId = meshFilter.sharedMesh != null ? meshFilter.sharedMesh.GetInstanceID() : 0;
Assert.AreEqual(beforeId, afterId, "sharedMesh instanceID must not change during edit-mode serialization (no instantiation)");
Assert.IsNotNull(result, "GetComponentData should return a result");
var propsObj = (result as Dictionary<string, object>) != null && ((Dictionary<string, object>)result).TryGetValue("properties", out var p)
? p as Dictionary<string, object>
: null;
if (propsObj != null)
{
long? foundInstanceId = null;
if (propsObj.TryGetValue("mesh", out var meshObj) && meshObj is Dictionary<string, object> meshDict && meshDict.TryGetValue("instanceID", out var idObj1) && idObj1 is long id1)
{
foundInstanceId = id1;
}
else if (propsObj.TryGetValue("sharedMesh", out var sharedMeshObj) && sharedMeshObj is Dictionary<string, object> sharedMeshDict && sharedMeshDict.TryGetValue("instanceID", out var idObj2) && idObj2 is long id2)
{
foundInstanceId = id2;
}
if (foundInstanceId.HasValue)
{
Assert.AreEqual(beforeId, (int)foundInstanceId.Value, "Serialized mesh must reference the sharedMesh instance");
}
}
// Clean up the instantiated mesh
UnityEngine.Object.DestroyImmediate(uniqueMesh);
}
// (The two strong tests above replace the prior lighter-weight versions.)
[Test]
public void GetComponentData_HandlesNullSharedMaterial()
{
// Arrange - Create MeshRenderer without setting shared material
var meshRenderer = testGameObject.AddComponent<MeshRenderer>();
// Don't set sharedMaterial - it should be null
// Act - Get component data
var result = GameObjectSerializer.GetComponentData(meshRenderer);
// Assert - Should handle null shared material gracefully
Assert.IsNotNull(result, "GetComponentData should handle null shared material");
}
[Test]
public void GetComponentData_HandlesNullSharedMesh()
{
// Arrange - Create MeshFilter without setting shared mesh
var meshFilter = testGameObject.AddComponent<MeshFilter>();
// Don't set sharedMesh - it should be null
// Act - Get component data
var result = GameObjectSerializer.GetComponentData(meshFilter);
// Assert - Should handle null shared mesh gracefully
Assert.IsNotNull(result, "GetComponentData should handle null shared mesh");
}
[Test]
public void GetComponentData_WorksWithMultipleSharedMaterials()
{
// Arrange - Create MeshRenderer with multiple shared materials
var meshRenderer = testGameObject.AddComponent<MeshRenderer>();
var material1 = new Material(Shader.Find("Standard"));
material1.name = "TestMaterial1";
var material2 = new Material(Shader.Find("Standard"));
material2.name = "TestMaterial2";
meshRenderer.sharedMaterials = new Material[] { material1, material2 };
// Act - Get component data
var result = GameObjectSerializer.GetComponentData(meshRenderer);
// Assert - Should handle multiple shared materials
Assert.IsNotNull(result, "GetComponentData should handle multiple shared materials");
// Clean up additional materials
UnityEngine.Object.DestroyImmediate(material1);
UnityEngine.Object.DestroyImmediate(material2);
}
[Test]
public void GetComponentData_EditModeDetectionWorks()
{
// This test verifies that our edit mode detection is working
// We can't easily test Application.isPlaying directly, but we can verify
// that the behavior is consistent with edit mode expectations
// Arrange - Create components that would trigger warnings in edit mode
var meshRenderer = testGameObject.AddComponent<MeshRenderer>();
var meshFilter = testGameObject.AddComponent<MeshFilter>();
meshRenderer.sharedMaterial = testMaterial;
meshFilter.sharedMesh = testMesh;
// Act - Get component data multiple times
var rendererResult = GameObjectSerializer.GetComponentData(meshRenderer);
var meshFilterResult = GameObjectSerializer.GetComponentData(meshFilter);
// Assert - Both operations should succeed without warnings
Assert.IsNotNull(rendererResult, "MeshRenderer serialization should work in edit mode");
Assert.IsNotNull(meshFilterResult, "MeshFilter serialization should work in edit mode");
}
// Removed low-value property-presence tests; the instanceID tests are the authoritative guardrails.
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f67ba1d248b564c97b1afa12caae0196
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,167 @@
using System;
using System.IO;
using MCPForUnity.Editor.Tools;
using MCPForUnity.Editor.Tools.GameObjects;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
public class MaterialParameterToolTests
{
private const string TempRoot = "Assets/Temp/MaterialParameterToolTests";
private string _matPath; // unique per test run
private GameObject _sphere;
[SetUp]
public void SetUp()
{
_matPath = $"{TempRoot}/BlueURP_{Guid.NewGuid().ToString("N")}.mat";
if (!AssetDatabase.IsValidFolder("Assets/Temp"))
{
AssetDatabase.CreateFolder("Assets", "Temp");
}
if (!AssetDatabase.IsValidFolder(TempRoot))
{
AssetDatabase.CreateFolder("Assets/Temp", "MaterialParameterToolTests");
}
// Ensure any leftover material from previous runs is removed
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(_matPath) != null)
{
AssetDatabase.DeleteAsset(_matPath);
AssetDatabase.Refresh();
}
// Hard-delete any stray files on disk (in case GUID lookup fails)
var abs = Path.Combine(Directory.GetCurrentDirectory(), _matPath);
try
{
if (File.Exists(abs)) File.Delete(abs);
if (File.Exists(abs + ".meta")) File.Delete(abs + ".meta");
}
catch { /* best-effort cleanup */ }
AssetDatabase.Refresh();
}
[TearDown]
public void TearDown()
{
if (_sphere != null)
{
UnityEngine.Object.DestroyImmediate(_sphere);
_sphere = null;
}
if (AssetDatabase.LoadAssetAtPath<Material>(_matPath) != null)
{
AssetDatabase.DeleteAsset(_matPath);
}
// Clean up temp directory after each test
if (AssetDatabase.IsValidFolder(TempRoot))
{
AssetDatabase.DeleteAsset(TempRoot);
}
// Clean up empty parent folders to avoid debris
CleanupEmptyParentFolders(TempRoot);
AssetDatabase.Refresh();
}
[Test]
public void CreateMaterial_WithObjectProperties_SucceedsAndSetsColor()
{
// Ensure a clean state if a previous run left the asset behind (uses _matPath now)
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(_matPath) != null)
{
AssetDatabase.DeleteAsset(_matPath);
AssetDatabase.Refresh();
}
var createParams = new JObject
{
["action"] = "create",
["path"] = _matPath,
["assetType"] = "Material",
["properties"] = new JObject
{
["shader"] = "Universal Render Pipeline/Lit",
["color"] = new JArray(0f, 0f, 1f, 1f)
}
};
var result = ToJObject(ManageAsset.HandleCommand(createParams));
Assert.IsTrue(result.Value<bool>("success"), result.Value<string>("error"));
var mat = AssetDatabase.LoadAssetAtPath<Material>(_matPath);
Assert.IsNotNull(mat, "Material should exist at path.");
// Verify color if shader exposes _Color
if (mat.HasProperty("_Color"))
{
Assert.AreEqual(Color.blue, mat.GetColor("_Color"));
}
}
[Test]
public void AssignMaterial_ToSphere_UsingManageMaterial_Succeeds()
{
// Ensure material exists first
CreateMaterial_WithObjectProperties_SucceedsAndSetsColor();
// Create a sphere via handler
var createGo = new JObject
{
["action"] = "create",
["name"] = "ToolTestSphere",
["primitiveType"] = "Sphere"
};
var createGoResult = ToJObject(ManageGameObject.HandleCommand(createGo));
Assert.IsTrue(createGoResult.Value<bool>("success"), createGoResult.Value<string>("error"));
_sphere = GameObject.Find("ToolTestSphere");
Assert.IsNotNull(_sphere, "Sphere should be created.");
// Assign material via ManageMaterial tool
var assignParams = new JObject
{
["action"] = "assign_material_to_renderer",
["target"] = "ToolTestSphere",
["searchMethod"] = "by_name",
["materialPath"] = _matPath,
["slot"] = 0
};
var assignResult = ToJObject(ManageMaterial.HandleCommand(assignParams));
Assert.IsTrue(assignResult.Value<bool>("success"), assignResult.ToString());
var renderer = _sphere.GetComponent<MeshRenderer>();
Assert.IsNotNull(renderer, "Sphere should have MeshRenderer.");
Assert.IsNotNull(renderer.sharedMaterial, "sharedMaterial should be assigned.");
StringAssert.StartsWith("BlueURP_", renderer.sharedMaterial.name);
}
[Test]
public void ReadRendererData_DoesNotInstantiateMaterial_AndIncludesSharedMaterial()
{
// Prepare object and assignment
AssignMaterial_ToSphere_UsingManageMaterial_Succeeds();
var renderer = _sphere.GetComponent<MeshRenderer>();
int beforeId = renderer.sharedMaterial != null ? renderer.sharedMaterial.GetInstanceID() : 0;
var data = MCPForUnity.Editor.Helpers.GameObjectSerializer.GetComponentData(renderer) as System.Collections.Generic.Dictionary<string, object>;
Assert.IsNotNull(data, "Serializer should return data.");
int afterId = renderer.sharedMaterial != null ? renderer.sharedMaterial.GetInstanceID() : 0;
Assert.AreEqual(beforeId, afterId, "sharedMaterial instance must not change (no instantiation in EditMode).");
if (data.TryGetValue("properties", out var propsObj) && propsObj is System.Collections.Generic.Dictionary<string, object> props)
{
Assert.IsTrue(
props.ContainsKey("sharedMaterial") || props.ContainsKey("material") || props.ContainsKey("sharedMaterials") || props.ContainsKey("materials"),
"Serialized data should include material info.");
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bd76b616a816c47a79c4a3da4c307cff
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,328 @@
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
{
/// <summary>
/// Tests to reproduce issue #654: PropertyConversion crash causing dispatcher unavailability
/// while telemetry continues reporting success.
/// </summary>
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);
}
}
/// <summary>
/// Test case 1: Integer value for object reference property (AudioClip on AudioSource)
/// Should return graceful error, not crash dispatcher
/// </summary>
[Test]
public void ManageComponents_SetProperty_IntegerForObjectReference_ReturnsGracefulError()
{
// Add AudioSource component
var audioSource = testGameObject.AddComponent<AudioSource>();
// 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");
}
}
/// <summary>
/// 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
/// </summary>
[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<AudioSource>();
// 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");
}
/// <summary>
/// Test case 3: Multiple property conversion failures in sequence
/// Tests if dispatcher remains responsive after multiple errors
/// </summary>
[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<AudioSource>();
// 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)");
}
/// <summary>
/// Test case 4: After property conversion failures, other commands still work
/// Tests dispatcher responsiveness
/// </summary>
[Test]
public void ManageComponents_AfterConversionFailure_OtherOperationsWork()
{
var audioSource = testGameObject.AddComponent<AudioSource>();
// 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");
}
/// <summary>
/// Test case 5: Telemetry continues reporting success even after conversion errors
/// This is the core of issue #654: telemetry should accurately reflect dispatcher health
/// </summary>
[Test]
public void ManageEditor_TelemetryStatus_ReportsAccurateHealth()
{
// Trigger multiple conversion failures first
var audioSource = testGameObject.AddComponent<AudioSource>();
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.
}
/// <summary>
/// Test case 6: Direct PropertyConversion error handling
/// Tests if PropertyConversion.ConvertToType properly handles exceptions
/// </summary>
[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");
}
}
/// <summary>
/// Test case 7: TryConvertToType should never throw
/// </summary>
[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
}
/// <summary>
/// Test case 8: ComponentOps error handling
/// Tests if ComponentOps.SetProperty properly catches exceptions
/// </summary>
[Test]
public void ComponentOps_SetProperty_HandlesConversionErrors()
{
var audioSource = testGameObject.AddComponent<AudioSource>();
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)");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 815e2cf338526014d93708b9070b7fc5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,58 @@
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
{
/// <summary>
/// ISOLATED TEST: Array format [0, 0] for float property
/// Issue #654 - Test 2 of 8
/// This is the test that triggers "Error converting token to System.Single"
/// </summary>
public class PropertyConversion_ArrayForFloat_Test
{
private GameObject testGameObject;
[SetUp]
public void SetUp()
{
testGameObject = new GameObject("PropertyConversion_ArrayForFloat_Test");
}
[TearDown]
public void TearDown()
{
if (testGameObject != null)
{
UnityEngine.Object.DestroyImmediate(testGameObject);
}
}
[Test]
public void SetProperty_ArrayForFloat_ReturnsError()
{
// Expect the error log that will be generated
LogAssert.Expect(LogType.Error, new Regex("Error converting token to System.Single"));
var audioSource = testGameObject.AddComponent<AudioSource>();
// This is the exact error from issue #654
var setPropertyParams = new JObject
{
["action"] = "set_property",
["target"] = testGameObject.name,
["componentType"] = "AudioSource",
["property"] = "spatialBlend",
["value"] = JArray.Parse("[0, 0]") // Array for float = error
};
var result = ManageComponents.HandleCommand(setPropertyParams);
Assert.IsNotNull(result, "Should return a result");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c4b99eb57f53db948bd5e8a0a08dfec8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,116 @@
using System;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using UnityEngine;
using MCPForUnity.Editor.Tools;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
public class ReadConsoleTests
{
[Test]
public void HandleCommand_Clear_Works()
{
// Arrange
// Ensure there's something to clear
Debug.Log("Log to clear");
// Verify content exists before clear
var getBefore = ToJObject(ReadConsole.HandleCommand(new JObject { ["action"] = "get", ["types"] = new JArray { "error", "warning", "log" }, ["count"] = 10 }));
Assert.IsTrue(getBefore.Value<bool>("success"), getBefore.ToString());
var entriesBefore = getBefore["data"] as JArray;
// Ideally we'd assert count > 0, but other tests/system logs might affect this.
// Just ensuring the call doesn't fail is a baseline, but let's try to be stricter if possible.
// Since we just logged, there should be at least one entry.
Assert.IsTrue(entriesBefore != null && entriesBefore.Count > 0, "Setup failed: console should have logs.");
// Act
var result = ToJObject(ReadConsole.HandleCommand(new JObject { ["action"] = "clear" }));
// Assert
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
// Verify clear effect
var getAfter = ToJObject(ReadConsole.HandleCommand(new JObject { ["action"] = "get", ["types"] = new JArray { "error", "warning", "log" }, ["count"] = 10 }));
Assert.IsTrue(getAfter.Value<bool>("success"), getAfter.ToString());
var entriesAfter = getAfter["data"] as JArray;
Assert.IsTrue(entriesAfter == null || entriesAfter.Count == 0, "Console should be empty after clear.");
}
[Test]
public void HandleCommand_Get_Works()
{
// Arrange
string uniqueMessage = $"Test Log Message {Guid.NewGuid()}";
Debug.Log(uniqueMessage);
var paramsObj = new JObject
{
["action"] = "get",
["types"] = new JArray { "error", "warning", "log" },
["format"] = "detailed",
["count"] = 1000 // Fetch enough to likely catch our message
};
// Act
var result = ToJObject(ReadConsole.HandleCommand(paramsObj));
// Assert
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"] as JArray;
Assert.IsNotNull(data, "Data array should not be null.");
Assert.IsTrue(data.Count > 0, "Should retrieve at least one log entry.");
// Verify content
bool found = false;
foreach (var entry in data)
{
if (entry["message"]?.ToString().Contains(uniqueMessage) == true)
{
found = true;
break;
}
}
Assert.IsTrue(found, $"The unique log message '{uniqueMessage}' was not found in retrieved logs.");
}
[Test]
public void HandleCommand_Get_PreservesMultilineMessageBody()
{
string id = Guid.NewGuid().ToString();
string firstLine = $"First line {id}";
string secondLine = $"Second line {id}";
Debug.Log($"{firstLine}\n\n{secondLine}");
var paramsObj = new JObject
{
["action"] = "get",
["types"] = new JArray { "error", "warning", "log" },
["format"] = "detailed",
["count"] = 1000
};
var result = ToJObject(ReadConsole.HandleCommand(paramsObj));
Assert.IsTrue(result.Value<bool>("success"), result.ToString());
var data = result["data"] as JArray;
Assert.IsNotNull(data, "Data array should not be null.");
string message = null;
foreach (var entry in data)
{
string candidate = entry["message"]?.ToString();
if (candidate != null && candidate.Contains(firstLine))
{
message = candidate;
break;
}
}
Assert.IsNotNull(message, "Multi-line log entry was not found.");
StringAssert.Contains($"{firstLine}\n\n{secondLine}", message);
StringAssert.DoesNotContain("UnityEngine.Debug", message);
}
}
}

Some files were not shown because too many files have changed in this diff Show More