Harden `manage_scriptable_object` Tool (#522)

* feat(manage_scriptable_object): harden tool with path normalization, auto-resize, bulk mapping

Phase 1: Path Syntax & Auto-Resizing
- Add NormalizePropertyPath() to convert field[index] to Array.data format
- Add EnsureArrayCapacity() to auto-grow arrays when targeting out-of-bounds indices

Phase 2: Consolidation
- Replace duplicate TryGet* helpers with ParamCoercion/VectorParsing shared utilities
- Add Vector4 parsing support to VectorParsing.cs

Phase 3: Bulk Data Mapping
- Handle JArray values for list/array properties (recursive element setting)
- Handle JObject values for nested struct/class properties

Phase 4: Enhanced Reference Resolution
- Support plain 32-char GUID strings for ObjectReference fields

Phase 5: Validation & Dry-Run
- Add ValidatePatches() for pre-validation of all patches
- Add dry_run parameter to validate without mutating

Includes comprehensive stress test suite covering:
- Big Bang (large nested arrays), Out of Bounds, Friendly Path Syntax
- Deep Nesting, Mixed References, Rapid Fire, Type Mismatch
- Bulk Array Mapping, GUID Shorthand, Dry Run validation

* feat: Add AnimationCurve and Quaternion support to manage_scriptable_object tool

- Implement TrySetAnimationCurve() supporting both {'keys': [...]} and direct [...] formats
  * Support keyframe properties: time, value, inSlope, outSlope, weightedMode, inWeight, outWeight
  * Gracefully default missing optional fields to 0
  * Clear error messages for malformed structures

- Implement TrySetQuaternion() with 4 input formats:
  * Euler array [x, y, z] - 3 elements interpreted as degrees
  * Raw array [x, y, z, w] - 4 components
  * Object format {x, y, z, w} - explicit components
  * Explicit euler {euler: [x, y, z]} - labeled format

- Improve error handling:
  * Null values: AnimationCurve→empty, Quaternion→identity
  * Invalid inputs rejected with specific, actionable error messages
  * Validate keyframe objects and array sizes

- Add comprehensive test coverage in ManageScriptableObjectStressTests.cs:
  * AnimationCurve with keyframe array format
  * AnimationCurve with direct array (no wrapper)
  * Quaternion via Euler angles
  * Quaternion via raw components
  * Quaternion via object format
  * Quaternion via explicit euler property

- Fix test file compilation issues:
  * Replace undefined TestFolder with _runRoot
  * Add System.IO using statement

* refactor: consolidate test utilities to eliminate duplication

- Add TestUtilities.cs with shared helpers:
  - ToJObject() - consolidates 11 duplicates across test files
  - EnsureFolder() - consolidates 2 duplicates
  - WaitForUnityReady() - consolidates 2 duplicates
  - FindFallbackShader() - consolidates shader chain duplicates
  - SafeDeleteAsset() - helper for asset cleanup
  - CleanupEmptyParentFolders() - standardizes TearDown cleanup

- Update 11 test files to use shared TestUtilities via 'using static'
- Standardize TearDown cleanup patterns across all test files
- Net reduction of ~40 lines while improving maintainability

* fix: add missing animCurve and rotation fields to ComplexStressSO

Add AnimationCurve and Quaternion fields required by Phase 6 stress tests.
main
dsarno 2026-01-07 06:46:35 -08:00 committed by GitHub
parent 3090a014fb
commit 552b2d3aae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 2250 additions and 350 deletions

View File

@ -97,6 +97,49 @@ namespace MCPForUnity.Editor.Helpers
return null;
}
/// <summary>
/// Parses a JToken (array or object) into a Vector4.
/// </summary>
/// <param name="token">The JSON token to parse</param>
/// <returns>The parsed Vector4 or null if parsing fails</returns>
public static Vector4? ParseVector4(JToken token)
{
if (token == null || token.Type == JTokenType.Null)
return null;
try
{
// Array format: [x, y, z, w]
if (token is JArray array && array.Count >= 4)
{
return new Vector4(
array[0].ToObject<float>(),
array[1].ToObject<float>(),
array[2].ToObject<float>(),
array[3].ToObject<float>()
);
}
// Object format: {x: 1, y: 2, z: 3, w: 4}
if (token is JObject obj && obj.ContainsKey("x") && obj.ContainsKey("y") &&
obj.ContainsKey("z") && obj.ContainsKey("w"))
{
return new Vector4(
obj["x"].ToObject<float>(),
obj["y"].ToObject<float>(),
obj["z"].ToObject<float>(),
obj["w"].ToObject<float>()
);
}
}
catch (Exception ex)
{
Debug.LogWarning($"[VectorParsing] Failed to parse Vector4 from '{token}': {ex.Message}");
}
return null;
}
/// <summary>
/// Parses a JToken (array or object) into a Quaternion.
/// Supports both euler angles [x, y, z] and quaternion components [x, y, z, w].

File diff suppressed because it is too large Load Diff

View File

@ -41,6 +41,8 @@ async def manage_scriptable_object(
target: Annotated[dict[str, Any] | str | None, "Target asset reference {guid|path} (for modify)."] = None,
# --- shared ---
patches: Annotated[list[dict[str, Any]] | str | None, "Patch list (or JSON string) to apply."] = None,
# --- validation ---
dry_run: Annotated[bool | str | None, "If true, validate patches without applying (modify only)."] = None,
) -> dict[str, Any]:
unity_instance = get_unity_instance_from_context(ctx)
@ -62,6 +64,7 @@ async def manage_scriptable_object(
"overwrite": coerce_bool(overwrite, default=None),
"target": parsed_target,
"patches": parsed_patches,
"dryRun": coerce_bool(dry_run, default=None),
}
# Remove None values to keep Unity handler simpler

View File

@ -69,4 +69,62 @@ def test_manage_scriptable_object_forwards_modify_params(monkeypatch):
assert captured["params"]["patches"][0]["op"] == "array_resize"
def test_manage_scriptable_object_forwards_dry_run_param(monkeypatch):
captured = {}
async def fake_async_send(cmd, params, **kwargs):
captured["cmd"] = cmd
captured["params"] = params
return {"success": True, "data": {"dryRun": True, "validationResults": []}}
monkeypatch.setattr(mod, "async_send_command_with_retry", fake_async_send)
ctx = DummyContext()
ctx.set_state("unity_instance", "UnityMCPTests@dummy")
result = asyncio.run(
mod.manage_scriptable_object(
ctx=ctx,
action="modify",
target='{"guid":"abc123"}',
patches=[{"propertyPath": "intValue", "op": "set", "value": 42}],
dry_run=True,
)
)
assert result["success"] is True
assert captured["cmd"] == "manage_scriptable_object"
assert captured["params"]["action"] == "modify"
assert captured["params"]["dryRun"] is True
assert captured["params"]["target"] == {"guid": "abc123"}
def test_manage_scriptable_object_dry_run_string_coercion(monkeypatch):
"""Test that dry_run accepts string 'true' and coerces to boolean."""
captured = {}
async def fake_async_send(cmd, params, **kwargs):
captured["cmd"] = cmd
captured["params"] = params
return {"success": True, "data": {"dryRun": True}}
monkeypatch.setattr(mod, "async_send_command_with_retry", fake_async_send)
ctx = DummyContext()
ctx.set_state("unity_instance", "UnityMCPTests@dummy")
result = asyncio.run(
mod.manage_scriptable_object(
ctx=ctx,
action="modify",
target={"guid": "xyz"},
patches=[],
dry_run="true", # String instead of bool
)
)
assert result["success"] is True
assert captured["params"]["dryRun"] is True

View File

@ -0,0 +1,140 @@
using System;
using System.Collections;
using System.IO;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
namespace MCPForUnityTests.Editor
{
/// <summary>
/// Shared test utilities for EditMode tests across the MCP for Unity test suite.
/// Consolidates common patterns to avoid duplication across test files.
/// </summary>
public static class TestUtilities
{
/// <summary>
/// Safely converts a command result to JObject, handling both JSON objects and other types.
/// Returns an empty JObject if result is null.
/// </summary>
public static JObject ToJObject(object result)
{
if (result == null) return new JObject();
return result as JObject ?? JObject.FromObject(result);
}
/// <summary>
/// Creates all parent directories for the given asset path if they don't exist.
/// Handles normalization and validates against dangerous patterns.
/// </summary>
/// <param name="folderPath">An Assets-relative folder path (e.g., "Assets/Temp/MyFolder")</param>
public static void EnsureFolder(string folderPath)
{
if (AssetDatabase.IsValidFolder(folderPath))
return;
var sanitized = MCPForUnity.Editor.Helpers.AssetPathUtility.SanitizeAssetPath(folderPath);
if (string.Equals(sanitized, "Assets", StringComparison.OrdinalIgnoreCase))
return;
var parts = sanitized.Split('/');
string current = "Assets";
for (int i = 1; i < parts.Length; i++)
{
var next = current + "/" + parts[i];
if (!AssetDatabase.IsValidFolder(next))
{
AssetDatabase.CreateFolder(current, parts[i]);
}
current = next;
}
}
/// <summary>
/// Waits for Unity to finish compiling and updating, with a configurable timeout.
/// Some EditMode tests trigger script compilation/domain reload.
/// Tools intentionally return "compiling_or_reloading" during these windows.
/// </summary>
/// <param name="timeoutSeconds">Maximum time to wait before failing the test.</param>
public static IEnumerator WaitForUnityReady(double timeoutSeconds = 30.0)
{
double start = EditorApplication.timeSinceStartup;
while (EditorApplication.isCompiling || EditorApplication.isUpdating)
{
if (EditorApplication.timeSinceStartup - start > timeoutSeconds)
{
Assert.Fail($"Timed out waiting for Unity to finish compiling/updating (>{timeoutSeconds:0.0}s).");
}
yield return null;
}
}
/// <summary>
/// Finds a fallback shader for creating materials in tests.
/// Tries modern pipelines first, then falls back to Standard/Unlit.
/// </summary>
/// <returns>A shader suitable for test materials, or null if none found.</returns>
public static Shader FindFallbackShader()
{
return Shader.Find("Universal Render Pipeline/Lit")
?? Shader.Find("HDRP/Lit")
?? Shader.Find("Standard")
?? Shader.Find("Unlit/Color");
}
/// <summary>
/// Safely deletes an asset if it exists.
/// </summary>
/// <param name="path">The asset path to delete.</param>
public static void SafeDeleteAsset(string path)
{
if (!string.IsNullOrEmpty(path) && AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path) != null)
{
AssetDatabase.DeleteAsset(path);
}
}
/// <summary>
/// Cleans up empty parent folders recursively up to but not including "Assets".
/// Useful in TearDown to avoid leaving folder debris.
/// </summary>
/// <param name="folderPath">The starting folder path to check.</param>
public static void CleanupEmptyParentFolders(string folderPath)
{
if (string.IsNullOrEmpty(folderPath))
return;
var parent = Path.GetDirectoryName(folderPath)?.Replace('\\', '/');
while (!string.IsNullOrEmpty(parent) && parent != "Assets")
{
if (AssetDatabase.IsValidFolder(parent))
{
try
{
var dirs = Directory.GetDirectories(parent);
var files = Directory.GetFiles(parent);
if (dirs.Length == 0 && files.Length == 0)
{
AssetDatabase.DeleteAsset(parent);
parent = Path.GetDirectoryName(parent)?.Replace('\\', '/');
}
else
{
break;
}
}
catch
{
break;
}
}
else
{
break;
}
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4bf7029284fff48b6a5e003e262ab5c8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e8f17bd366ad941fc95a0b60a727a90d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,7 @@
using UnityEngine;
[CreateAssetMenu(fileName = "ArrayStressSO", menuName = "StressTests/ArrayStressSO")]
public class ArrayStressSO : ScriptableObject
{
public float[] floatArray = new float[3];
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9eec250e4deee48c69c12acfde8c2adc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a73b1ff3fe1fa4b3fadb70c7c257d5a8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cc8fe16aef3ae4cbc949300f5fed2187
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -6,10 +6,10 @@ using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Tools;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Resources.Scene;
using UnityEngine.TestTools;
using Debug = UnityEngine.Debug;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
@ -51,21 +51,6 @@ namespace MCPForUnityTests.Editor.Tools
return go;
}
private static JObject ToJObject(object result)
{
if (result == null) return new JObject();
if (result is JObject jobj) return jobj;
try
{
return JObject.FromObject(result);
}
catch (Exception ex)
{
Debug.LogWarning($"[ToJObject] Failed to convert result: {ex.Message}");
return new JObject { ["error"] = ex.Message };
}
}
#region Bulk GameObject Creation
[Test]

View File

@ -4,6 +4,7 @@ using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Tools;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
@ -33,11 +34,9 @@ namespace MCPForUnityTests.Editor.Tools
{
AssetDatabase.DeleteAsset(TempRoot);
}
}
private static JObject ToJObject(object result)
{
return result as JObject ?? JObject.FromObject(result);
// Clean up empty parent folders to avoid debris
CleanupEmptyParentFolders(TempRoot);
}
[Test]

View File

@ -4,6 +4,7 @@ using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Tools;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
@ -35,11 +36,9 @@ namespace MCPForUnityTests.Editor.Tools
{
AssetDatabase.DeleteAsset(TempRoot);
}
}
private static JObject ToJObject(object result)
{
return result as JObject ?? JObject.FromObject(result);
// Clean up empty parent folders to avoid debris
CleanupEmptyParentFolders(TempRoot);
}
[Test]

View File

@ -5,7 +5,7 @@ using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Tools;
using MCPForUnity.Editor.Helpers;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
@ -52,21 +52,8 @@ namespace MCPForUnityTests.Editor.Tools
AssetDatabase.DeleteAsset(TempRoot);
}
// Clean up parent Temp folder if it's empty
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");
}
}
}
private static JObject ToJObject(object result)
{
return result as JObject ?? JObject.FromObject(result);
// Clean up empty parent folders to avoid debris
CleanupEmptyParentFolders(TempRoot);
}
[Test]

View File

@ -5,7 +5,7 @@ using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Tools;
using MCPForUnity.Editor.Helpers;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
@ -43,21 +43,8 @@ namespace MCPForUnityTests.Editor.Tools
AssetDatabase.DeleteAsset(TempRoot);
}
// Clean up parent Temp folder if it's empty
if (AssetDatabase.IsValidFolder("Assets/Temp"))
{
// Only delete if empty
var subFolders = AssetDatabase.GetSubFolders("Assets/Temp");
if (subFolders.Length == 0)
{
AssetDatabase.DeleteAsset("Assets/Temp");
}
}
}
private static JObject ToJObject(object result)
{
return result as JObject ?? JObject.FromObject(result);
// Clean up empty parent folders to avoid debris
CleanupEmptyParentFolders(TempRoot);
}
[Test]

View File

@ -5,6 +5,7 @@ using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using MCPForUnity.Editor.Tools.Prefabs;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
@ -30,16 +31,8 @@ namespace MCPForUnityTests.Editor.Tools
AssetDatabase.DeleteAsset(TempDirectory);
}
// Clean up parent Temp folder if it's empty
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");
}
}
// Clean up empty parent folders to avoid debris
CleanupEmptyParentFolders(TempDirectory);
}
[Test]
@ -252,10 +245,5 @@ namespace MCPForUnityTests.Editor.Tools
AssetDatabase.CreateFolder("Assets/Temp", "ManagePrefabsTests");
}
}
private static JObject ToJObject(object result)
{
return result as JObject ?? JObject.FromObject(result);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2b53f7e50a801437e83e646cf00effed
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -5,9 +5,9 @@ using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using UnityEngine.TestTools;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Tools;
using MCPForUnityTests.Editor.Tools.Fixtures;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
@ -41,10 +41,7 @@ namespace MCPForUnityTests.Editor.Tools
// 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 = Shader.Find("Universal Render Pipeline/Lit")
?? Shader.Find("HDRP/Lit")
?? Shader.Find("Standard")
?? Shader.Find("Unlit/Color");
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);
@ -75,16 +72,8 @@ namespace MCPForUnityTests.Editor.Tools
AssetDatabase.DeleteAsset(_runRoot);
}
// Clean up parent Temp folder if empty
if (AssetDatabase.IsValidFolder("Assets/Temp"))
{
var remainingDirs = System.IO.Directory.GetDirectories("Assets/Temp");
var remainingFiles = System.IO.Directory.GetFiles("Assets/Temp");
if (remainingDirs.Length == 0 && remainingFiles.Length == 0)
{
AssetDatabase.DeleteAsset("Assets/Temp");
}
}
// Clean up empty parent folders to avoid debris
CleanupEmptyParentFolders(TempRoot);
AssetDatabase.Refresh();
}
@ -312,50 +301,6 @@ namespace MCPForUnityTests.Editor.Tools
$"Expected sanitized Assets-rooted path, got: {path}");
Assert.IsFalse(path.Contains("//", StringComparison.Ordinal), $"Path should not contain double slashes: {path}");
}
private static void EnsureFolder(string folderPath)
{
if (AssetDatabase.IsValidFolder(folderPath))
return;
// Only used for Assets/... paths in tests.
var sanitized = AssetPathUtility.SanitizeAssetPath(folderPath);
if (string.Equals(sanitized, "Assets", StringComparison.OrdinalIgnoreCase))
return;
var parts = sanitized.Split('/');
string current = "Assets";
for (int i = 1; i < parts.Length; i++)
{
var next = current + "/" + parts[i];
if (!AssetDatabase.IsValidFolder(next))
{
AssetDatabase.CreateFolder(current, parts[i]);
}
current = next;
}
}
private static JObject ToJObject(object result)
{
return result as JObject ?? JObject.FromObject(result);
}
private static IEnumerator WaitForUnityReady(double timeoutSeconds = 30.0)
{
// Some EditMode tests trigger script compilation/domain reload. Tools like ManageScriptableObject
// intentionally return "compiling_or_reloading" during these windows. Wait until Unity is stable
// to make tests deterministic.
double start = EditorApplication.timeSinceStartup;
while (EditorApplication.isCompiling || EditorApplication.isUpdating)
{
if (EditorApplication.timeSinceStartup - start > timeoutSeconds)
{
Assert.Fail($"Timed out waiting for Unity to finish compiling/updating (>{timeoutSeconds:0.0}s).");
}
yield return null; // yield to the editor loop so importing/compiling can actually progress
}
}
}
}

View File

@ -5,6 +5,7 @@ using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Tools;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
@ -57,16 +58,8 @@ namespace MCPForUnityTests.Editor.Tools
AssetDatabase.DeleteAsset(TempRoot);
}
// Clean up parent Temp folder if it's empty
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");
}
}
// Clean up empty parent folders to avoid debris
CleanupEmptyParentFolders(TempRoot);
AssetDatabase.Refresh();
}
@ -99,11 +92,6 @@ namespace MCPForUnityTests.Editor.Tools
return tex;
}
private static JObject ToJObject(object result)
{
return result as JObject ?? JObject.FromObject(result);
}
[Test]
public void CreateAndModifyMaterial_WithDirectPropertyKeys_Works()
{

View File

@ -5,6 +5,7 @@ using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Tools;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
@ -62,25 +63,12 @@ namespace MCPForUnityTests.Editor.Tools
AssetDatabase.DeleteAsset(TempRoot);
}
// Clean up parent Temp folder if it's empty
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");
}
}
// Clean up empty parent folders to avoid debris
CleanupEmptyParentFolders(TempRoot);
AssetDatabase.Refresh();
}
private static JObject ToJObject(object result)
{
return result as JObject ?? JObject.FromObject(result);
}
[Test]
public void CreateMaterial_WithObjectProperties_SucceedsAndSetsColor()
{

View File

@ -1,11 +1,9 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using MCPForUnity.Editor.Tools;
using MCPForUnity.Editor.Helpers;
using static MCPForUnityTests.Editor.TestUtilities;
namespace MCPForUnityTests.Editor.Tools
{
@ -77,16 +75,5 @@ namespace MCPForUnityTests.Editor.Tools
}
Assert.IsTrue(found, $"The unique log message '{uniqueMessage}' was not found in retrieved logs.");
}
private static JObject ToJObject(object result)
{
if (result == null)
{
Assert.Fail("ReadConsole.HandleCommand returned null.");
return new JObject(); // Unreachable, but satisfies return type.
}
return result as JObject ?? JObject.FromObject(result);
}
}
}