2025-12-29 12:15:50 +08:00
using System ;
2026-01-02 12:36:45 +08:00
using System.Collections ;
2025-12-29 12:15:50 +08:00
using Newtonsoft.Json.Linq ;
using NUnit.Framework ;
using UnityEditor ;
using UnityEngine ;
2026-01-02 12:36:45 +08:00
using UnityEngine.TestTools ;
2025-12-29 12:15:50 +08:00
using MCPForUnity.Editor.Tools ;
using MCPForUnityTests.Editor.Tools.Fixtures ;
Harden `manage_scriptable_object` Tool (#522)
* feat(manage_scriptable_object): harden tool with path normalization, auto-resize, bulk mapping
Phase 1: Path Syntax & Auto-Resizing
- Add NormalizePropertyPath() to convert field[index] to Array.data format
- Add EnsureArrayCapacity() to auto-grow arrays when targeting out-of-bounds indices
Phase 2: Consolidation
- Replace duplicate TryGet* helpers with ParamCoercion/VectorParsing shared utilities
- Add Vector4 parsing support to VectorParsing.cs
Phase 3: Bulk Data Mapping
- Handle JArray values for list/array properties (recursive element setting)
- Handle JObject values for nested struct/class properties
Phase 4: Enhanced Reference Resolution
- Support plain 32-char GUID strings for ObjectReference fields
Phase 5: Validation & Dry-Run
- Add ValidatePatches() for pre-validation of all patches
- Add dry_run parameter to validate without mutating
Includes comprehensive stress test suite covering:
- Big Bang (large nested arrays), Out of Bounds, Friendly Path Syntax
- Deep Nesting, Mixed References, Rapid Fire, Type Mismatch
- Bulk Array Mapping, GUID Shorthand, Dry Run validation
* feat: Add AnimationCurve and Quaternion support to manage_scriptable_object tool
- Implement TrySetAnimationCurve() supporting both {'keys': [...]} and direct [...] formats
* Support keyframe properties: time, value, inSlope, outSlope, weightedMode, inWeight, outWeight
* Gracefully default missing optional fields to 0
* Clear error messages for malformed structures
- Implement TrySetQuaternion() with 4 input formats:
* Euler array [x, y, z] - 3 elements interpreted as degrees
* Raw array [x, y, z, w] - 4 components
* Object format {x, y, z, w} - explicit components
* Explicit euler {euler: [x, y, z]} - labeled format
- Improve error handling:
* Null values: AnimationCurve→empty, Quaternion→identity
* Invalid inputs rejected with specific, actionable error messages
* Validate keyframe objects and array sizes
- Add comprehensive test coverage in ManageScriptableObjectStressTests.cs:
* AnimationCurve with keyframe array format
* AnimationCurve with direct array (no wrapper)
* Quaternion via Euler angles
* Quaternion via raw components
* Quaternion via object format
* Quaternion via explicit euler property
- Fix test file compilation issues:
* Replace undefined TestFolder with _runRoot
* Add System.IO using statement
* refactor: consolidate test utilities to eliminate duplication
- Add TestUtilities.cs with shared helpers:
- ToJObject() - consolidates 11 duplicates across test files
- EnsureFolder() - consolidates 2 duplicates
- WaitForUnityReady() - consolidates 2 duplicates
- FindFallbackShader() - consolidates shader chain duplicates
- SafeDeleteAsset() - helper for asset cleanup
- CleanupEmptyParentFolders() - standardizes TearDown cleanup
- Update 11 test files to use shared TestUtilities via 'using static'
- Standardize TearDown cleanup patterns across all test files
- Net reduction of ~40 lines while improving maintainability
* fix: add missing animCurve and rotation fields to ComplexStressSO
Add AnimationCurve and Quaternion fields required by Phase 6 stress tests.
2026-01-07 22:46:35 +08:00
using static MCPForUnityTests . Editor . TestUtilities ;
2025-12-29 12:15:50 +08:00
namespace MCPForUnityTests.Editor.Tools
{
public class ManageScriptableObjectTests
{
private const string TempRoot = "Assets/Temp/ManageScriptableObjectTests" ;
2026-01-02 12:36:45 +08:00
private const double UnityReadyTimeoutSeconds = 180.0 ;
2025-12-29 12:15:50 +08:00
2026-01-04 04:42:32 +08:00
private string _runRoot ;
private string _nestedFolder ;
2025-12-29 12:15:50 +08:00
private string _createdAssetPath ;
private string _createdGuid ;
private string _matAPath ;
private string _matBPath ;
2026-01-02 12:36:45 +08:00
[UnitySetUp]
public IEnumerator SetUp ( )
2025-12-29 12:15:50 +08:00
{
2026-01-02 12:36:45 +08:00
yield return WaitForUnityReady ( UnityReadyTimeoutSeconds ) ;
2025-12-29 12:15:50 +08:00
EnsureFolder ( "Assets/Temp" ) ;
2026-01-04 04:42:32 +08:00
// Avoid deleting/recreating the entire TempRoot each test (can trigger heavy reimport churn).
// Instead, isolate each test in its own unique subfolder under TempRoot.
2025-12-29 12:15:50 +08:00
EnsureFolder ( TempRoot ) ;
2026-01-04 04:42:32 +08:00
_runRoot = $"{TempRoot}/Run_{Guid.NewGuid():N}" ;
EnsureFolder ( _runRoot ) ;
_nestedFolder = _runRoot + "/Nested/Deeper" ;
2025-12-29 12:15:50 +08:00
_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" ;
Harden `manage_scriptable_object` Tool (#522)
* feat(manage_scriptable_object): harden tool with path normalization, auto-resize, bulk mapping
Phase 1: Path Syntax & Auto-Resizing
- Add NormalizePropertyPath() to convert field[index] to Array.data format
- Add EnsureArrayCapacity() to auto-grow arrays when targeting out-of-bounds indices
Phase 2: Consolidation
- Replace duplicate TryGet* helpers with ParamCoercion/VectorParsing shared utilities
- Add Vector4 parsing support to VectorParsing.cs
Phase 3: Bulk Data Mapping
- Handle JArray values for list/array properties (recursive element setting)
- Handle JObject values for nested struct/class properties
Phase 4: Enhanced Reference Resolution
- Support plain 32-char GUID strings for ObjectReference fields
Phase 5: Validation & Dry-Run
- Add ValidatePatches() for pre-validation of all patches
- Add dry_run parameter to validate without mutating
Includes comprehensive stress test suite covering:
- Big Bang (large nested arrays), Out of Bounds, Friendly Path Syntax
- Deep Nesting, Mixed References, Rapid Fire, Type Mismatch
- Bulk Array Mapping, GUID Shorthand, Dry Run validation
* feat: Add AnimationCurve and Quaternion support to manage_scriptable_object tool
- Implement TrySetAnimationCurve() supporting both {'keys': [...]} and direct [...] formats
* Support keyframe properties: time, value, inSlope, outSlope, weightedMode, inWeight, outWeight
* Gracefully default missing optional fields to 0
* Clear error messages for malformed structures
- Implement TrySetQuaternion() with 4 input formats:
* Euler array [x, y, z] - 3 elements interpreted as degrees
* Raw array [x, y, z, w] - 4 components
* Object format {x, y, z, w} - explicit components
* Explicit euler {euler: [x, y, z]} - labeled format
- Improve error handling:
* Null values: AnimationCurve→empty, Quaternion→identity
* Invalid inputs rejected with specific, actionable error messages
* Validate keyframe objects and array sizes
- Add comprehensive test coverage in ManageScriptableObjectStressTests.cs:
* AnimationCurve with keyframe array format
* AnimationCurve with direct array (no wrapper)
* Quaternion via Euler angles
* Quaternion via raw components
* Quaternion via object format
* Quaternion via explicit euler property
- Fix test file compilation issues:
* Replace undefined TestFolder with _runRoot
* Add System.IO using statement
* refactor: consolidate test utilities to eliminate duplication
- Add TestUtilities.cs with shared helpers:
- ToJObject() - consolidates 11 duplicates across test files
- EnsureFolder() - consolidates 2 duplicates
- WaitForUnityReady() - consolidates 2 duplicates
- FindFallbackShader() - consolidates shader chain duplicates
- SafeDeleteAsset() - helper for asset cleanup
- CleanupEmptyParentFolders() - standardizes TearDown cleanup
- Update 11 test files to use shared TestUtilities via 'using static'
- Standardize TearDown cleanup patterns across all test files
- Net reduction of ~40 lines while improving maintainability
* fix: add missing animCurve and rotation fields to ComplexStressSO
Add AnimationCurve and Quaternion fields required by Phase 6 stress tests.
2026-01-07 22:46:35 +08:00
var shader = FindFallbackShader ( ) ;
2025-12-29 12:15:50 +08:00
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 ( ) ;
2026-01-02 12:36:45 +08:00
yield return WaitForUnityReady ( UnityReadyTimeoutSeconds ) ;
2025-12-29 12:15:50 +08:00
}
[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 ) ;
}
2026-01-04 04:42:32 +08:00
if ( ! string . IsNullOrEmpty ( _runRoot ) & & AssetDatabase . IsValidFolder ( _runRoot ) )
2025-12-29 12:15:50 +08:00
{
2026-01-04 04:42:32 +08:00
AssetDatabase . DeleteAsset ( _runRoot ) ;
2025-12-29 12:15:50 +08:00
}
Harden `manage_scriptable_object` Tool (#522)
* feat(manage_scriptable_object): harden tool with path normalization, auto-resize, bulk mapping
Phase 1: Path Syntax & Auto-Resizing
- Add NormalizePropertyPath() to convert field[index] to Array.data format
- Add EnsureArrayCapacity() to auto-grow arrays when targeting out-of-bounds indices
Phase 2: Consolidation
- Replace duplicate TryGet* helpers with ParamCoercion/VectorParsing shared utilities
- Add Vector4 parsing support to VectorParsing.cs
Phase 3: Bulk Data Mapping
- Handle JArray values for list/array properties (recursive element setting)
- Handle JObject values for nested struct/class properties
Phase 4: Enhanced Reference Resolution
- Support plain 32-char GUID strings for ObjectReference fields
Phase 5: Validation & Dry-Run
- Add ValidatePatches() for pre-validation of all patches
- Add dry_run parameter to validate without mutating
Includes comprehensive stress test suite covering:
- Big Bang (large nested arrays), Out of Bounds, Friendly Path Syntax
- Deep Nesting, Mixed References, Rapid Fire, Type Mismatch
- Bulk Array Mapping, GUID Shorthand, Dry Run validation
* feat: Add AnimationCurve and Quaternion support to manage_scriptable_object tool
- Implement TrySetAnimationCurve() supporting both {'keys': [...]} and direct [...] formats
* Support keyframe properties: time, value, inSlope, outSlope, weightedMode, inWeight, outWeight
* Gracefully default missing optional fields to 0
* Clear error messages for malformed structures
- Implement TrySetQuaternion() with 4 input formats:
* Euler array [x, y, z] - 3 elements interpreted as degrees
* Raw array [x, y, z, w] - 4 components
* Object format {x, y, z, w} - explicit components
* Explicit euler {euler: [x, y, z]} - labeled format
- Improve error handling:
* Null values: AnimationCurve→empty, Quaternion→identity
* Invalid inputs rejected with specific, actionable error messages
* Validate keyframe objects and array sizes
- Add comprehensive test coverage in ManageScriptableObjectStressTests.cs:
* AnimationCurve with keyframe array format
* AnimationCurve with direct array (no wrapper)
* Quaternion via Euler angles
* Quaternion via raw components
* Quaternion via object format
* Quaternion via explicit euler property
- Fix test file compilation issues:
* Replace undefined TestFolder with _runRoot
* Add System.IO using statement
* refactor: consolidate test utilities to eliminate duplication
- Add TestUtilities.cs with shared helpers:
- ToJObject() - consolidates 11 duplicates across test files
- EnsureFolder() - consolidates 2 duplicates
- WaitForUnityReady() - consolidates 2 duplicates
- FindFallbackShader() - consolidates shader chain duplicates
- SafeDeleteAsset() - helper for asset cleanup
- CleanupEmptyParentFolders() - standardizes TearDown cleanup
- Update 11 test files to use shared TestUtilities via 'using static'
- Standardize TearDown cleanup patterns across all test files
- Net reduction of ~40 lines while improving maintainability
* fix: add missing animCurve and rotation fields to ComplexStressSO
Add AnimationCurve and Quaternion fields required by Phase 6 stress tests.
2026-01-07 22:46:35 +08:00
// Clean up empty parent folders to avoid debris
CleanupEmptyParentFolders ( TempRoot ) ;
2025-12-29 12:15:50 +08:00
AssetDatabase . Refresh ( ) ;
}
[Test]
2026-01-04 04:42:32 +08:00
public void Create_CreatesNestedFolders_PlacesAssetCorrectly ( )
2025-12-29 12:15:50 +08:00
{
var create = new JObject
{
["action"] = "create" ,
["typeName"] = typeof ( ManageScriptableObjectTestDefinition ) . FullName ,
2026-01-04 04:42:32 +08:00
["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" ,
2025-12-29 12:15:50 +08:00
["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 ( ) ) ;
2026-01-04 04:42:32 +08:00
2025-12-29 12:15:50 +08:00
var data = result [ "data" ] as JObject ;
Assert . IsNotNull ( data , "Expected data payload" ) ;
_createdGuid = data ! [ "guid" ] ? . ToString ( ) ;
_createdAssetPath = data [ "path" ] ? . ToString ( ) ;
2026-01-04 04:42:32 +08:00
Assert . IsTrue ( _createdAssetPath ! . StartsWith ( _runRoot , StringComparison . Ordinal ) , $"Asset should be created under {_runRoot}: {_createdAssetPath}" ) ;
2025-12-29 12:15:50 +08:00
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 ,
2026-01-04 04:42:32 +08:00
["folderPath"] = _runRoot ,
2025-12-29 12:15:50 +08:00
["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}" ) ;
}
}
}