@atelierai/uco 1.0.7 → 1.0.9

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 (107) hide show
  1. package/CHANGELOG.md +29 -8
  2. package/package.json +2 -2
  3. package/vendor/plugin/com.atelierai.unity.copilot/CHANGELOG.md +4 -4
  4. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetConfig.cs +132 -132
  5. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetDependencyResolver.cs +237 -237
  6. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetDownloader.cs +82 -82
  7. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Resource/MenuItems.cs +247 -247
  8. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Resource/ToolGroups.cs +153 -153
  9. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/SystemTool/Skills.Create.SkillBody.cs +155 -155
  10. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Console.GetLogs.cs +95 -95
  11. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Docs.HttpFetcher.cs +139 -139
  12. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Instance.ListAll.cs +72 -72
  13. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/PathReadHelper.cs +106 -106
  14. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Script.Validate.cs +252 -252
  15. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Tests/TestResultCollector.cs +537 -537
  16. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Tests.cs +400 -400
  17. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/ToolGroups.SetEnabled.cs +92 -92
  18. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Branding/ProductInfo.cs +62 -62
  19. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/CopilotServerManager.cs +887 -1034
  20. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Skills/Skill_InitialSetup.cs +197 -197
  21. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.Editor.cs +201 -201
  22. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.cs +67 -67
  23. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/CopilotListWindowBase.cs +393 -393
  24. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/CopilotWindowBase.cs +113 -113
  25. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.Connection.cs +3 -3
  26. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotFeatures.cs +195 -195
  27. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotServer.cs +22 -22
  28. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CreateGUI.cs +354 -354
  29. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.cs +78 -78
  30. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowInitializer.cs +47 -47
  31. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/SerializationCheckWindow.cs +190 -190
  32. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Build.cs +91 -91
  33. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Config.cs +298 -298
  34. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Static.cs +296 -296
  35. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.cs +69 -69
  36. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/PackageUtils.cs +168 -168
  37. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/PrefsKeyMigration.cs +119 -119
  38. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/SafeDefaultsGuard.cs +180 -180
  39. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ScriptUtils.cs +229 -229
  40. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ToolGroupRegistry.cs +461 -460
  41. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ToolsManifestGenerator.cs +200 -200
  42. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityAuthoringAdapters.cs +1467 -1467
  43. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityAuthoringSafetyReloadGuard.cs +65 -65
  44. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityCopilotUpdateProjectSettings.cs +66 -66
  45. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotPromptsWindow.uss +11 -11
  46. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotResourcesWindow.uss +16 -16
  47. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotToolsWindow.uss +36 -36
  48. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/MainWindow.uss +183 -183
  49. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/{_mcp-list-window.uss → _copilot-list-window.uss} +18 -18
  50. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/_list-view.uss +77 -77
  51. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotPromptsWindow.uxml +18 -18
  52. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotResourcesWindow.uxml +18 -18
  53. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotToolsWindow.uxml +18 -18
  54. package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/MainWindow.uxml +115 -115
  55. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/ReflectorNet.dll +0 -0
  56. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.Common.dll +0 -0
  57. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.dll +0 -0
  58. package/vendor/plugin/com.atelierai.unity.copilot/README.md +88 -88
  59. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Converter/Json/Structures/EntityIdConverter.cs +120 -120
  60. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Data/GameObjectMetadata.cs +110 -110
  61. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Data/GameObjectMetadata.pre-Unity.6.5.cs +110 -110
  62. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Unity/Logs/FileLogStorage.cs +547 -547
  63. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.Build.cs +243 -243
  64. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.Config.cs +3 -3
  65. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.cs +295 -295
  66. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginBuilder.cs +105 -105
  67. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginRuntime.Static.cs +155 -155
  68. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginRuntime.cs +59 -59
  69. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Utils/EnvironmentUtils.cs +11 -11
  70. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AtomicApi/AtomicApiTestFixtures.cs +99 -99
  71. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AtomicApi/TryModifyAtTests.cs +227 -227
  72. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AuthoringSafetyPilotTests.cs +1050 -1050
  73. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetExtractorDevelopmentDependencyTests.cs +148 -148
  74. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetExtractorFlatLayoutTests.cs +166 -166
  75. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetInstallManifestTests.cs +436 -436
  76. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetLegacyMigrationTests.cs +304 -304
  77. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetPackageInstallerStaleVersionCleanupTests.cs +200 -200
  78. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Environment/EnvironmentUtilsTests.cs +2 -2
  79. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/BlacklistTypeTests.cs +416 -416
  80. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.cs +172 -172
  81. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.pre-Unity.6.5.cs +169 -169
  82. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPopulationTests.cs +182 -182
  83. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPopulationTests.pre-Unity.6.5.cs +179 -179
  84. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.cs +175 -175
  85. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.pre-Unity.6.5.cs +174 -174
  86. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.cs +171 -171
  87. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.pre-Unity.6.5.cs +170 -170
  88. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsCreateFolderTests.cs +243 -243
  89. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsMaterialTests.cs +101 -101
  90. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsPrefabCreateTests.cs +348 -348
  91. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsPrefabCreateTests.pre-Unity.6.5.cs +348 -348
  92. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsShaderTests.cs +194 -194
  93. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/GameObject/TestJsonSchema.cs +199 -199
  94. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/PathBasedToolTests.cs +573 -573
  95. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/PathBasedToolTests.pre-Unity.6.5.cs +513 -513
  96. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotCameraTests.cs +154 -154
  97. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotCameraTests.pre-Unity.6.5.cs +154 -154
  98. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotGameViewTests.cs +164 -164
  99. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotSceneViewTests.cs +155 -155
  100. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolParameterTests.cs +56 -56
  101. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolThreadSafetyTests.cs +651 -651
  102. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolThreadSafetyTests.pre-Unity.6.5.cs +651 -651
  103. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ToolBatchExecutionIntegrationTests.cs +407 -407
  104. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/{McpPluginTests.cs → UcoPluginTests.cs} +105 -105
  105. package/vendor/plugin/com.atelierai.unity.copilot/package.json +1 -1
  106. /package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/{_mcp-list-window.uss.meta → _copilot-list-window.uss.meta} +0 -0
  107. /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