@atelierai/uco 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/package.json +1 -1
  3. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetConfig.cs +132 -132
  4. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetDependencyResolver.cs +237 -237
  5. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetDownloader.cs +82 -82
  6. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Resource/MenuItems.cs +247 -247
  7. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Resource/ToolGroups.cs +153 -153
  8. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/SystemTool/Skills.Create.SkillBody.cs +155 -155
  9. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Console.GetLogs.cs +95 -95
  10. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Docs.HttpFetcher.cs +139 -139
  11. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Instance.ListAll.cs +72 -72
  12. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/PathReadHelper.cs +106 -106
  13. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Script.Validate.cs +252 -252
  14. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Tests/TestResultCollector.cs +537 -537
  15. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Tests.cs +400 -400
  16. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Branding/ProductInfo.cs +62 -62
  17. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/CopilotServerManager.cs +1034 -1034
  18. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Skills/Skill_InitialSetup.cs +197 -197
  19. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.Editor.cs +201 -201
  20. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.cs +67 -67
  21. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/CopilotListWindowBase.cs +393 -393
  22. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/CopilotWindowBase.cs +113 -113
  23. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.Connection.cs +3 -3
  24. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotFeatures.cs +195 -195
  25. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotServer.cs +20 -20
  26. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CreateGUI.cs +354 -354
  27. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.cs +78 -78
  28. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowInitializer.cs +47 -47
  29. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/SerializationCheckWindow.cs +190 -190
  30. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Build.cs +91 -91
  31. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Config.cs +298 -298
  32. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Static.cs +296 -296
  33. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.cs +69 -69
  34. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/PackageUtils.cs +168 -168
  35. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/PrefsKeyMigration.cs +119 -119
  36. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/SafeDefaultsGuard.cs +180 -180
  37. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ScriptUtils.cs +229 -229
  38. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ToolsManifestGenerator.cs +200 -200
  39. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityAuthoringAdapters.cs +1467 -1467
  40. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityAuthoringSafetyReloadGuard.cs +65 -65
  41. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityCopilotUpdateProjectSettings.cs +66 -66
  42. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/ReflectorNet.dll +0 -0
  43. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.Common.dll +0 -0
  44. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.dll +0 -0
  45. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Converter/Json/Structures/EntityIdConverter.cs +120 -120
  46. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Data/GameObjectMetadata.cs +110 -110
  47. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Data/GameObjectMetadata.pre-Unity.6.5.cs +110 -110
  48. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Unity/Logs/FileLogStorage.cs +547 -547
  49. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.Build.cs +243 -243
  50. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.Config.cs +3 -3
  51. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.cs +295 -295
  52. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginBuilder.cs +105 -105
  53. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginRuntime.Static.cs +155 -155
  54. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginRuntime.cs +59 -59
  55. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Utils/EnvironmentUtils.cs +11 -11
  56. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AtomicApi/AtomicApiTestFixtures.cs +99 -99
  57. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AtomicApi/TryModifyAtTests.cs +227 -227
  58. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AuthoringSafetyPilotTests.cs +1050 -1050
  59. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetLegacyMigrationTests.cs +304 -304
  60. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Environment/EnvironmentUtilsTests.cs +1 -1
  61. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/BlacklistTypeTests.cs +416 -416
  62. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.cs +172 -172
  63. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.pre-Unity.6.5.cs +169 -169
  64. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPopulationTests.cs +182 -182
  65. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPopulationTests.pre-Unity.6.5.cs +179 -179
  66. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.cs +175 -175
  67. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.pre-Unity.6.5.cs +174 -174
  68. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.cs +171 -171
  69. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.pre-Unity.6.5.cs +170 -170
  70. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsCreateFolderTests.cs +243 -243
  71. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsMaterialTests.cs +101 -101
  72. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsPrefabCreateTests.cs +348 -348
  73. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsPrefabCreateTests.pre-Unity.6.5.cs +348 -348
  74. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsShaderTests.cs +194 -194
  75. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/PathBasedToolTests.cs +573 -573
  76. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/PathBasedToolTests.pre-Unity.6.5.cs +513 -513
  77. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotCameraTests.cs +154 -154
  78. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotCameraTests.pre-Unity.6.5.cs +154 -154
  79. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotGameViewTests.cs +164 -164
  80. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotSceneViewTests.cs +155 -155
  81. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolParameterTests.cs +56 -56
  82. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolThreadSafetyTests.cs +651 -651
  83. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolThreadSafetyTests.pre-Unity.6.5.cs +651 -651
  84. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ToolBatchExecutionIntegrationTests.cs +407 -407
  85. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/{McpPluginTests.cs → UcoPluginTests.cs} +105 -105
  86. package/vendor/plugin/com.atelierai.unity.copilot/package.json +1 -1
  87. /package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/{McpPluginTests.cs.meta → UcoPluginTests.cs.meta} +0 -0
@@ -1,651 +1,651 @@
1
- /*
2
- ┌──────────────────────────────────────────────────────────────────┐
3
- │ Author: Ivan Murzak (https://github.com/IvanMurzak) │
4
- │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │
5
- │ Copyright (c) 2025 Ivan Murzak │
6
- │ Licensed under the Apache License, Version 2.0. │
7
- │ See the LICENSE file in the project root for more information. │
8
- └──────────────────────────────────────────────────────────────────┘
9
- */
10
-
11
- #nullable enable
12
- #if !UNITY_6000_5_OR_NEWER
13
- using System.Collections;
14
- using System.IO;
15
- using com.AtelierAI.Unity.Copilot.Editor.API;
16
- using NUnit.Framework;
17
- using UnityEditor;
18
- using UnityEngine;
19
- using UnityEngine.TestTools;
20
-
21
- namespace com.AtelierAI.Unity.Copilot.Editor.Tests
22
- {
23
- /// <summary>
24
- /// Thread-safety coverage for MCP tools — regression for
25
- /// <see href="https://github.com/IvanMurzak/uco-plugin/issues/633">#633</see>.
26
- ///
27
- /// For each tool we want to guarantee the tool's body handles invocation from any
28
- /// thread (the MCP server dispatches tool calls from a SignalR background thread,
29
- /// so tools that touch Unity APIs must marshal back to the main thread via
30
- /// <c>MainThread.Instance.Run</c>). Forgetting to do so caused the regression in
31
- /// <see href="https://github.com/IvanMurzak/uco-plugin/issues/632">#632</see> /
32
- /// <see href="https://github.com/IvanMurzak/uco-plugin/pull/637">#637</see>
33
- /// (<c>console-clear-logs</c>).
34
- ///
35
- /// <b>Excluded tools</b>: <c>package-add</c>/<c>package-remove</c> (trigger domain
36
- /// reload), <c>tests-run</c> (recursive hang), system tools (<c>ping</c>,
37
- /// <c>unity-skill-*</c>). See inline comments for rationale.
38
- ///
39
- /// Two helpers from <see cref="BaseTest"/> are used:
40
- /// <list type="bullet">
41
- /// <item><see cref="BaseTest.RunToolBothThreads"/> — happy path; asserts the tool
42
- /// succeeds on both threads. Used when crafting a valid input is practical.</item>
43
- /// <item><see cref="BaseTest.RunToolExpectNoThreadViolation"/> — lenient mode; tolerates
44
- /// business-level errors and only fails the test if Unity surfaces a
45
- /// <c>"can only be called from the main thread"</c> exception. Used for tools
46
- /// that need elaborate setup (<c>SerializedMember</c>, prefab stages, etc.) or
47
- /// whose happy path would mutate the project in risky ways
48
- /// (<c>package-add</c>/<c>package-remove</c>, <c>tests-run</c>, etc.).</item>
49
- /// </list>
50
- /// </summary>
51
- public class ToolThreadSafetyTests : BaseTest
52
- {
53
- const string TmpFolderName = "BgThreadTests_TMP";
54
- const string TmpFolder = "Assets/" + TmpFolderName;
55
-
56
- [TearDown]
57
- public void BgTearDown()
58
- {
59
- if (AssetDatabase.IsValidFolder(TmpFolder))
60
- AssetDatabase.DeleteAsset(TmpFolder);
61
- }
62
-
63
- // ================================================================
64
- // NO-ARG / READ-ONLY tools (happy path, empty JSON)
65
- // ================================================================
66
-
67
- [UnityTest] public IEnumerator ToolList_BothThreads()
68
- => RunToolBothThreads(Tool_Tool.ToolListId, "{}");
69
-
70
- [UnityTest] public IEnumerator GameObjectComponentListAll_BothThreads()
71
- => RunToolBothThreads(Tool_GameObject.ComponentListToolId, @"{""pageSize"":3}");
72
-
73
- [UnityTest] public IEnumerator SceneListOpened_BothThreads()
74
- => RunToolBothThreads(Tool_Scene.SceneListOpenedToolId, "{}");
75
-
76
- [UnityTest] public IEnumerator SceneGetData_BothThreads()
77
- => RunToolBothThreads(Tool_Scene.SceneGetDataToolId, "{}");
78
-
79
- [UnityTest] public IEnumerator EditorApplicationGetState_BothThreads()
80
- => RunToolBothThreads(Tool_Editor.EditorApplicationGetStateToolId, "{}");
81
-
82
- [UnityTest] public IEnumerator EditorSelectionGet_BothThreads()
83
- => RunToolBothThreads(Tool_Editor_Selection.EditorSelectionGetToolId, "{}");
84
-
85
- [UnityTest] public IEnumerator ConsoleGetLogs_BothThreads()
86
- => RunToolBothThreads(Tool_Console.ConsoleGetLogsToolId, "{}");
87
-
88
- [UnityTest]
89
- public IEnumerator ConsoleClearLogs_BothThreads()
90
- {
91
- // Regression for #632/#637: ClearLogs calls Debug.ClearDeveloperConsole() which
92
- // MUST be invoked on the main thread.
93
- yield return RunToolBothThreads(Tool_Console.ConsoleClearLogsToolId, "{}");
94
- }
95
-
96
- [UnityTest] public IEnumerator AssetsFind_BothThreads()
97
- => RunToolBothThreads(Tool_Assets.AssetsFindToolId, @"{""filter"":""t:Scene"",""maxResults"":3}");
98
-
99
- [UnityTest] public IEnumerator AssetsFindBuiltIn_BothThreads()
100
- => RunToolBothThreads(Tool_Assets.AssetsFindBuiltInToolId, @"{""maxResults"":3}");
101
-
102
- [UnityTest] public IEnumerator AssetsShaderListAll_BothThreads()
103
- => RunToolBothThreads(Tool_Assets_Shader.AssetsShaderListAllToolId, "{}");
104
-
105
- [UnityTest] public IEnumerator AssetsRefresh_BothThreads()
106
- => RunToolBothThreads(Tool_Assets.AssetsRefreshToolId, "{}");
107
-
108
- [UnityTest] public IEnumerator PackageList_BothThreads()
109
- => RunToolBothThreads(Tool_Package.PackageListToolId, "{}");
110
-
111
- [UnityTest] public IEnumerator TypeGetJsonSchema_BothThreads()
112
- => RunToolBothThreads(Tool_Type.TypeGetJsonSchemaToolId, @"{""typeName"":""UnityEngine.Vector3""}");
113
-
114
- // ================================================================
115
- // GAMEOBJECT tools — happy path, per-call setup
116
- // ================================================================
117
-
118
- [UnityTest]
119
- public IEnumerator GameObjectCreate_BothThreads()
120
- {
121
- yield return RunToolMainThreadCoop(Tool_GameObject.GameObjectCreateToolId, @"{""name"":""bg_main""}");
122
- Assert.IsNotNull(GameObject.Find("bg_main"));
123
- yield return RunToolFromBackgroundThread(Tool_GameObject.GameObjectCreateToolId, @"{""name"":""bg_bg""}");
124
- Assert.IsNotNull(GameObject.Find("bg_bg"));
125
- }
126
-
127
- [UnityTest]
128
- public IEnumerator GameObjectFind_BothThreads()
129
- {
130
- var go = new GameObject("bg_find");
131
- var id = go.GetInstanceID();
132
- yield return RunToolBothThreads(Tool_GameObject.GameObjectFindToolId,
133
- $@"{{""gameObjectRef"":{{""instanceID"":{id}}}}}");
134
- }
135
-
136
- [UnityTest]
137
- public IEnumerator GameObjectDestroy_BothThreads()
138
- {
139
- var a = new GameObject("bg_destroy_a").GetInstanceID();
140
- yield return RunToolMainThreadCoop(Tool_GameObject.GameObjectDestroyToolId,
141
- $@"{{""gameObjectRef"":{{""instanceID"":{a}}}}}");
142
-
143
- var b = new GameObject("bg_destroy_b").GetInstanceID();
144
- yield return RunToolFromBackgroundThread(Tool_GameObject.GameObjectDestroyToolId,
145
- $@"{{""gameObjectRef"":{{""instanceID"":{b}}}}}");
146
- }
147
-
148
- [UnityTest]
149
- public IEnumerator GameObjectDuplicate_BothThreads()
150
- {
151
- var a = new GameObject("bg_dup_a").GetInstanceID();
152
- yield return RunToolMainThreadCoop(Tool_GameObject.GameObjectDuplicateToolId,
153
- $@"{{""gameObjectRefs"":[{{""instanceID"":{a}}}]}}");
154
-
155
- var b = new GameObject("bg_dup_b").GetInstanceID();
156
- yield return RunToolFromBackgroundThread(Tool_GameObject.GameObjectDuplicateToolId,
157
- $@"{{""gameObjectRefs"":[{{""instanceID"":{b}}}]}}");
158
- }
159
-
160
- [UnityTest]
161
- public IEnumerator GameObjectSetParent_BothThreads()
162
- {
163
- var parent = new GameObject("bg_parent").GetInstanceID();
164
- var child1 = new GameObject("bg_child_a").GetInstanceID();
165
- yield return RunToolMainThreadCoop(Tool_GameObject.GameObjectSetParentToolId,
166
- $@"{{""gameObjectRefs"":[{{""instanceID"":{child1}}}],""parentGameObjectRef"":{{""instanceID"":{parent}}}}}");
167
-
168
- var child2 = new GameObject("bg_child_b").GetInstanceID();
169
- yield return RunToolFromBackgroundThread(Tool_GameObject.GameObjectSetParentToolId,
170
- $@"{{""gameObjectRefs"":[{{""instanceID"":{child2}}}],""parentGameObjectRef"":{{""instanceID"":{parent}}}}}");
171
- }
172
-
173
- [UnityTest]
174
- public IEnumerator GameObjectComponentAdd_BothThreads()
175
- {
176
- var a = new GameObject("bg_addc_a").GetInstanceID();
177
- yield return RunToolMainThreadCoop(Tool_GameObject.GameObjectComponentAddToolId,
178
- $@"{{""gameObjectRef"":{{""instanceID"":{a}}},""componentNames"":[""UnityEngine.BoxCollider""]}}");
179
-
180
- var b = new GameObject("bg_addc_b").GetInstanceID();
181
- yield return RunToolFromBackgroundThread(Tool_GameObject.GameObjectComponentAddToolId,
182
- $@"{{""gameObjectRef"":{{""instanceID"":{b}}},""componentNames"":[""UnityEngine.SphereCollider""]}}");
183
- }
184
-
185
- [UnityTest]
186
- public IEnumerator GameObjectComponentGet_BothThreads()
187
- {
188
- var go = new GameObject("bg_getc");
189
- var id = go.GetInstanceID();
190
- // Transform lives at component index 0 on every GameObject.
191
- yield return RunToolBothThreads(Tool_GameObject.GameObjectComponentGetToolId,
192
- $@"{{""gameObjectRef"":{{""instanceID"":{id}}},""componentRef"":{{""index"":0}}}}");
193
- }
194
-
195
- [UnityTest]
196
- public IEnumerator GameObjectComponentDestroy_BothThreads()
197
- {
198
- var a = new GameObject("bg_destc_a");
199
- a.AddComponent<BoxCollider>();
200
- var idA = a.GetInstanceID();
201
- yield return RunToolMainThreadCoop(Tool_GameObject.GameObjectComponentDestroyToolId,
202
- $@"{{""gameObjectRef"":{{""instanceID"":{idA}}},""destroyComponentRefs"":[{{""typeName"":""UnityEngine.BoxCollider""}}]}}");
203
-
204
- var b = new GameObject("bg_destc_b");
205
- b.AddComponent<SphereCollider>();
206
- var idB = b.GetInstanceID();
207
- yield return RunToolFromBackgroundThread(Tool_GameObject.GameObjectComponentDestroyToolId,
208
- $@"{{""gameObjectRef"":{{""instanceID"":{idB}}},""destroyComponentRefs"":[{{""typeName"":""UnityEngine.SphereCollider""}}]}}");
209
- }
210
-
211
- [UnityTest]
212
- public IEnumerator GameObjectModify_ThreadSafetyOnly()
213
- {
214
- // gameobject-modify requires a SerializedMember tree — we cover thread safety
215
- // only (business errors from an empty diff are tolerated).
216
- var id = new GameObject("bg_modify").GetInstanceID();
217
- yield return RunToolExpectNoThreadViolation(Tool_GameObject.GameObjectModifyToolId,
218
- $@"{{""gameObjectRefs"":[{{""instanceID"":{id}}}],""gameObjectDiffs"":[{{""name"":""root""}}]}}");
219
- }
220
-
221
- [UnityTest]
222
- public IEnumerator GameObjectComponentModify_ThreadSafetyOnly()
223
- {
224
- var go = new GameObject("bg_cmodify");
225
- go.AddComponent<BoxCollider>();
226
- var id = go.GetInstanceID();
227
- yield return RunToolExpectNoThreadViolation(Tool_GameObject.GameObjectComponentModifyToolId,
228
- $@"{{""gameObjectRef"":{{""instanceID"":{id}}},""componentRef"":{{""typeName"":""UnityEngine.BoxCollider""}},""componentDiff"":{{""name"":""root""}}}}");
229
- }
230
-
231
- // ================================================================
232
- // OBJECT tools
233
- // ================================================================
234
-
235
- [UnityTest]
236
- public IEnumerator ObjectGetData_BothThreads()
237
- {
238
- var id = new GameObject("bg_obj").GetInstanceID();
239
- yield return RunToolBothThreads(Tool_Object.ObjectGetDataToolId,
240
- $@"{{""objectRef"":{{""instanceID"":{id}}}}}");
241
- }
242
-
243
- [UnityTest]
244
- public IEnumerator ObjectModify_ThreadSafetyOnly()
245
- {
246
- var id = new GameObject("bg_objmod").GetInstanceID();
247
- yield return RunToolExpectNoThreadViolation(Tool_Object.ObjectModifyToolId,
248
- $@"{{""objectRef"":{{""instanceID"":{id}}},""objectDiff"":{{""name"":""root""}}}}");
249
- }
250
-
251
- // ================================================================
252
- // EDITOR tools
253
- // ================================================================
254
-
255
- [UnityTest]
256
- public IEnumerator EditorSelectionSet_BothThreads()
257
- {
258
- var id = new GameObject("bg_select").GetInstanceID();
259
- yield return RunToolBothThreads(Tool_Editor_Selection.EditorSelectionSetToolId,
260
- $@"{{""select"":[{{""instanceID"":{id}}}]}}");
261
- }
262
-
263
- [UnityTest]
264
- public IEnumerator EditorApplicationSetState_BothThreads()
265
- {
266
- // No-op toggle: current state is not-playing, not-paused.
267
- yield return RunToolBothThreads(Tool_Editor.EditorApplicationSetStateToolId,
268
- @"{""isPlaying"":false,""isPaused"":false}");
269
- }
270
-
271
- // ================================================================
272
- // ASSETS tools
273
- // ================================================================
274
-
275
- [UnityTest]
276
- public IEnumerator AssetsCreateFolder_BothThreads()
277
- {
278
- EnsureTmpFolder();
279
- yield return RunToolMainThreadCoop(Tool_Assets.AssetsCreateFolderToolId,
280
- $@"{{""inputs"":[{{""parentFolderPath"":""{TmpFolder}"",""newFolderName"":""a""}}]}}");
281
- yield return RunToolFromBackgroundThread(Tool_Assets.AssetsCreateFolderToolId,
282
- $@"{{""inputs"":[{{""parentFolderPath"":""{TmpFolder}"",""newFolderName"":""b""}}]}}");
283
- }
284
-
285
- [UnityTest]
286
- public IEnumerator AssetsGetData_BothThreads()
287
- {
288
- var assetPath = CreateTmpTextAsset("get_data.txt", "hello");
289
- yield return RunToolBothThreads(Tool_Assets.AssetsGetDataToolId,
290
- $@"{{""assetRef"":{{""assetPath"":""{assetPath}""}}}}");
291
- }
292
-
293
- [UnityTest]
294
- public IEnumerator AssetsModify_ThreadSafetyOnly()
295
- {
296
- var assetPath = CreateTmpTextAsset("modify.txt", "hello");
297
- yield return RunToolExpectNoThreadViolation(Tool_Assets.AssetsModifyToolId,
298
- $@"{{""assetRef"":{{""assetPath"":""{assetPath}""}},""content"":{{""name"":""root""}}}}");
299
- }
300
-
301
- [UnityTest]
302
- public IEnumerator AssetsMaterialCreate_BothThreads()
303
- {
304
- EnsureTmpFolder();
305
- yield return RunToolMainThreadCoop(Tool_Assets.AssetsMaterialCreateToolId,
306
- $@"{{""assetPath"":""{TmpFolder}/mat_main.mat"",""shaderName"":""Standard""}}");
307
- yield return RunToolFromBackgroundThread(Tool_Assets.AssetsMaterialCreateToolId,
308
- $@"{{""assetPath"":""{TmpFolder}/mat_bg.mat"",""shaderName"":""Standard""}}");
309
- }
310
-
311
- [UnityTest]
312
- public IEnumerator AssetsShaderGetData_BothThreads()
313
- {
314
- // Find any shader asset in the project to query.
315
- var shaderPath = FindFirstShaderPath();
316
- if (shaderPath == null)
317
- {
318
- Assert.Ignore("No shader asset available in project.");
319
- yield break;
320
- }
321
- yield return RunToolBothThreads(Tool_Assets_Shader.AssetsShaderGetDataToolId,
322
- $@"{{""assetRef"":{{""assetPath"":""{shaderPath}""}}}}");
323
- }
324
-
325
- [UnityTest]
326
- public IEnumerator AssetsCopy_BothThreads()
327
- {
328
- var src = CreateTmpTextAsset("copy_src.txt", "source");
329
- yield return RunToolMainThreadCoop(Tool_Assets.AssetsCopyToolId,
330
- $@"{{""sourcePaths"":[""{src}""],""destinationPaths"":[""{TmpFolder}/copy_main.txt""]}}");
331
- yield return RunToolFromBackgroundThread(Tool_Assets.AssetsCopyToolId,
332
- $@"{{""sourcePaths"":[""{src}""],""destinationPaths"":[""{TmpFolder}/copy_bg.txt""]}}");
333
- }
334
-
335
- [UnityTest]
336
- public IEnumerator AssetsMove_BothThreads()
337
- {
338
- var src1 = CreateTmpTextAsset("move_a.txt", "a");
339
- yield return RunToolMainThreadCoop(Tool_Assets.AssetsMoveToolId,
340
- $@"{{""sourcePaths"":[""{src1}""],""destinationPaths"":[""{TmpFolder}/move_a_done.txt""]}}");
341
-
342
- var src2 = CreateTmpTextAsset("move_b.txt", "b");
343
- yield return RunToolFromBackgroundThread(Tool_Assets.AssetsMoveToolId,
344
- $@"{{""sourcePaths"":[""{src2}""],""destinationPaths"":[""{TmpFolder}/move_b_done.txt""]}}");
345
- }
346
-
347
- [UnityTest]
348
- public IEnumerator AssetsDelete_BothThreads()
349
- {
350
- var src1 = CreateTmpTextAsset("del_a.txt", "a");
351
- yield return RunToolMainThreadCoop(Tool_Assets.AssetsDeleteToolId, $@"{{""paths"":[""{src1}""]}}");
352
-
353
- var src2 = CreateTmpTextAsset("del_b.txt", "b");
354
- yield return RunToolFromBackgroundThread(Tool_Assets.AssetsDeleteToolId,
355
- $@"{{""paths"":[""{src2}""]}}");
356
- }
357
-
358
- // ---------- Prefabs ----------
359
-
360
- [UnityTest]
361
- public IEnumerator AssetsPrefabCreate_BothThreads()
362
- {
363
- EnsureTmpFolder();
364
- var idA = new GameObject("bg_prefab_a").GetInstanceID();
365
- yield return RunToolMainThreadCoop(Tool_Assets_Prefab.AssetsPrefabCreateToolId,
366
- $@"{{""prefabAssetPath"":""{TmpFolder}/prefab_a.prefab"",""gameObjectRef"":{{""instanceID"":{idA}}}}}");
367
-
368
- var idB = new GameObject("bg_prefab_b").GetInstanceID();
369
- yield return RunToolFromBackgroundThread(Tool_Assets_Prefab.AssetsPrefabCreateToolId,
370
- $@"{{""prefabAssetPath"":""{TmpFolder}/prefab_b.prefab"",""gameObjectRef"":{{""instanceID"":{idB}}}}}");
371
- }
372
-
373
- [UnityTest]
374
- public IEnumerator AssetsPrefabInstantiate_BothThreads()
375
- {
376
- EnsureTmpFolder();
377
- var seedId = new GameObject("bg_seed_prefab").GetInstanceID();
378
- var prefabPath = $"{TmpFolder}/seed.prefab";
379
- yield return RunToolMainThreadCoop(Tool_Assets_Prefab.AssetsPrefabCreateToolId,
380
- $@"{{""prefabAssetPath"":""{prefabPath}"",""gameObjectRef"":{{""instanceID"":{seedId}}}}}");
381
-
382
- yield return RunToolMainThreadCoop(Tool_Assets_Prefab.AssetsPrefabInstantiateToolId,
383
- $@"{{""prefabAssetPath"":""{prefabPath}"",""gameObjectPath"":""bg_inst_main""}}");
384
- yield return RunToolFromBackgroundThread(Tool_Assets_Prefab.AssetsPrefabInstantiateToolId,
385
- $@"{{""prefabAssetPath"":""{prefabPath}"",""gameObjectPath"":""bg_inst_bg""}}");
386
- }
387
-
388
- [UnityTest]
389
- public IEnumerator AssetsPrefabOpenSaveClose_BothThreads()
390
- {
391
- EnsureTmpFolder();
392
-
393
- // Create a prefab asset from a scene GameObject.
394
- var seedA = new GameObject("bg_prefab_osc_a").GetInstanceID();
395
- var prefabPathA = $"{TmpFolder}/osc_a.prefab";
396
- yield return RunToolMainThreadCoop(Tool_Assets_Prefab.AssetsPrefabCreateToolId,
397
- $@"{{""prefabAssetPath"":""{prefabPathA}"",""gameObjectRef"":{{""instanceID"":{seedA}}}}}");
398
-
399
- // Instantiate it so we get a prefab instance to open.
400
- yield return RunToolMainThreadCoop(Tool_Assets_Prefab.AssetsPrefabInstantiateToolId,
401
- $@"{{""prefabAssetPath"":""{prefabPathA}"",""gameObjectPath"":""bg_osc_inst_a""}}");
402
- var instanceA = GameObject.Find("bg_osc_inst_a");
403
- Assert.IsNotNull(instanceA, "Prefab instance not found after instantiate.");
404
-
405
- // Open → Save → Close (main thread).
406
- yield return RunToolMainThreadCoop(Tool_Assets_Prefab.AssetsPrefabOpenToolId,
407
- $@"{{""gameObjectRef"":{{""instanceID"":{instanceA!.GetInstanceID()}}}}}");
408
- yield return RunToolMainThreadCoop(Tool_Assets_Prefab.AssetsPrefabSaveToolId, "{}");
409
- yield return RunToolMainThreadCoop(Tool_Assets_Prefab.AssetsPrefabCloseToolId, @"{""save"":false}");
410
-
411
- // Same sequence from a background thread.
412
- var seedB = new GameObject("bg_prefab_osc_b").GetInstanceID();
413
- var prefabPathB = $"{TmpFolder}/osc_b.prefab";
414
- yield return RunToolMainThreadCoop(Tool_Assets_Prefab.AssetsPrefabCreateToolId,
415
- $@"{{""prefabAssetPath"":""{prefabPathB}"",""gameObjectRef"":{{""instanceID"":{seedB}}}}}");
416
- yield return RunToolMainThreadCoop(Tool_Assets_Prefab.AssetsPrefabInstantiateToolId,
417
- $@"{{""prefabAssetPath"":""{prefabPathB}"",""gameObjectPath"":""bg_osc_inst_b""}}");
418
- var instanceB = GameObject.Find("bg_osc_inst_b");
419
- Assert.IsNotNull(instanceB, "Prefab instance not found after instantiate.");
420
-
421
- yield return RunToolFromBackgroundThread(Tool_Assets_Prefab.AssetsPrefabOpenToolId,
422
- $@"{{""gameObjectRef"":{{""instanceID"":{instanceB!.GetInstanceID()}}}}}");
423
- yield return RunToolFromBackgroundThread(Tool_Assets_Prefab.AssetsPrefabSaveToolId, "{}");
424
- yield return RunToolFromBackgroundThread(Tool_Assets_Prefab.AssetsPrefabCloseToolId, @"{""save"":false}");
425
- }
426
-
427
- // ================================================================
428
- // SCENE tools
429
- // ================================================================
430
-
431
- [UnityTest]
432
- public IEnumerator SceneCreate_BothThreads()
433
- {
434
- EnsureTmpFolder();
435
- var startingActive = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
436
- yield return RunToolMainThreadCoop(Tool_Scene.SceneCreateToolId,
437
- $@"{{""path"":""{TmpFolder}/scene_main.unity""}}");
438
- yield return RunToolFromBackgroundThread(Tool_Scene.SceneCreateToolId,
439
- $@"{{""path"":""{TmpFolder}/scene_bg.unity""}}");
440
-
441
- // Restore a baseline active scene if the test clobbered it.
442
- if (startingActive.IsValid() && UnityEngine.SceneManagement.SceneManager.GetActiveScene() != startingActive)
443
- UnityEngine.SceneManagement.SceneManager.SetActiveScene(startingActive);
444
- }
445
-
446
- [UnityTest]
447
- public IEnumerator SceneOpenSaveSetActive_BothThreads()
448
- {
449
- EnsureTmpFolder();
450
- var scenePath = $"{TmpFolder}/reopen.unity";
451
- yield return RunToolMainThreadCoop(Tool_Scene.SceneCreateToolId, $@"{{""path"":""{scenePath}""}}");
452
-
453
- // open (main + bg)
454
- yield return RunToolMainThreadCoop(Tool_Scene.SceneOpenToolId,
455
- $@"{{""sceneRef"":{{""assetPath"":""{scenePath}""}}}}");
456
- yield return RunToolFromBackgroundThread(Tool_Scene.SceneOpenToolId,
457
- $@"{{""sceneRef"":{{""assetPath"":""{scenePath}""}}}}");
458
-
459
- // set-active (both)
460
- yield return RunToolBothThreads(Tool_Scene.SceneSetActiveToolId,
461
- $@"{{""sceneRef"":{{""assetPath"":""{scenePath}""}}}}");
462
-
463
- // save (both)
464
- yield return RunToolBothThreads(Tool_Scene.SceneSaveToolId, "{}");
465
- }
466
-
467
- [UnityTest]
468
- public IEnumerator SceneUnload_BothThreads()
469
- {
470
- EnsureTmpFolder();
471
-
472
- // Create a saved scene so we can open additional scenes additively.
473
- var basePath = $"{TmpFolder}/unload_base.unity";
474
- yield return RunToolMainThreadCoop(Tool_Scene.SceneCreateToolId,
475
- $@"{{""path"":""{basePath}""}}");
476
-
477
- // Create two extra scenes additively (allowed now because base is saved).
478
- var mainPath = $"{TmpFolder}/unload_main.unity";
479
- var bgPath = $"{TmpFolder}/unload_bg.unity";
480
- yield return RunToolMainThreadCoop(Tool_Scene.SceneCreateToolId,
481
- $@"{{""path"":""{mainPath}"",""newSceneMode"":""Additive""}}");
482
- yield return RunToolMainThreadCoop(Tool_Scene.SceneCreateToolId,
483
- $@"{{""path"":""{bgPath}"",""newSceneMode"":""Additive""}}");
484
-
485
- // Unload each additive scene from both threads (tool takes scene name, not path).
486
- yield return RunToolMainThreadCoop(Tool_Scene.SceneUnloadToolId,
487
- @"{""name"":""unload_main""}");
488
- yield return RunToolFromBackgroundThread(Tool_Scene.SceneUnloadToolId,
489
- @"{""name"":""unload_bg""}");
490
- }
491
-
492
- // ================================================================
493
- // SCRIPT tools
494
- // ================================================================
495
-
496
- [UnityTest]
497
- public IEnumerator ScriptUpdateOrCreate_ReadDelete_BothThreads()
498
- {
499
- EnsureTmpFolder();
500
- var pathA = $"{TmpFolder}/S_a.cs";
501
- var pathB = $"{TmpFolder}/S_b.cs";
502
- const string SrcA = "public class BgSa { public static void M() {} }";
503
- const string SrcB = "public class BgSb { public static void M() {} }";
504
-
505
- // update-or-create
506
- yield return RunToolMainThreadCoop(Tool_Script.ScriptUpdateOrCreateToolId,
507
- $@"{{""filePath"":""{pathA}"",""content"":{JsonString(SrcA)}}}");
508
- yield return RunToolFromBackgroundThread(Tool_Script.ScriptUpdateOrCreateToolId,
509
- $@"{{""filePath"":""{pathB}"",""content"":{JsonString(SrcB)}}}");
510
-
511
- // read (main + bg)
512
- yield return RunToolBothThreads(Tool_Script.ScriptReadToolId,
513
- $@"{{""filePath"":""{pathA}""}}",
514
- $@"{{""filePath"":""{pathB}""}}");
515
-
516
- // delete
517
- yield return RunToolMainThreadCoop(Tool_Script.ScriptDeleteToolId, $@"{{""files"":[""{pathA}""]}}");
518
- yield return RunToolFromBackgroundThread(Tool_Script.ScriptDeleteToolId,
519
- $@"{{""files"":[""{pathB}""]}}");
520
- }
521
-
522
- [UnityTest]
523
- public IEnumerator ScriptExecute_BothThreads()
524
- {
525
- const string Code = "public class Script { public static int Main() { return 42; } }";
526
- yield return RunToolBothThreads(Tool_Script.ScriptExecuteToolId,
527
- $@"{{""csharpCode"":{JsonString(Code)},""className"":""Script"",""methodName"":""Main""}}");
528
- }
529
-
530
- // ================================================================
531
- // TOOL CONTROL
532
- // ================================================================
533
-
534
- [UnityTest]
535
- public IEnumerator ToolSetEnabledState_BothThreads()
536
- {
537
- // Enable unity-tool-list twice (idempotent, already enabled) from both threads.
538
- yield return RunToolBothThreads(Tool_Tool.ToolSetEnabledStateId,
539
- @"{""tools"":[{""name"":""unity-tool-list"",""enabled"":true}]}");
540
- }
541
-
542
- // ================================================================
543
- // REFLECTION (complex MethodRef schema → thread-safety only)
544
- // ================================================================
545
-
546
- [UnityTest]
547
- public IEnumerator ReflectionMethodFind_ThreadSafetyOnly()
548
- {
549
- yield return RunToolExpectNoThreadViolation(Tool_Reflection.ReflectionMethodFindToolId,
550
- @"{""filter"":{""Namespace"":""UnityEngine"",""TypeName"":""Transform"",""MethodName"":""LookAt""}}");
551
- }
552
-
553
- [UnityTest]
554
- public IEnumerator ReflectionMethodCall_BothThreads()
555
- {
556
- // Call a static method on a test-scoped GameObject's Transform via instance method.
557
- var go = new GameObject("bg_reflect");
558
- var id = go.GetInstanceID();
559
- yield return RunToolBothThreads(Tool_Reflection.ReflectionMethodCallToolId,
560
- $@"{{""filter"":{{""Namespace"":""UnityEngine"",""TypeName"":""GameObject"",""MethodName"":""Find"",""InputParameters"":[{{""TypeName"":""System.String"",""Name"":""name""}}]}},""inputParameters"":[{{""typeName"":""System.String"",""name"":""name"",""value"":""bg_reflect""}}]}}");
561
- }
562
-
563
- // ================================================================
564
- // PACKAGE MANAGER — package-add / package-remove intentionally NOT
565
- // exercised here: both return status "Processing" and kick off a real
566
- // Unity PackageManager resolution on a background task, which in turn
567
- // forces a domain reload. Under the test runner that hangs the whole
568
- // batch-mode run. Their thread-safety is already covered by the fact
569
- // that their entry path is a synchronous parameter check.
570
- // ================================================================
571
-
572
- [UnityTest]
573
- public IEnumerator PackageSearch_ThreadSafetyOnly()
574
- {
575
- // Registry round-trip is slow and flaky under test; thread-safety only.
576
- yield return RunToolExpectNoThreadViolation(Tool_Package.PackageSearchToolId,
577
- @"{""query"":""com.unity.textmeshpro"",""maxResults"":1,""offlineMode"":true}");
578
- }
579
-
580
- // ================================================================
581
- // SCREENSHOTS — not guaranteed to have a camera/view in batchmode.
582
- // ================================================================
583
-
584
- [UnityTest]
585
- public IEnumerator ScreenshotCamera_BothThreads()
586
- {
587
- new GameObject("TestCamera").AddComponent<Camera>();
588
- yield return RunToolBothThreads(Tool_Screenshot.ScreenshotCameraToolId, @"{""width"":64,""height"":64}");
589
- }
590
-
591
- [UnityTest]
592
- public IEnumerator ScreenshotGameView_ThreadSafetyOnly()
593
- => RunToolExpectNoThreadViolation(Tool_Screenshot.ScreenshotGameViewToolId, "{}");
594
-
595
- [UnityTest]
596
- public IEnumerator ScreenshotSceneView_ThreadSafetyOnly()
597
- => RunToolExpectNoThreadViolation(Tool_Screenshot.ScreenshotSceneViewToolId, @"{""width"":64,""height"":64}");
598
-
599
- // ================================================================
600
- // TESTS — recursive; skip deliberately.
601
- // ================================================================
602
- // tests-run intentionally has no test here: executing it from within the test
603
- // runner causes recursion and a hang. Its thread safety is implicit because the
604
- // test runner itself schedules the invocation onto the main thread.
605
-
606
- // ================================================================
607
- // Helpers
608
- // ================================================================
609
-
610
- static void EnsureTmpFolder()
611
- {
612
- if (!AssetDatabase.IsValidFolder(TmpFolder))
613
- AssetDatabase.CreateFolder("Assets", TmpFolderName);
614
- }
615
-
616
- static string CreateTmpTextAsset(string fileName, string contents)
617
- {
618
- EnsureTmpFolder();
619
- var path = $"{TmpFolder}/{fileName}";
620
- File.WriteAllText(path, contents);
621
- AssetDatabase.ImportAsset(path);
622
- return path;
623
- }
624
-
625
- static string? FindFirstShaderPath()
626
- {
627
- // Prefer the built-in Standard shader.
628
- var standard = Shader.Find("Standard");
629
- if (standard != null)
630
- {
631
- var p = AssetDatabase.GetAssetPath(standard);
632
- if (!string.IsNullOrEmpty(p))
633
- return p;
634
- }
635
-
636
- var guids = AssetDatabase.FindAssets("t:Shader");
637
- foreach (var guid in guids)
638
- {
639
- var p = AssetDatabase.GUIDToAssetPath(guid);
640
- if (!string.IsNullOrEmpty(p))
641
- return p;
642
- }
643
- return null;
644
- }
645
-
646
- /// <summary>Returns a JSON string literal (quoted, backslash-escaped).</summary>
647
- static string JsonString(string raw)
648
- => System.Text.Json.JsonSerializer.Serialize(raw);
649
- }
650
- }
651
- #endif
1
+ /*
2
+ ┌──────────────────────────────────────────────────────────────────┐
3
+ │ Author: Ivan Murzak (https://github.com/IvanMurzak) │
4
+ │ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │
5
+ │ Copyright (c) 2025 Ivan Murzak │
6
+ │ Licensed under the Apache License, Version 2.0. │
7
+ │ See the LICENSE file in the project root for more information. │
8
+ └──────────────────────────────────────────────────────────────────┘
9
+ */
10
+
11
+ #nullable enable
12
+ #if !UNITY_6000_5_OR_NEWER
13
+ using System.Collections;
14
+ using System.IO;
15
+ using com.AtelierAI.Unity.Copilot.Editor.API;
16
+ using NUnit.Framework;
17
+ using UnityEditor;
18
+ using UnityEngine;
19
+ using UnityEngine.TestTools;
20
+
21
+ namespace com.AtelierAI.Unity.Copilot.Editor.Tests
22
+ {
23
+ /// <summary>
24
+ /// Thread-safety coverage for Uco tools — regression for
25
+ /// <see href="https://github.com/IvanMurzak/uco-plugin/issues/633">#633</see>.
26
+ ///
27
+ /// For each tool we want to guarantee the tool's body handles invocation from any
28
+ /// thread (the Uco server dispatches tool calls from a SignalR background thread,
29
+ /// so tools that touch Unity APIs must marshal back to the main thread via
30
+ /// <c>MainThread.Instance.Run</c>). Forgetting to do so caused the regression in
31
+ /// <see href="https://github.com/IvanMurzak/uco-plugin/issues/632">#632</see> /
32
+ /// <see href="https://github.com/IvanMurzak/uco-plugin/pull/637">#637</see>
33
+ /// (<c>console-clear-logs</c>).
34
+ ///
35
+ /// <b>Excluded tools</b>: <c>package-add</c>/<c>package-remove</c> (trigger domain
36
+ /// reload), <c>tests-run</c> (recursive hang), system tools (<c>ping</c>,
37
+ /// <c>unity-skill-*</c>). See inline comments for rationale.
38
+ ///
39
+ /// Two helpers from <see cref="BaseTest"/> are used:
40
+ /// <list type="bullet">
41
+ /// <item><see cref="BaseTest.RunToolBothThreads"/> — happy path; asserts the tool
42
+ /// succeeds on both threads. Used when crafting a valid input is practical.</item>
43
+ /// <item><see cref="BaseTest.RunToolExpectNoThreadViolation"/> — lenient mode; tolerates
44
+ /// business-level errors and only fails the test if Unity surfaces a
45
+ /// <c>"can only be called from the main thread"</c> exception. Used for tools
46
+ /// that need elaborate setup (<c>SerializedMember</c>, prefab stages, etc.) or
47
+ /// whose happy path would mutate the project in risky ways
48
+ /// (<c>package-add</c>/<c>package-remove</c>, <c>tests-run</c>, etc.).</item>
49
+ /// </list>
50
+ /// </summary>
51
+ public class ToolThreadSafetyTests : BaseTest
52
+ {
53
+ const string TmpFolderName = "BgThreadTests_TMP";
54
+ const string TmpFolder = "Assets/" + TmpFolderName;
55
+
56
+ [TearDown]
57
+ public void BgTearDown()
58
+ {
59
+ if (AssetDatabase.IsValidFolder(TmpFolder))
60
+ AssetDatabase.DeleteAsset(TmpFolder);
61
+ }
62
+
63
+ // ================================================================
64
+ // NO-ARG / READ-ONLY tools (happy path, empty JSON)
65
+ // ================================================================
66
+
67
+ [UnityTest] public IEnumerator ToolList_BothThreads()
68
+ => RunToolBothThreads(Tool_Tool.ToolListId, "{}");
69
+
70
+ [UnityTest] public IEnumerator GameObjectComponentListAll_BothThreads()
71
+ => RunToolBothThreads(Tool_GameObject.ComponentListToolId, @"{""pageSize"":3}");
72
+
73
+ [UnityTest] public IEnumerator SceneListOpened_BothThreads()
74
+ => RunToolBothThreads(Tool_Scene.SceneListOpenedToolId, "{}");
75
+
76
+ [UnityTest] public IEnumerator SceneGetData_BothThreads()
77
+ => RunToolBothThreads(Tool_Scene.SceneGetDataToolId, "{}");
78
+
79
+ [UnityTest] public IEnumerator EditorApplicationGetState_BothThreads()
80
+ => RunToolBothThreads(Tool_Editor.EditorApplicationGetStateToolId, "{}");
81
+
82
+ [UnityTest] public IEnumerator EditorSelectionGet_BothThreads()
83
+ => RunToolBothThreads(Tool_Editor_Selection.EditorSelectionGetToolId, "{}");
84
+
85
+ [UnityTest] public IEnumerator ConsoleGetLogs_BothThreads()
86
+ => RunToolBothThreads(Tool_Console.ConsoleGetLogsToolId, "{}");
87
+
88
+ [UnityTest]
89
+ public IEnumerator ConsoleClearLogs_BothThreads()
90
+ {
91
+ // Regression for #632/#637: ClearLogs calls Debug.ClearDeveloperConsole() which
92
+ // MUST be invoked on the main thread.
93
+ yield return RunToolBothThreads(Tool_Console.ConsoleClearLogsToolId, "{}");
94
+ }
95
+
96
+ [UnityTest] public IEnumerator AssetsFind_BothThreads()
97
+ => RunToolBothThreads(Tool_Assets.AssetsFindToolId, @"{""filter"":""t:Scene"",""maxResults"":3}");
98
+
99
+ [UnityTest] public IEnumerator AssetsFindBuiltIn_BothThreads()
100
+ => RunToolBothThreads(Tool_Assets.AssetsFindBuiltInToolId, @"{""maxResults"":3}");
101
+
102
+ [UnityTest] public IEnumerator AssetsShaderListAll_BothThreads()
103
+ => RunToolBothThreads(Tool_Assets_Shader.AssetsShaderListAllToolId, "{}");
104
+
105
+ [UnityTest] public IEnumerator AssetsRefresh_BothThreads()
106
+ => RunToolBothThreads(Tool_Assets.AssetsRefreshToolId, "{}");
107
+
108
+ [UnityTest] public IEnumerator PackageList_BothThreads()
109
+ => RunToolBothThreads(Tool_Package.PackageListToolId, "{}");
110
+
111
+ [UnityTest] public IEnumerator TypeGetJsonSchema_BothThreads()
112
+ => RunToolBothThreads(Tool_Type.TypeGetJsonSchemaToolId, @"{""typeName"":""UnityEngine.Vector3""}");
113
+
114
+ // ================================================================
115
+ // GAMEOBJECT tools — happy path, per-call setup
116
+ // ================================================================
117
+
118
+ [UnityTest]
119
+ public IEnumerator GameObjectCreate_BothThreads()
120
+ {
121
+ yield return RunToolMainThreadCoop(Tool_GameObject.GameObjectCreateToolId, @"{""name"":""bg_main""}");
122
+ Assert.IsNotNull(GameObject.Find("bg_main"));
123
+ yield return RunToolFromBackgroundThread(Tool_GameObject.GameObjectCreateToolId, @"{""name"":""bg_bg""}");
124
+ Assert.IsNotNull(GameObject.Find("bg_bg"));
125
+ }
126
+
127
+ [UnityTest]
128
+ public IEnumerator GameObjectFind_BothThreads()
129
+ {
130
+ var go = new GameObject("bg_find");
131
+ var id = go.GetInstanceID();
132
+ yield return RunToolBothThreads(Tool_GameObject.GameObjectFindToolId,
133
+ $@"{{""gameObjectRef"":{{""instanceID"":{id}}}}}");
134
+ }
135
+
136
+ [UnityTest]
137
+ public IEnumerator GameObjectDestroy_BothThreads()
138
+ {
139
+ var a = new GameObject("bg_destroy_a").GetInstanceID();
140
+ yield return RunToolMainThreadCoop(Tool_GameObject.GameObjectDestroyToolId,
141
+ $@"{{""gameObjectRef"":{{""instanceID"":{a}}}}}");
142
+
143
+ var b = new GameObject("bg_destroy_b").GetInstanceID();
144
+ yield return RunToolFromBackgroundThread(Tool_GameObject.GameObjectDestroyToolId,
145
+ $@"{{""gameObjectRef"":{{""instanceID"":{b}}}}}");
146
+ }
147
+
148
+ [UnityTest]
149
+ public IEnumerator GameObjectDuplicate_BothThreads()
150
+ {
151
+ var a = new GameObject("bg_dup_a").GetInstanceID();
152
+ yield return RunToolMainThreadCoop(Tool_GameObject.GameObjectDuplicateToolId,
153
+ $@"{{""gameObjectRefs"":[{{""instanceID"":{a}}}]}}");
154
+
155
+ var b = new GameObject("bg_dup_b").GetInstanceID();
156
+ yield return RunToolFromBackgroundThread(Tool_GameObject.GameObjectDuplicateToolId,
157
+ $@"{{""gameObjectRefs"":[{{""instanceID"":{b}}}]}}");
158
+ }
159
+
160
+ [UnityTest]
161
+ public IEnumerator GameObjectSetParent_BothThreads()
162
+ {
163
+ var parent = new GameObject("bg_parent").GetInstanceID();
164
+ var child1 = new GameObject("bg_child_a").GetInstanceID();
165
+ yield return RunToolMainThreadCoop(Tool_GameObject.GameObjectSetParentToolId,
166
+ $@"{{""gameObjectRefs"":[{{""instanceID"":{child1}}}],""parentGameObjectRef"":{{""instanceID"":{parent}}}}}");
167
+
168
+ var child2 = new GameObject("bg_child_b").GetInstanceID();
169
+ yield return RunToolFromBackgroundThread(Tool_GameObject.GameObjectSetParentToolId,
170
+ $@"{{""gameObjectRefs"":[{{""instanceID"":{child2}}}],""parentGameObjectRef"":{{""instanceID"":{parent}}}}}");
171
+ }
172
+
173
+ [UnityTest]
174
+ public IEnumerator GameObjectComponentAdd_BothThreads()
175
+ {
176
+ var a = new GameObject("bg_addc_a").GetInstanceID();
177
+ yield return RunToolMainThreadCoop(Tool_GameObject.GameObjectComponentAddToolId,
178
+ $@"{{""gameObjectRef"":{{""instanceID"":{a}}},""componentNames"":[""UnityEngine.BoxCollider""]}}");
179
+
180
+ var b = new GameObject("bg_addc_b").GetInstanceID();
181
+ yield return RunToolFromBackgroundThread(Tool_GameObject.GameObjectComponentAddToolId,
182
+ $@"{{""gameObjectRef"":{{""instanceID"":{b}}},""componentNames"":[""UnityEngine.SphereCollider""]}}");
183
+ }
184
+
185
+ [UnityTest]
186
+ public IEnumerator GameObjectComponentGet_BothThreads()
187
+ {
188
+ var go = new GameObject("bg_getc");
189
+ var id = go.GetInstanceID();
190
+ // Transform lives at component index 0 on every GameObject.
191
+ yield return RunToolBothThreads(Tool_GameObject.GameObjectComponentGetToolId,
192
+ $@"{{""gameObjectRef"":{{""instanceID"":{id}}},""componentRef"":{{""index"":0}}}}");
193
+ }
194
+
195
+ [UnityTest]
196
+ public IEnumerator GameObjectComponentDestroy_BothThreads()
197
+ {
198
+ var a = new GameObject("bg_destc_a");
199
+ a.AddComponent<BoxCollider>();
200
+ var idA = a.GetInstanceID();
201
+ yield return RunToolMainThreadCoop(Tool_GameObject.GameObjectComponentDestroyToolId,
202
+ $@"{{""gameObjectRef"":{{""instanceID"":{idA}}},""destroyComponentRefs"":[{{""typeName"":""UnityEngine.BoxCollider""}}]}}");
203
+
204
+ var b = new GameObject("bg_destc_b");
205
+ b.AddComponent<SphereCollider>();
206
+ var idB = b.GetInstanceID();
207
+ yield return RunToolFromBackgroundThread(Tool_GameObject.GameObjectComponentDestroyToolId,
208
+ $@"{{""gameObjectRef"":{{""instanceID"":{idB}}},""destroyComponentRefs"":[{{""typeName"":""UnityEngine.SphereCollider""}}]}}");
209
+ }
210
+
211
+ [UnityTest]
212
+ public IEnumerator GameObjectModify_ThreadSafetyOnly()
213
+ {
214
+ // gameobject-modify requires a SerializedMember tree — we cover thread safety
215
+ // only (business errors from an empty diff are tolerated).
216
+ var id = new GameObject("bg_modify").GetInstanceID();
217
+ yield return RunToolExpectNoThreadViolation(Tool_GameObject.GameObjectModifyToolId,
218
+ $@"{{""gameObjectRefs"":[{{""instanceID"":{id}}}],""gameObjectDiffs"":[{{""name"":""root""}}]}}");
219
+ }
220
+
221
+ [UnityTest]
222
+ public IEnumerator GameObjectComponentModify_ThreadSafetyOnly()
223
+ {
224
+ var go = new GameObject("bg_cmodify");
225
+ go.AddComponent<BoxCollider>();
226
+ var id = go.GetInstanceID();
227
+ yield return RunToolExpectNoThreadViolation(Tool_GameObject.GameObjectComponentModifyToolId,
228
+ $@"{{""gameObjectRef"":{{""instanceID"":{id}}},""componentRef"":{{""typeName"":""UnityEngine.BoxCollider""}},""componentDiff"":{{""name"":""root""}}}}");
229
+ }
230
+
231
+ // ================================================================
232
+ // OBJECT tools
233
+ // ================================================================
234
+
235
+ [UnityTest]
236
+ public IEnumerator ObjectGetData_BothThreads()
237
+ {
238
+ var id = new GameObject("bg_obj").GetInstanceID();
239
+ yield return RunToolBothThreads(Tool_Object.ObjectGetDataToolId,
240
+ $@"{{""objectRef"":{{""instanceID"":{id}}}}}");
241
+ }
242
+
243
+ [UnityTest]
244
+ public IEnumerator ObjectModify_ThreadSafetyOnly()
245
+ {
246
+ var id = new GameObject("bg_objmod").GetInstanceID();
247
+ yield return RunToolExpectNoThreadViolation(Tool_Object.ObjectModifyToolId,
248
+ $@"{{""objectRef"":{{""instanceID"":{id}}},""objectDiff"":{{""name"":""root""}}}}");
249
+ }
250
+
251
+ // ================================================================
252
+ // EDITOR tools
253
+ // ================================================================
254
+
255
+ [UnityTest]
256
+ public IEnumerator EditorSelectionSet_BothThreads()
257
+ {
258
+ var id = new GameObject("bg_select").GetInstanceID();
259
+ yield return RunToolBothThreads(Tool_Editor_Selection.EditorSelectionSetToolId,
260
+ $@"{{""select"":[{{""instanceID"":{id}}}]}}");
261
+ }
262
+
263
+ [UnityTest]
264
+ public IEnumerator EditorApplicationSetState_BothThreads()
265
+ {
266
+ // No-op toggle: current state is not-playing, not-paused.
267
+ yield return RunToolBothThreads(Tool_Editor.EditorApplicationSetStateToolId,
268
+ @"{""isPlaying"":false,""isPaused"":false}");
269
+ }
270
+
271
+ // ================================================================
272
+ // ASSETS tools
273
+ // ================================================================
274
+
275
+ [UnityTest]
276
+ public IEnumerator AssetsCreateFolder_BothThreads()
277
+ {
278
+ EnsureTmpFolder();
279
+ yield return RunToolMainThreadCoop(Tool_Assets.AssetsCreateFolderToolId,
280
+ $@"{{""inputs"":[{{""parentFolderPath"":""{TmpFolder}"",""newFolderName"":""a""}}]}}");
281
+ yield return RunToolFromBackgroundThread(Tool_Assets.AssetsCreateFolderToolId,
282
+ $@"{{""inputs"":[{{""parentFolderPath"":""{TmpFolder}"",""newFolderName"":""b""}}]}}");
283
+ }
284
+
285
+ [UnityTest]
286
+ public IEnumerator AssetsGetData_BothThreads()
287
+ {
288
+ var assetPath = CreateTmpTextAsset("get_data.txt", "hello");
289
+ yield return RunToolBothThreads(Tool_Assets.AssetsGetDataToolId,
290
+ $@"{{""assetRef"":{{""assetPath"":""{assetPath}""}}}}");
291
+ }
292
+
293
+ [UnityTest]
294
+ public IEnumerator AssetsModify_ThreadSafetyOnly()
295
+ {
296
+ var assetPath = CreateTmpTextAsset("modify.txt", "hello");
297
+ yield return RunToolExpectNoThreadViolation(Tool_Assets.AssetsModifyToolId,
298
+ $@"{{""assetRef"":{{""assetPath"":""{assetPath}""}},""content"":{{""name"":""root""}}}}");
299
+ }
300
+
301
+ [UnityTest]
302
+ public IEnumerator AssetsMaterialCreate_BothThreads()
303
+ {
304
+ EnsureTmpFolder();
305
+ yield return RunToolMainThreadCoop(Tool_Assets.AssetsMaterialCreateToolId,
306
+ $@"{{""assetPath"":""{TmpFolder}/mat_main.mat"",""shaderName"":""Standard""}}");
307
+ yield return RunToolFromBackgroundThread(Tool_Assets.AssetsMaterialCreateToolId,
308
+ $@"{{""assetPath"":""{TmpFolder}/mat_bg.mat"",""shaderName"":""Standard""}}");
309
+ }
310
+
311
+ [UnityTest]
312
+ public IEnumerator AssetsShaderGetData_BothThreads()
313
+ {
314
+ // Find any shader asset in the project to query.
315
+ var shaderPath = FindFirstShaderPath();
316
+ if (shaderPath == null)
317
+ {
318
+ Assert.Ignore("No shader asset available in project.");
319
+ yield break;
320
+ }
321
+ yield return RunToolBothThreads(Tool_Assets_Shader.AssetsShaderGetDataToolId,
322
+ $@"{{""assetRef"":{{""assetPath"":""{shaderPath}""}}}}");
323
+ }
324
+
325
+ [UnityTest]
326
+ public IEnumerator AssetsCopy_BothThreads()
327
+ {
328
+ var src = CreateTmpTextAsset("copy_src.txt", "source");
329
+ yield return RunToolMainThreadCoop(Tool_Assets.AssetsCopyToolId,
330
+ $@"{{""sourcePaths"":[""{src}""],""destinationPaths"":[""{TmpFolder}/copy_main.txt""]}}");
331
+ yield return RunToolFromBackgroundThread(Tool_Assets.AssetsCopyToolId,
332
+ $@"{{""sourcePaths"":[""{src}""],""destinationPaths"":[""{TmpFolder}/copy_bg.txt""]}}");
333
+ }
334
+
335
+ [UnityTest]
336
+ public IEnumerator AssetsMove_BothThreads()
337
+ {
338
+ var src1 = CreateTmpTextAsset("move_a.txt", "a");
339
+ yield return RunToolMainThreadCoop(Tool_Assets.AssetsMoveToolId,
340
+ $@"{{""sourcePaths"":[""{src1}""],""destinationPaths"":[""{TmpFolder}/move_a_done.txt""]}}");
341
+
342
+ var src2 = CreateTmpTextAsset("move_b.txt", "b");
343
+ yield return RunToolFromBackgroundThread(Tool_Assets.AssetsMoveToolId,
344
+ $@"{{""sourcePaths"":[""{src2}""],""destinationPaths"":[""{TmpFolder}/move_b_done.txt""]}}");
345
+ }
346
+
347
+ [UnityTest]
348
+ public IEnumerator AssetsDelete_BothThreads()
349
+ {
350
+ var src1 = CreateTmpTextAsset("del_a.txt", "a");
351
+ yield return RunToolMainThreadCoop(Tool_Assets.AssetsDeleteToolId, $@"{{""paths"":[""{src1}""]}}");
352
+
353
+ var src2 = CreateTmpTextAsset("del_b.txt", "b");
354
+ yield return RunToolFromBackgroundThread(Tool_Assets.AssetsDeleteToolId,
355
+ $@"{{""paths"":[""{src2}""]}}");
356
+ }
357
+
358
+ // ---------- Prefabs ----------
359
+
360
+ [UnityTest]
361
+ public IEnumerator AssetsPrefabCreate_BothThreads()
362
+ {
363
+ EnsureTmpFolder();
364
+ var idA = new GameObject("bg_prefab_a").GetInstanceID();
365
+ yield return RunToolMainThreadCoop(Tool_Assets_Prefab.AssetsPrefabCreateToolId,
366
+ $@"{{""prefabAssetPath"":""{TmpFolder}/prefab_a.prefab"",""gameObjectRef"":{{""instanceID"":{idA}}}}}");
367
+
368
+ var idB = new GameObject("bg_prefab_b").GetInstanceID();
369
+ yield return RunToolFromBackgroundThread(Tool_Assets_Prefab.AssetsPrefabCreateToolId,
370
+ $@"{{""prefabAssetPath"":""{TmpFolder}/prefab_b.prefab"",""gameObjectRef"":{{""instanceID"":{idB}}}}}");
371
+ }
372
+
373
+ [UnityTest]
374
+ public IEnumerator AssetsPrefabInstantiate_BothThreads()
375
+ {
376
+ EnsureTmpFolder();
377
+ var seedId = new GameObject("bg_seed_prefab").GetInstanceID();
378
+ var prefabPath = $"{TmpFolder}/seed.prefab";
379
+ yield return RunToolMainThreadCoop(Tool_Assets_Prefab.AssetsPrefabCreateToolId,
380
+ $@"{{""prefabAssetPath"":""{prefabPath}"",""gameObjectRef"":{{""instanceID"":{seedId}}}}}");
381
+
382
+ yield return RunToolMainThreadCoop(Tool_Assets_Prefab.AssetsPrefabInstantiateToolId,
383
+ $@"{{""prefabAssetPath"":""{prefabPath}"",""gameObjectPath"":""bg_inst_main""}}");
384
+ yield return RunToolFromBackgroundThread(Tool_Assets_Prefab.AssetsPrefabInstantiateToolId,
385
+ $@"{{""prefabAssetPath"":""{prefabPath}"",""gameObjectPath"":""bg_inst_bg""}}");
386
+ }
387
+
388
+ [UnityTest]
389
+ public IEnumerator AssetsPrefabOpenSaveClose_BothThreads()
390
+ {
391
+ EnsureTmpFolder();
392
+
393
+ // Create a prefab asset from a scene GameObject.
394
+ var seedA = new GameObject("bg_prefab_osc_a").GetInstanceID();
395
+ var prefabPathA = $"{TmpFolder}/osc_a.prefab";
396
+ yield return RunToolMainThreadCoop(Tool_Assets_Prefab.AssetsPrefabCreateToolId,
397
+ $@"{{""prefabAssetPath"":""{prefabPathA}"",""gameObjectRef"":{{""instanceID"":{seedA}}}}}");
398
+
399
+ // Instantiate it so we get a prefab instance to open.
400
+ yield return RunToolMainThreadCoop(Tool_Assets_Prefab.AssetsPrefabInstantiateToolId,
401
+ $@"{{""prefabAssetPath"":""{prefabPathA}"",""gameObjectPath"":""bg_osc_inst_a""}}");
402
+ var instanceA = GameObject.Find("bg_osc_inst_a");
403
+ Assert.IsNotNull(instanceA, "Prefab instance not found after instantiate.");
404
+
405
+ // Open → Save → Close (main thread).
406
+ yield return RunToolMainThreadCoop(Tool_Assets_Prefab.AssetsPrefabOpenToolId,
407
+ $@"{{""gameObjectRef"":{{""instanceID"":{instanceA!.GetInstanceID()}}}}}");
408
+ yield return RunToolMainThreadCoop(Tool_Assets_Prefab.AssetsPrefabSaveToolId, "{}");
409
+ yield return RunToolMainThreadCoop(Tool_Assets_Prefab.AssetsPrefabCloseToolId, @"{""save"":false}");
410
+
411
+ // Same sequence from a background thread.
412
+ var seedB = new GameObject("bg_prefab_osc_b").GetInstanceID();
413
+ var prefabPathB = $"{TmpFolder}/osc_b.prefab";
414
+ yield return RunToolMainThreadCoop(Tool_Assets_Prefab.AssetsPrefabCreateToolId,
415
+ $@"{{""prefabAssetPath"":""{prefabPathB}"",""gameObjectRef"":{{""instanceID"":{seedB}}}}}");
416
+ yield return RunToolMainThreadCoop(Tool_Assets_Prefab.AssetsPrefabInstantiateToolId,
417
+ $@"{{""prefabAssetPath"":""{prefabPathB}"",""gameObjectPath"":""bg_osc_inst_b""}}");
418
+ var instanceB = GameObject.Find("bg_osc_inst_b");
419
+ Assert.IsNotNull(instanceB, "Prefab instance not found after instantiate.");
420
+
421
+ yield return RunToolFromBackgroundThread(Tool_Assets_Prefab.AssetsPrefabOpenToolId,
422
+ $@"{{""gameObjectRef"":{{""instanceID"":{instanceB!.GetInstanceID()}}}}}");
423
+ yield return RunToolFromBackgroundThread(Tool_Assets_Prefab.AssetsPrefabSaveToolId, "{}");
424
+ yield return RunToolFromBackgroundThread(Tool_Assets_Prefab.AssetsPrefabCloseToolId, @"{""save"":false}");
425
+ }
426
+
427
+ // ================================================================
428
+ // SCENE tools
429
+ // ================================================================
430
+
431
+ [UnityTest]
432
+ public IEnumerator SceneCreate_BothThreads()
433
+ {
434
+ EnsureTmpFolder();
435
+ var startingActive = UnityEngine.SceneManagement.SceneManager.GetActiveScene();
436
+ yield return RunToolMainThreadCoop(Tool_Scene.SceneCreateToolId,
437
+ $@"{{""path"":""{TmpFolder}/scene_main.unity""}}");
438
+ yield return RunToolFromBackgroundThread(Tool_Scene.SceneCreateToolId,
439
+ $@"{{""path"":""{TmpFolder}/scene_bg.unity""}}");
440
+
441
+ // Restore a baseline active scene if the test clobbered it.
442
+ if (startingActive.IsValid() && UnityEngine.SceneManagement.SceneManager.GetActiveScene() != startingActive)
443
+ UnityEngine.SceneManagement.SceneManager.SetActiveScene(startingActive);
444
+ }
445
+
446
+ [UnityTest]
447
+ public IEnumerator SceneOpenSaveSetActive_BothThreads()
448
+ {
449
+ EnsureTmpFolder();
450
+ var scenePath = $"{TmpFolder}/reopen.unity";
451
+ yield return RunToolMainThreadCoop(Tool_Scene.SceneCreateToolId, $@"{{""path"":""{scenePath}""}}");
452
+
453
+ // open (main + bg)
454
+ yield return RunToolMainThreadCoop(Tool_Scene.SceneOpenToolId,
455
+ $@"{{""sceneRef"":{{""assetPath"":""{scenePath}""}}}}");
456
+ yield return RunToolFromBackgroundThread(Tool_Scene.SceneOpenToolId,
457
+ $@"{{""sceneRef"":{{""assetPath"":""{scenePath}""}}}}");
458
+
459
+ // set-active (both)
460
+ yield return RunToolBothThreads(Tool_Scene.SceneSetActiveToolId,
461
+ $@"{{""sceneRef"":{{""assetPath"":""{scenePath}""}}}}");
462
+
463
+ // save (both)
464
+ yield return RunToolBothThreads(Tool_Scene.SceneSaveToolId, "{}");
465
+ }
466
+
467
+ [UnityTest]
468
+ public IEnumerator SceneUnload_BothThreads()
469
+ {
470
+ EnsureTmpFolder();
471
+
472
+ // Create a saved scene so we can open additional scenes additively.
473
+ var basePath = $"{TmpFolder}/unload_base.unity";
474
+ yield return RunToolMainThreadCoop(Tool_Scene.SceneCreateToolId,
475
+ $@"{{""path"":""{basePath}""}}");
476
+
477
+ // Create two extra scenes additively (allowed now because base is saved).
478
+ var mainPath = $"{TmpFolder}/unload_main.unity";
479
+ var bgPath = $"{TmpFolder}/unload_bg.unity";
480
+ yield return RunToolMainThreadCoop(Tool_Scene.SceneCreateToolId,
481
+ $@"{{""path"":""{mainPath}"",""newSceneMode"":""Additive""}}");
482
+ yield return RunToolMainThreadCoop(Tool_Scene.SceneCreateToolId,
483
+ $@"{{""path"":""{bgPath}"",""newSceneMode"":""Additive""}}");
484
+
485
+ // Unload each additive scene from both threads (tool takes scene name, not path).
486
+ yield return RunToolMainThreadCoop(Tool_Scene.SceneUnloadToolId,
487
+ @"{""name"":""unload_main""}");
488
+ yield return RunToolFromBackgroundThread(Tool_Scene.SceneUnloadToolId,
489
+ @"{""name"":""unload_bg""}");
490
+ }
491
+
492
+ // ================================================================
493
+ // SCRIPT tools
494
+ // ================================================================
495
+
496
+ [UnityTest]
497
+ public IEnumerator ScriptUpdateOrCreate_ReadDelete_BothThreads()
498
+ {
499
+ EnsureTmpFolder();
500
+ var pathA = $"{TmpFolder}/S_a.cs";
501
+ var pathB = $"{TmpFolder}/S_b.cs";
502
+ const string SrcA = "public class BgSa { public static void M() {} }";
503
+ const string SrcB = "public class BgSb { public static void M() {} }";
504
+
505
+ // update-or-create
506
+ yield return RunToolMainThreadCoop(Tool_Script.ScriptUpdateOrCreateToolId,
507
+ $@"{{""filePath"":""{pathA}"",""content"":{JsonString(SrcA)}}}");
508
+ yield return RunToolFromBackgroundThread(Tool_Script.ScriptUpdateOrCreateToolId,
509
+ $@"{{""filePath"":""{pathB}"",""content"":{JsonString(SrcB)}}}");
510
+
511
+ // read (main + bg)
512
+ yield return RunToolBothThreads(Tool_Script.ScriptReadToolId,
513
+ $@"{{""filePath"":""{pathA}""}}",
514
+ $@"{{""filePath"":""{pathB}""}}");
515
+
516
+ // delete
517
+ yield return RunToolMainThreadCoop(Tool_Script.ScriptDeleteToolId, $@"{{""files"":[""{pathA}""]}}");
518
+ yield return RunToolFromBackgroundThread(Tool_Script.ScriptDeleteToolId,
519
+ $@"{{""files"":[""{pathB}""]}}");
520
+ }
521
+
522
+ [UnityTest]
523
+ public IEnumerator ScriptExecute_BothThreads()
524
+ {
525
+ const string Code = "public class Script { public static int Main() { return 42; } }";
526
+ yield return RunToolBothThreads(Tool_Script.ScriptExecuteToolId,
527
+ $@"{{""csharpCode"":{JsonString(Code)},""className"":""Script"",""methodName"":""Main""}}");
528
+ }
529
+
530
+ // ================================================================
531
+ // TOOL CONTROL
532
+ // ================================================================
533
+
534
+ [UnityTest]
535
+ public IEnumerator ToolSetEnabledState_BothThreads()
536
+ {
537
+ // Enable unity-tool-list twice (idempotent, already enabled) from both threads.
538
+ yield return RunToolBothThreads(Tool_Tool.ToolSetEnabledStateId,
539
+ @"{""tools"":[{""name"":""unity-tool-list"",""enabled"":true}]}");
540
+ }
541
+
542
+ // ================================================================
543
+ // REFLECTION (complex MethodRef schema → thread-safety only)
544
+ // ================================================================
545
+
546
+ [UnityTest]
547
+ public IEnumerator ReflectionMethodFind_ThreadSafetyOnly()
548
+ {
549
+ yield return RunToolExpectNoThreadViolation(Tool_Reflection.ReflectionMethodFindToolId,
550
+ @"{""filter"":{""Namespace"":""UnityEngine"",""TypeName"":""Transform"",""MethodName"":""LookAt""}}");
551
+ }
552
+
553
+ [UnityTest]
554
+ public IEnumerator ReflectionMethodCall_BothThreads()
555
+ {
556
+ // Call a static method on a test-scoped GameObject's Transform via instance method.
557
+ var go = new GameObject("bg_reflect");
558
+ var id = go.GetInstanceID();
559
+ yield return RunToolBothThreads(Tool_Reflection.ReflectionMethodCallToolId,
560
+ $@"{{""filter"":{{""Namespace"":""UnityEngine"",""TypeName"":""GameObject"",""MethodName"":""Find"",""InputParameters"":[{{""TypeName"":""System.String"",""Name"":""name""}}]}},""inputParameters"":[{{""typeName"":""System.String"",""name"":""name"",""value"":""bg_reflect""}}]}}");
561
+ }
562
+
563
+ // ================================================================
564
+ // PACKAGE MANAGER — package-add / package-remove intentionally NOT
565
+ // exercised here: both return status "Processing" and kick off a real
566
+ // Unity PackageManager resolution on a background task, which in turn
567
+ // forces a domain reload. Under the test runner that hangs the whole
568
+ // batch-mode run. Their thread-safety is already covered by the fact
569
+ // that their entry path is a synchronous parameter check.
570
+ // ================================================================
571
+
572
+ [UnityTest]
573
+ public IEnumerator PackageSearch_ThreadSafetyOnly()
574
+ {
575
+ // Registry round-trip is slow and flaky under test; thread-safety only.
576
+ yield return RunToolExpectNoThreadViolation(Tool_Package.PackageSearchToolId,
577
+ @"{""query"":""com.unity.textmeshpro"",""maxResults"":1,""offlineMode"":true}");
578
+ }
579
+
580
+ // ================================================================
581
+ // SCREENSHOTS — not guaranteed to have a camera/view in batchmode.
582
+ // ================================================================
583
+
584
+ [UnityTest]
585
+ public IEnumerator ScreenshotCamera_BothThreads()
586
+ {
587
+ new GameObject("TestCamera").AddComponent<Camera>();
588
+ yield return RunToolBothThreads(Tool_Screenshot.ScreenshotCameraToolId, @"{""width"":64,""height"":64}");
589
+ }
590
+
591
+ [UnityTest]
592
+ public IEnumerator ScreenshotGameView_ThreadSafetyOnly()
593
+ => RunToolExpectNoThreadViolation(Tool_Screenshot.ScreenshotGameViewToolId, "{}");
594
+
595
+ [UnityTest]
596
+ public IEnumerator ScreenshotSceneView_ThreadSafetyOnly()
597
+ => RunToolExpectNoThreadViolation(Tool_Screenshot.ScreenshotSceneViewToolId, @"{""width"":64,""height"":64}");
598
+
599
+ // ================================================================
600
+ // TESTS — recursive; skip deliberately.
601
+ // ================================================================
602
+ // tests-run intentionally has no test here: executing it from within the test
603
+ // runner causes recursion and a hang. Its thread safety is implicit because the
604
+ // test runner itself schedules the invocation onto the main thread.
605
+
606
+ // ================================================================
607
+ // Helpers
608
+ // ================================================================
609
+
610
+ static void EnsureTmpFolder()
611
+ {
612
+ if (!AssetDatabase.IsValidFolder(TmpFolder))
613
+ AssetDatabase.CreateFolder("Assets", TmpFolderName);
614
+ }
615
+
616
+ static string CreateTmpTextAsset(string fileName, string contents)
617
+ {
618
+ EnsureTmpFolder();
619
+ var path = $"{TmpFolder}/{fileName}";
620
+ File.WriteAllText(path, contents);
621
+ AssetDatabase.ImportAsset(path);
622
+ return path;
623
+ }
624
+
625
+ static string? FindFirstShaderPath()
626
+ {
627
+ // Prefer the built-in Standard shader.
628
+ var standard = Shader.Find("Standard");
629
+ if (standard != null)
630
+ {
631
+ var p = AssetDatabase.GetAssetPath(standard);
632
+ if (!string.IsNullOrEmpty(p))
633
+ return p;
634
+ }
635
+
636
+ var guids = AssetDatabase.FindAssets("t:Shader");
637
+ foreach (var guid in guids)
638
+ {
639
+ var p = AssetDatabase.GUIDToAssetPath(guid);
640
+ if (!string.IsNullOrEmpty(p))
641
+ return p;
642
+ }
643
+ return null;
644
+ }
645
+
646
+ /// <summary>Returns a JSON string literal (quoted, backslash-escaped).</summary>
647
+ static string JsonString(string raw)
648
+ => System.Text.Json.JsonSerializer.Serialize(raw);
649
+ }
650
+ }
651
+ #endif