@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,537 +1,537 @@
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
- using System;
13
- using System.Collections.Generic;
14
- using System.Linq;
15
- using System.Text.Json;
16
- using com.AtelierAI.Uco.Framework.Common.Model;
17
- using com.AtelierAI.Unity.Copilot.Editor.Utils;
18
- using com.AtelierAI.Unity.Copilot.Runtime.Utils;
19
- using UnityEditor.TestTools.TestRunner.Api;
20
- using UnityEngine;
21
-
22
- namespace com.AtelierAI.Unity.Copilot.Editor.API.TestRunner
23
- {
24
- internal sealed class TestRunCallbackOwnership
25
- {
26
- internal sealed class Context
27
- {
28
- public string OperationId = string.Empty;
29
- public string RequestId = string.Empty;
30
- public bool IncludePassingTests;
31
- public bool IncludeMessage;
32
- public bool IncludeMessageStacktrace;
33
- public bool IncludeLogs;
34
- public int IncludeLogsMinLevel;
35
- public bool IncludeLogsStacktrace;
36
- /// <summary>
37
- /// Filtered leaf count reported by RunStarted for the owning run.
38
- /// RunFinished prefers it over counting the whole Unity result
39
- /// tree when the persisted ExpectedMatchedTests is stale/zero
40
- /// (COCli-12: "total=1319 while executed=487").
41
- /// </summary>
42
- public int RunStartedTestCount;
43
- }
44
-
45
- readonly object _gate = new();
46
- Context? _active;
47
-
48
- internal bool HasUnsettledRun
49
- {
50
- get
51
- {
52
- lock (_gate)
53
- return _active != null;
54
- }
55
- }
56
-
57
- internal string ActiveOperationId
58
- {
59
- get
60
- {
61
- lock (_gate)
62
- return _active?.OperationId ?? string.Empty;
63
- }
64
- }
65
-
66
- internal bool CaptureCurrentRun()
67
- {
68
- var context = new Context
69
- {
70
- OperationId = TestResultCollector.TestOperationId.Value,
71
- RequestId = TestResultCollector.TestCallRequestID.Value,
72
- IncludePassingTests = TestResultCollector.IncludePassingTests.Value,
73
- IncludeMessage = TestResultCollector.IncludeMessage.Value,
74
- IncludeMessageStacktrace = TestResultCollector.IncludeMessageStacktrace.Value,
75
- IncludeLogs = TestResultCollector.IncludeLogs.Value,
76
- IncludeLogsMinLevel = TestResultCollector.IncludeLogsMinLevel.Value,
77
- IncludeLogsStacktrace = TestResultCollector.IncludeLogsStacktrace.Value
78
- };
79
-
80
- lock (_gate)
81
- {
82
- if (_active != null)
83
- return false;
84
- _active = context;
85
- return true;
86
- }
87
- }
88
-
89
- internal void SetRunStartedTestCount(int count)
90
- {
91
- lock (_gate)
92
- {
93
- if (_active != null)
94
- _active.RunStartedTestCount = count;
95
- }
96
- }
97
-
98
- internal void UpdateFromTestFinished(int executedTests, int matchedTests)
99
- {
100
- string operationId;
101
- lock (_gate)
102
- operationId = _active?.OperationId ?? string.Empty;
103
- if (string.IsNullOrEmpty(operationId))
104
- return;
105
-
106
- var operation = EditorOperationRegistry.Get(operationId);
107
- if (operation == null || operation.IsTerminal)
108
- return;
109
- var total = Math.Max(1, matchedTests);
110
- EditorOperationRegistry.Update(operationId, "executing",
111
- Math.Min(1d, (double)executedTests / total));
112
- }
113
-
114
- internal Context? ReleaseForRunFinished()
115
- {
116
- lock (_gate)
117
- {
118
- var released = _active;
119
- _active = null;
120
- return released;
121
- }
122
- }
123
-
124
- internal bool CompleteFromRunFinished(
125
- Context? context,
126
- int failedTests,
127
- int executedTests,
128
- string resultJson)
129
- {
130
- if (context == null)
131
- return false;
132
-
133
- var completed = TestResultCollector.CompleteOperationFromCallback(
134
- context.OperationId, failedTests, executedTests, resultJson);
135
- ClearPersistedOwnershipIfMatching(context);
136
- return completed;
137
- }
138
-
139
- static void ClearPersistedOwnershipIfMatching(Context context)
140
- {
141
- if (!string.IsNullOrEmpty(context.OperationId)
142
- && string.Equals(TestResultCollector.TestOperationId.Value,
143
- context.OperationId, StringComparison.Ordinal))
144
- {
145
- TestResultCollector.TestOperationId.Value = string.Empty;
146
- TestResultCollector.ExpectedDiscoveredTests.Value = 0;
147
- TestResultCollector.ExpectedMatchedTests.Value = 0;
148
- }
149
-
150
- if (!string.IsNullOrEmpty(context.RequestId)
151
- && string.Equals(TestResultCollector.TestCallRequestID.Value,
152
- context.RequestId, StringComparison.Ordinal))
153
- {
154
- TestResultCollector.TestCallRequestID.Value = string.Empty;
155
- }
156
- }
157
- }
158
-
159
- public class TestResultCollector : ICallbacks
160
- {
161
- static volatile int counter = 0;
162
- static readonly TestRunCallbackOwnership s_callbackOwnership = new();
163
-
164
- readonly TestRunCallbackOwnership _callbackOwnership;
165
- readonly object _logsMutex = new();
166
- readonly List<TestResultData> _results = new();
167
- readonly TestSummaryData _summary = new();
168
- readonly List<TestLogEntry> _logs = new();
169
- readonly TestMode _testMode;
170
-
171
- DateTime startTime;
172
-
173
- public List<TestResultData> GetResults() => _results;
174
- public TestSummaryData GetSummary() => _summary;
175
- public List<TestLogEntry> GetLogs()
176
- {
177
- lock (_logsMutex)
178
- {
179
- return _logs.ToList();
180
- }
181
- }
182
- public TestMode GetTestMode() => _testMode;
183
-
184
- public string TestModeAsString => _testMode switch
185
- {
186
- TestMode.EditMode => "EditMode",
187
- TestMode.PlayMode => "PlayMode",
188
- _ => "Unknown"
189
- };
190
-
191
- public static PlayerPrefsString TestCallRequestID = new PlayerPrefsString("Unity_MCP_TestRunner_TestCallRequestID");
192
- public static PlayerPrefsString TestOperationId = new PlayerPrefsString("Unity_MCP_TestRunner_TestOperationId");
193
- public static PlayerPrefsInt ExpectedDiscoveredTests = new PlayerPrefsInt("Unity_MCP_TestRunner_DiscoveredTests");
194
- public static PlayerPrefsInt ExpectedMatchedTests = new PlayerPrefsInt("Unity_MCP_TestRunner_MatchedTests");
195
-
196
- internal static int ExpectedDiscoveredTestCount
197
- {
198
- get => ExpectedDiscoveredTests.Value;
199
- set => ExpectedDiscoveredTests.Value = value;
200
- }
201
-
202
- internal static int ExpectedMatchedTestCount
203
- {
204
- get => ExpectedMatchedTests.Value;
205
- set => ExpectedMatchedTests.Value = value;
206
- }
207
-
208
- public static PlayerPrefsBool IncludePassingTests = new PlayerPrefsBool("Unity_MCP_TestRunner_IncludePassingTests");
209
- public static PlayerPrefsBool IncludeMessage = new PlayerPrefsBool("Unity_MCP_TestRunner_IncludeMessage", true);
210
- public static PlayerPrefsBool IncludeMessageStacktrace = new PlayerPrefsBool("Unity_MCP_TestRunner_IncludeStacktrace");
211
-
212
- public static PlayerPrefsBool IncludeLogs = new PlayerPrefsBool("Unity_MCP_TestRunner_IncludeLogs");
213
- public static PlayerPrefsInt IncludeLogsMinLevel = new PlayerPrefsInt("Unity_MCP_TestRunner_IncludeLogsMinLevel", (int)LogType.Warning);
214
- public static PlayerPrefsBool IncludeLogsStacktrace = new PlayerPrefsBool("Unity_MCP_TestRunner_IncludeLogsStacktrace");
215
-
216
- internal static bool HasUnsettledRun => s_callbackOwnership.HasUnsettledRun;
217
- internal static string ActiveOperationId => s_callbackOwnership.ActiveOperationId;
218
-
219
- public TestResultCollector() : this(s_callbackOwnership, enforceSingleton: true)
220
- {
221
- }
222
-
223
- TestResultCollector(
224
- TestRunCallbackOwnership callbackOwnership,
225
- bool enforceSingleton)
226
- {
227
- _callbackOwnership = callbackOwnership
228
- ?? throw new ArgumentNullException(nameof(callbackOwnership));
229
- if (!enforceSingleton)
230
- return;
231
-
232
- int newCount = System.Threading.Interlocked.Increment(ref counter);
233
-
234
- UnityCopilotPluginEditor.Instance.LogTrace("Ctor", typeof(TestResultCollector));
235
-
236
- if (newCount > 1)
237
- throw new InvalidOperationException($"Only one instance of {nameof(TestResultCollector)} is allowed. Current count: {newCount}");
238
- }
239
-
240
- internal static TestResultCollector CreateIsolatedForTests()
241
- => new(new TestRunCallbackOwnership(), enforceSingleton: false);
242
-
243
- public void RunStarted(ITestAdaptor testsToRun)
244
- {
245
- UnityCopilotPluginEditor.Instance.LogInfo("RunStarted", typeof(TestResultCollector));
246
-
247
- if (!_callbackOwnership.CaptureCurrentRun())
248
- {
249
- UnityCopilotPluginEditor.Instance.LogError(
250
- "Ignoring overlapping test RunStarted callback while operation '{operationId}' still owns the runner.",
251
- typeof(TestResultCollector), _callbackOwnership.ActiveOperationId);
252
- return;
253
- }
254
-
255
- startTime = DateTime.Now;
256
- var testCount = CountTests(testsToRun);
257
- _callbackOwnership.SetRunStartedTestCount(testCount);
258
-
259
- lock (_logsMutex)
260
- {
261
- _logs.Clear();
262
- }
263
- _results.Clear();
264
- _summary.Clear();
265
- _summary.TotalTests = testCount;
266
- _summary.DiscoveredTests = ExpectedDiscoveredTests.Value;
267
- _summary.MatchedTests = ExpectedMatchedTests.Value;
268
-
269
- // Subscribe to log messages (using threaded version to catch logs from all threads)
270
- Application.logMessageReceivedThreaded -= OnLogMessageReceived;
271
- Application.logMessageReceivedThreaded += OnLogMessageReceived;
272
-
273
- UnityCopilotPluginEditor.Instance.LogInfo("Run {testMode} started: {testCount} tests.",
274
- typeof(TestResultCollector), TestModeAsString, testCount);
275
- }
276
-
277
- public void RunFinished(ITestResultAdaptor result)
278
- {
279
- UnityCopilotPluginEditor.Instance.LogInfo("RunFinished", typeof(TestResultCollector));
280
-
281
- // Unsubscribe from log messages
282
- Application.logMessageReceivedThreaded -= OnLogMessageReceived;
283
-
284
- // Release the ownership context up front: its captured members
285
- // (including RunStartedTestCount) feed the summary fallback below
286
- // and the structured response afterwards.
287
- var ownership = _callbackOwnership.ReleaseForRunFinished();
288
-
289
- var duration = DateTime.Now - startTime;
290
- _summary.Duration = DateTime.Now - startTime;
291
- _summary.ExecutedTests = _results.Count;
292
- // COCli-12: TotalTests reports the filtered execution scope, never
293
- // the whole-project discovery count. Preference order: the
294
- // discovery-matched count, the RunStarted filtered count, then
295
- // (only for runs that bypassed discovery) the executed count
296
- // before falling back to counting the Unity result tree.
297
- _summary.TotalTests = _summary.MatchedTests > 0
298
- ? _summary.MatchedTests
299
- : ownership is { RunStartedTestCount: > 0 }
300
- ? ownership.RunStartedTestCount
301
- : Math.Max(_summary.ExecutedTests, CountTests(result.Test));
302
- if (_summary.FailedTests > 0)
303
- {
304
- _summary.Status = TestRunStatus.Failed;
305
- }
306
- else if (_summary.PassedTests > 0)
307
- {
308
- _summary.Status = TestRunStatus.Passed;
309
- }
310
- else
311
- {
312
- _summary.Status = TestRunStatus.Unknown;
313
- }
314
-
315
- UnityCopilotPluginEditor.Instance.LogInfo("Run {testMode} finished with {totalTests} test results. Result status: {status}",
316
- typeof(TestResultCollector), TestModeAsString, _summary.TotalTests, result.TestStatus);
317
- UnityCopilotPluginEditor.Instance.LogInfo("Final duration: {duration:mm\\:ss\\.fff}. Completed: {completed}/{total}",
318
- typeof(TestResultCollector), duration, _results.Count, _summary.TotalTests);
319
-
320
- UnityCopilotPluginEditor.Instance.BuildMcpPluginIfNeeded();
321
-
322
- if (!EnvironmentUtils.IsCi() && !EnvironmentUtils.IsBatchMode())
323
- UnityCopilotPluginEditor.ConnectIfNeeded();
324
-
325
- var structuredResponse = CreateStructuredResponse(
326
- includePassingTests: ownership?.IncludePassingTests ?? IncludePassingTests.Value,
327
- includeMessage: ownership?.IncludeMessage ?? IncludeMessage.Value,
328
- includeLogs: ownership?.IncludeLogs ?? IncludeLogs.Value,
329
- includeLogsMinLevel: ownership?.IncludeLogsMinLevel ?? IncludeLogsMinLevel.Value,
330
- includeMessageStacktrace: ownership?.IncludeMessageStacktrace ?? IncludeMessageStacktrace.Value,
331
- includeLogsStacktrace: ownership?.IncludeLogsStacktrace ?? IncludeLogsStacktrace.Value);
332
-
333
- // Scene hygiene (COCli-06): restore a sandboxed run and report the
334
- // observed mutation against the persisted pre-run baseline.
335
- var sceneOutcome = TestRunSceneSandbox.RestoreForOperation(ownership?.OperationId);
336
- structuredResponse.Mutated = sceneOutcome.Mutation.Mutated;
337
- structuredResponse.MutatedScenes = sceneOutcome.Mutation.MutatedScenes;
338
- structuredResponse.SandboxRestored = sceneOutcome.SandboxRestored;
339
- structuredResponse.SandboxRestoreCause = sceneOutcome.SandboxRestoreCause;
340
-
341
- var resultJson = JsonSerializer.Serialize(structuredResponse);
342
- var completedOwnedOperation = _callbackOwnership.CompleteFromRunFinished(
343
- ownership, _summary.FailedTests, _summary.ExecutedTests, resultJson);
344
-
345
- var requestId = ownership?.RequestId ?? string.Empty;
346
- if (completedOwnedOperation && string.IsNullOrEmpty(requestId) == false)
347
- {
348
- var mcpPlugin = UnityCopilotPluginEditor.Instance.UcoPluginInstance ?? throw new InvalidOperationException("MCP Plugin instance is not available.");
349
-
350
- var response = ResponseCallValueTool<TestRunResponse>
351
- .SuccessStructured(mcpPlugin.UcoManager.Reflector.JsonSerializer.SerializeToNode(structuredResponse))
352
- .SetRequestID(requestId);
353
-
354
- _ = UnityCopilotPluginEditor.NotifyToolRequestCompleted(new RequestToolCompletedData
355
- {
356
- RequestId = requestId,
357
- OperationId = ownership?.OperationId,
358
- Result = response
359
- });
360
- }
361
- }
362
-
363
- internal static bool CompleteOperationFromCallback(
364
- string operationId,
365
- int failedTests,
366
- int executedTests,
367
- string resultJson)
368
- {
369
- var operation = EditorOperationRegistry.Get(operationId);
370
- if (operation == null || operation.IsTerminal)
371
- return false;
372
-
373
- if (operation.CancellationRequested)
374
- EditorOperationRegistry.CancelRunning(operationId, "test-run-cancelled", resultJson);
375
- else if (failedTests > 0)
376
- EditorOperationRegistry.Fail(operationId, "tests-failed",
377
- $"{failedTests} test(s) failed.", "completed-with-failures", resultJson);
378
- else if (executedTests == 0)
379
- EditorOperationRegistry.Fail(operationId, "tests-no-results",
380
- "Unity Test Runner completed without executing a matched test.", "completed-without-results", resultJson);
381
- else
382
- EditorOperationRegistry.Succeed(operationId, resultJson);
383
- return true;
384
- }
385
-
386
- public void TestStarted(ITestAdaptor test)
387
- {
388
- // Test started - could log this if needed
389
- }
390
-
391
- public void TestFinished(ITestResultAdaptor result)
392
- {
393
- // Only count actual tests, not test suites
394
- if (!result.Test.IsSuite)
395
- {
396
- var testResult = new TestResultData
397
- {
398
- Name = result.Test.FullName,
399
- Status = ConvertTestStatus(result.TestStatus),
400
- Duration = TimeSpan.FromSeconds(result.Duration),
401
- Message = result.Message,
402
- StackTrace = result.StackTrace
403
- };
404
-
405
- _results.Add(testResult);
406
-
407
- var statusEmoji = result.TestStatus switch
408
- {
409
- TestStatus.Passed => "<color=green>✅</color>",
410
- TestStatus.Failed => "<color=red>❌</color>",
411
- TestStatus.Skipped => "<color=yellow>⚠️</color>",
412
- _ => string.Empty
413
- };
414
-
415
- UnityCopilotPluginEditor.Instance.LogInfo("{emoji} Test finished ({counter}/{total}): {testName} - {testStatus}",
416
- typeof(TestResultCollector), statusEmoji, _results.Count, _summary.TotalTests, result.Test.FullName, result.TestStatus);
417
-
418
- // Update summary counts
419
- switch (result.TestStatus)
420
- {
421
- case TestStatus.Passed:
422
- _summary.PassedTests++;
423
- break;
424
- case TestStatus.Failed:
425
- _summary.FailedTests++;
426
- break;
427
- case TestStatus.Skipped:
428
- _summary.SkippedTests++;
429
- break;
430
- }
431
-
432
- // Update duration as tests complete
433
- _summary.Duration = DateTime.Now - startTime;
434
- _summary.ExecutedTests = _results.Count;
435
-
436
- _callbackOwnership.UpdateFromTestFinished(
437
- _results.Count, _summary.MatchedTests);
438
-
439
- // Check if all tests are complete
440
- if (_results.Count >= _summary.TotalTests)
441
- {
442
- UnityCopilotPluginEditor.Instance.LogInfo("All tests completed via TestFinished. Final duration: {duration:mm\\:ss\\.fff}",
443
- typeof(TestResultCollector), _summary.Duration);
444
- }
445
- }
446
- }
447
-
448
- void OnLogMessageReceived(string condition, string stackTrace, LogType type)
449
- {
450
- var entry = new TestLogEntry(type, condition, stackTrace);
451
- lock (_logsMutex)
452
- {
453
- _logs.Add(entry);
454
- }
455
- }
456
-
457
- TestRunResponse CreateStructuredResponse(
458
- bool includePassingTests,
459
- bool includeMessage,
460
- bool includeMessageStacktrace,
461
- bool includeLogs,
462
- int includeLogsMinLevel,
463
- bool includeLogsStacktrace)
464
- {
465
- var results = GetResults();
466
- var summary = GetSummary();
467
- var logs = GetLogs();
468
-
469
- var response = new TestRunResponse
470
- {
471
- Summary = summary,
472
- Results = new List<TestResultData>()
473
- };
474
-
475
- // Filter test results based on includePassingTests, includeMessage and includeMessageStacktrace
476
- foreach (var result in results)
477
- {
478
- // Skip passing tests if includePassingTests is false
479
- if (!includePassingTests && result.Status == TestResultStatus.Passed)
480
- continue;
481
-
482
- var filteredResult = new TestResultData
483
- {
484
- Name = result.Name,
485
- Status = result.Status,
486
- Duration = result.Duration,
487
- Message = includeMessage ? result.Message : null,
488
- StackTrace = includeMessageStacktrace ? result.StackTrace : null
489
- };
490
- response.Results.Add(filteredResult);
491
- }
492
-
493
- // Include logs if requested
494
- if (includeLogs && logs.Any())
495
- {
496
- var minLogLevel = TestLogEntry.ToLogLevel((LogType)includeLogsMinLevel);
497
- response.Logs = logs
498
- .Where(log => log.LogLevel >= minLogLevel)
499
- .Select(log => includeLogsStacktrace
500
- ? log
501
- : new TestLogEntry(log.Type, log.Condition, null, log.Timestamp))
502
- .ToList();
503
- }
504
-
505
- return response;
506
- }
507
-
508
- public static int CountTests(ITestAdaptor test)
509
- {
510
- try
511
- {
512
- if (test == null)
513
- return 0;
514
-
515
- if (test.HasChildren && test.Children != null)
516
- return test.Children.Sum(CountTests);
517
-
518
- return test.IsSuite ? 0 : 1;
519
- }
520
- catch
521
- {
522
- return 0;
523
- }
524
- }
525
-
526
- static TestResultStatus ConvertTestStatus(TestStatus testStatus)
527
- {
528
- return testStatus switch
529
- {
530
- TestStatus.Passed => TestResultStatus.Passed,
531
- TestStatus.Failed => TestResultStatus.Failed,
532
- TestStatus.Skipped => TestResultStatus.Skipped,
533
- _ => TestResultStatus.Skipped
534
- };
535
- }
536
- }
537
- }
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
+ using System;
13
+ using System.Collections.Generic;
14
+ using System.Linq;
15
+ using System.Text.Json;
16
+ using com.AtelierAI.Uco.Framework.Common.Model;
17
+ using com.AtelierAI.Unity.Copilot.Editor.Utils;
18
+ using com.AtelierAI.Unity.Copilot.Runtime.Utils;
19
+ using UnityEditor.TestTools.TestRunner.Api;
20
+ using UnityEngine;
21
+
22
+ namespace com.AtelierAI.Unity.Copilot.Editor.API.TestRunner
23
+ {
24
+ internal sealed class TestRunCallbackOwnership
25
+ {
26
+ internal sealed class Context
27
+ {
28
+ public string OperationId = string.Empty;
29
+ public string RequestId = string.Empty;
30
+ public bool IncludePassingTests;
31
+ public bool IncludeMessage;
32
+ public bool IncludeMessageStacktrace;
33
+ public bool IncludeLogs;
34
+ public int IncludeLogsMinLevel;
35
+ public bool IncludeLogsStacktrace;
36
+ /// <summary>
37
+ /// Filtered leaf count reported by RunStarted for the owning run.
38
+ /// RunFinished prefers it over counting the whole Unity result
39
+ /// tree when the persisted ExpectedMatchedTests is stale/zero
40
+ /// (COCli-12: "total=1319 while executed=487").
41
+ /// </summary>
42
+ public int RunStartedTestCount;
43
+ }
44
+
45
+ readonly object _gate = new();
46
+ Context? _active;
47
+
48
+ internal bool HasUnsettledRun
49
+ {
50
+ get
51
+ {
52
+ lock (_gate)
53
+ return _active != null;
54
+ }
55
+ }
56
+
57
+ internal string ActiveOperationId
58
+ {
59
+ get
60
+ {
61
+ lock (_gate)
62
+ return _active?.OperationId ?? string.Empty;
63
+ }
64
+ }
65
+
66
+ internal bool CaptureCurrentRun()
67
+ {
68
+ var context = new Context
69
+ {
70
+ OperationId = TestResultCollector.TestOperationId.Value,
71
+ RequestId = TestResultCollector.TestCallRequestID.Value,
72
+ IncludePassingTests = TestResultCollector.IncludePassingTests.Value,
73
+ IncludeMessage = TestResultCollector.IncludeMessage.Value,
74
+ IncludeMessageStacktrace = TestResultCollector.IncludeMessageStacktrace.Value,
75
+ IncludeLogs = TestResultCollector.IncludeLogs.Value,
76
+ IncludeLogsMinLevel = TestResultCollector.IncludeLogsMinLevel.Value,
77
+ IncludeLogsStacktrace = TestResultCollector.IncludeLogsStacktrace.Value
78
+ };
79
+
80
+ lock (_gate)
81
+ {
82
+ if (_active != null)
83
+ return false;
84
+ _active = context;
85
+ return true;
86
+ }
87
+ }
88
+
89
+ internal void SetRunStartedTestCount(int count)
90
+ {
91
+ lock (_gate)
92
+ {
93
+ if (_active != null)
94
+ _active.RunStartedTestCount = count;
95
+ }
96
+ }
97
+
98
+ internal void UpdateFromTestFinished(int executedTests, int matchedTests)
99
+ {
100
+ string operationId;
101
+ lock (_gate)
102
+ operationId = _active?.OperationId ?? string.Empty;
103
+ if (string.IsNullOrEmpty(operationId))
104
+ return;
105
+
106
+ var operation = EditorOperationRegistry.Get(operationId);
107
+ if (operation == null || operation.IsTerminal)
108
+ return;
109
+ var total = Math.Max(1, matchedTests);
110
+ EditorOperationRegistry.Update(operationId, "executing",
111
+ Math.Min(1d, (double)executedTests / total));
112
+ }
113
+
114
+ internal Context? ReleaseForRunFinished()
115
+ {
116
+ lock (_gate)
117
+ {
118
+ var released = _active;
119
+ _active = null;
120
+ return released;
121
+ }
122
+ }
123
+
124
+ internal bool CompleteFromRunFinished(
125
+ Context? context,
126
+ int failedTests,
127
+ int executedTests,
128
+ string resultJson)
129
+ {
130
+ if (context == null)
131
+ return false;
132
+
133
+ var completed = TestResultCollector.CompleteOperationFromCallback(
134
+ context.OperationId, failedTests, executedTests, resultJson);
135
+ ClearPersistedOwnershipIfMatching(context);
136
+ return completed;
137
+ }
138
+
139
+ static void ClearPersistedOwnershipIfMatching(Context context)
140
+ {
141
+ if (!string.IsNullOrEmpty(context.OperationId)
142
+ && string.Equals(TestResultCollector.TestOperationId.Value,
143
+ context.OperationId, StringComparison.Ordinal))
144
+ {
145
+ TestResultCollector.TestOperationId.Value = string.Empty;
146
+ TestResultCollector.ExpectedDiscoveredTests.Value = 0;
147
+ TestResultCollector.ExpectedMatchedTests.Value = 0;
148
+ }
149
+
150
+ if (!string.IsNullOrEmpty(context.RequestId)
151
+ && string.Equals(TestResultCollector.TestCallRequestID.Value,
152
+ context.RequestId, StringComparison.Ordinal))
153
+ {
154
+ TestResultCollector.TestCallRequestID.Value = string.Empty;
155
+ }
156
+ }
157
+ }
158
+
159
+ public class TestResultCollector : ICallbacks
160
+ {
161
+ static volatile int counter = 0;
162
+ static readonly TestRunCallbackOwnership s_callbackOwnership = new();
163
+
164
+ readonly TestRunCallbackOwnership _callbackOwnership;
165
+ readonly object _logsMutex = new();
166
+ readonly List<TestResultData> _results = new();
167
+ readonly TestSummaryData _summary = new();
168
+ readonly List<TestLogEntry> _logs = new();
169
+ readonly TestMode _testMode;
170
+
171
+ DateTime startTime;
172
+
173
+ public List<TestResultData> GetResults() => _results;
174
+ public TestSummaryData GetSummary() => _summary;
175
+ public List<TestLogEntry> GetLogs()
176
+ {
177
+ lock (_logsMutex)
178
+ {
179
+ return _logs.ToList();
180
+ }
181
+ }
182
+ public TestMode GetTestMode() => _testMode;
183
+
184
+ public string TestModeAsString => _testMode switch
185
+ {
186
+ TestMode.EditMode => "EditMode",
187
+ TestMode.PlayMode => "PlayMode",
188
+ _ => "Unknown"
189
+ };
190
+
191
+ public static PlayerPrefsString TestCallRequestID = new PlayerPrefsString("Unity_COPILOT_TestRunner_TestCallRequestID");
192
+ public static PlayerPrefsString TestOperationId = new PlayerPrefsString("Unity_COPILOT_TestRunner_TestOperationId");
193
+ public static PlayerPrefsInt ExpectedDiscoveredTests = new PlayerPrefsInt("Unity_COPILOT_TestRunner_DiscoveredTests");
194
+ public static PlayerPrefsInt ExpectedMatchedTests = new PlayerPrefsInt("Unity_COPILOT_TestRunner_MatchedTests");
195
+
196
+ internal static int ExpectedDiscoveredTestCount
197
+ {
198
+ get => ExpectedDiscoveredTests.Value;
199
+ set => ExpectedDiscoveredTests.Value = value;
200
+ }
201
+
202
+ internal static int ExpectedMatchedTestCount
203
+ {
204
+ get => ExpectedMatchedTests.Value;
205
+ set => ExpectedMatchedTests.Value = value;
206
+ }
207
+
208
+ public static PlayerPrefsBool IncludePassingTests = new PlayerPrefsBool("Unity_COPILOT_TestRunner_IncludePassingTests");
209
+ public static PlayerPrefsBool IncludeMessage = new PlayerPrefsBool("Unity_COPILOT_TestRunner_IncludeMessage", true);
210
+ public static PlayerPrefsBool IncludeMessageStacktrace = new PlayerPrefsBool("Unity_COPILOT_TestRunner_IncludeStacktrace");
211
+
212
+ public static PlayerPrefsBool IncludeLogs = new PlayerPrefsBool("Unity_COPILOT_TestRunner_IncludeLogs");
213
+ public static PlayerPrefsInt IncludeLogsMinLevel = new PlayerPrefsInt("Unity_COPILOT_TestRunner_IncludeLogsMinLevel", (int)LogType.Warning);
214
+ public static PlayerPrefsBool IncludeLogsStacktrace = new PlayerPrefsBool("Unity_COPILOT_TestRunner_IncludeLogsStacktrace");
215
+
216
+ internal static bool HasUnsettledRun => s_callbackOwnership.HasUnsettledRun;
217
+ internal static string ActiveOperationId => s_callbackOwnership.ActiveOperationId;
218
+
219
+ public TestResultCollector() : this(s_callbackOwnership, enforceSingleton: true)
220
+ {
221
+ }
222
+
223
+ TestResultCollector(
224
+ TestRunCallbackOwnership callbackOwnership,
225
+ bool enforceSingleton)
226
+ {
227
+ _callbackOwnership = callbackOwnership
228
+ ?? throw new ArgumentNullException(nameof(callbackOwnership));
229
+ if (!enforceSingleton)
230
+ return;
231
+
232
+ int newCount = System.Threading.Interlocked.Increment(ref counter);
233
+
234
+ UnityCopilotPluginEditor.Instance.LogTrace("Ctor", typeof(TestResultCollector));
235
+
236
+ if (newCount > 1)
237
+ throw new InvalidOperationException($"Only one instance of {nameof(TestResultCollector)} is allowed. Current count: {newCount}");
238
+ }
239
+
240
+ internal static TestResultCollector CreateIsolatedForTests()
241
+ => new(new TestRunCallbackOwnership(), enforceSingleton: false);
242
+
243
+ public void RunStarted(ITestAdaptor testsToRun)
244
+ {
245
+ UnityCopilotPluginEditor.Instance.LogInfo("RunStarted", typeof(TestResultCollector));
246
+
247
+ if (!_callbackOwnership.CaptureCurrentRun())
248
+ {
249
+ UnityCopilotPluginEditor.Instance.LogError(
250
+ "Ignoring overlapping test RunStarted callback while operation '{operationId}' still owns the runner.",
251
+ typeof(TestResultCollector), _callbackOwnership.ActiveOperationId);
252
+ return;
253
+ }
254
+
255
+ startTime = DateTime.Now;
256
+ var testCount = CountTests(testsToRun);
257
+ _callbackOwnership.SetRunStartedTestCount(testCount);
258
+
259
+ lock (_logsMutex)
260
+ {
261
+ _logs.Clear();
262
+ }
263
+ _results.Clear();
264
+ _summary.Clear();
265
+ _summary.TotalTests = testCount;
266
+ _summary.DiscoveredTests = ExpectedDiscoveredTests.Value;
267
+ _summary.MatchedTests = ExpectedMatchedTests.Value;
268
+
269
+ // Subscribe to log messages (using threaded version to catch logs from all threads)
270
+ Application.logMessageReceivedThreaded -= OnLogMessageReceived;
271
+ Application.logMessageReceivedThreaded += OnLogMessageReceived;
272
+
273
+ UnityCopilotPluginEditor.Instance.LogInfo("Run {testMode} started: {testCount} tests.",
274
+ typeof(TestResultCollector), TestModeAsString, testCount);
275
+ }
276
+
277
+ public void RunFinished(ITestResultAdaptor result)
278
+ {
279
+ UnityCopilotPluginEditor.Instance.LogInfo("RunFinished", typeof(TestResultCollector));
280
+
281
+ // Unsubscribe from log messages
282
+ Application.logMessageReceivedThreaded -= OnLogMessageReceived;
283
+
284
+ // Release the ownership context up front: its captured members
285
+ // (including RunStartedTestCount) feed the summary fallback below
286
+ // and the structured response afterwards.
287
+ var ownership = _callbackOwnership.ReleaseForRunFinished();
288
+
289
+ var duration = DateTime.Now - startTime;
290
+ _summary.Duration = DateTime.Now - startTime;
291
+ _summary.ExecutedTests = _results.Count;
292
+ // COCli-12: TotalTests reports the filtered execution scope, never
293
+ // the whole-project discovery count. Preference order: the
294
+ // discovery-matched count, the RunStarted filtered count, then
295
+ // (only for runs that bypassed discovery) the executed count
296
+ // before falling back to counting the Unity result tree.
297
+ _summary.TotalTests = _summary.MatchedTests > 0
298
+ ? _summary.MatchedTests
299
+ : ownership is { RunStartedTestCount: > 0 }
300
+ ? ownership.RunStartedTestCount
301
+ : Math.Max(_summary.ExecutedTests, CountTests(result.Test));
302
+ if (_summary.FailedTests > 0)
303
+ {
304
+ _summary.Status = TestRunStatus.Failed;
305
+ }
306
+ else if (_summary.PassedTests > 0)
307
+ {
308
+ _summary.Status = TestRunStatus.Passed;
309
+ }
310
+ else
311
+ {
312
+ _summary.Status = TestRunStatus.Unknown;
313
+ }
314
+
315
+ UnityCopilotPluginEditor.Instance.LogInfo("Run {testMode} finished with {totalTests} test results. Result status: {status}",
316
+ typeof(TestResultCollector), TestModeAsString, _summary.TotalTests, result.TestStatus);
317
+ UnityCopilotPluginEditor.Instance.LogInfo("Final duration: {duration:mm\\:ss\\.fff}. Completed: {completed}/{total}",
318
+ typeof(TestResultCollector), duration, _results.Count, _summary.TotalTests);
319
+
320
+ UnityCopilotPluginEditor.Instance.BuildUcoPluginIfNeeded();
321
+
322
+ if (!EnvironmentUtils.IsCi() && !EnvironmentUtils.IsBatchMode())
323
+ UnityCopilotPluginEditor.ConnectIfNeeded();
324
+
325
+ var structuredResponse = CreateStructuredResponse(
326
+ includePassingTests: ownership?.IncludePassingTests ?? IncludePassingTests.Value,
327
+ includeMessage: ownership?.IncludeMessage ?? IncludeMessage.Value,
328
+ includeLogs: ownership?.IncludeLogs ?? IncludeLogs.Value,
329
+ includeLogsMinLevel: ownership?.IncludeLogsMinLevel ?? IncludeLogsMinLevel.Value,
330
+ includeMessageStacktrace: ownership?.IncludeMessageStacktrace ?? IncludeMessageStacktrace.Value,
331
+ includeLogsStacktrace: ownership?.IncludeLogsStacktrace ?? IncludeLogsStacktrace.Value);
332
+
333
+ // Scene hygiene (COCli-06): restore a sandboxed run and report the
334
+ // observed mutation against the persisted pre-run baseline.
335
+ var sceneOutcome = TestRunSceneSandbox.RestoreForOperation(ownership?.OperationId);
336
+ structuredResponse.Mutated = sceneOutcome.Mutation.Mutated;
337
+ structuredResponse.MutatedScenes = sceneOutcome.Mutation.MutatedScenes;
338
+ structuredResponse.SandboxRestored = sceneOutcome.SandboxRestored;
339
+ structuredResponse.SandboxRestoreCause = sceneOutcome.SandboxRestoreCause;
340
+
341
+ var resultJson = JsonSerializer.Serialize(structuredResponse);
342
+ var completedOwnedOperation = _callbackOwnership.CompleteFromRunFinished(
343
+ ownership, _summary.FailedTests, _summary.ExecutedTests, resultJson);
344
+
345
+ var requestId = ownership?.RequestId ?? string.Empty;
346
+ if (completedOwnedOperation && string.IsNullOrEmpty(requestId) == false)
347
+ {
348
+ var ucoPlugin = UnityCopilotPluginEditor.Instance.UcoPluginInstance ?? throw new InvalidOperationException("Uco Plugin instance is not available.");
349
+
350
+ var response = ResponseCallValueTool<TestRunResponse>
351
+ .SuccessStructured(ucoPlugin.UcoManager.Reflector.JsonSerializer.SerializeToNode(structuredResponse))
352
+ .SetRequestID(requestId);
353
+
354
+ _ = UnityCopilotPluginEditor.NotifyToolRequestCompleted(new RequestToolCompletedData
355
+ {
356
+ RequestId = requestId,
357
+ OperationId = ownership?.OperationId,
358
+ Result = response
359
+ });
360
+ }
361
+ }
362
+
363
+ internal static bool CompleteOperationFromCallback(
364
+ string operationId,
365
+ int failedTests,
366
+ int executedTests,
367
+ string resultJson)
368
+ {
369
+ var operation = EditorOperationRegistry.Get(operationId);
370
+ if (operation == null || operation.IsTerminal)
371
+ return false;
372
+
373
+ if (operation.CancellationRequested)
374
+ EditorOperationRegistry.CancelRunning(operationId, "test-run-cancelled", resultJson);
375
+ else if (failedTests > 0)
376
+ EditorOperationRegistry.Fail(operationId, "tests-failed",
377
+ $"{failedTests} test(s) failed.", "completed-with-failures", resultJson);
378
+ else if (executedTests == 0)
379
+ EditorOperationRegistry.Fail(operationId, "tests-no-results",
380
+ "Unity Test Runner completed without executing a matched test.", "completed-without-results", resultJson);
381
+ else
382
+ EditorOperationRegistry.Succeed(operationId, resultJson);
383
+ return true;
384
+ }
385
+
386
+ public void TestStarted(ITestAdaptor test)
387
+ {
388
+ // Test started - could log this if needed
389
+ }
390
+
391
+ public void TestFinished(ITestResultAdaptor result)
392
+ {
393
+ // Only count actual tests, not test suites
394
+ if (!result.Test.IsSuite)
395
+ {
396
+ var testResult = new TestResultData
397
+ {
398
+ Name = result.Test.FullName,
399
+ Status = ConvertTestStatus(result.TestStatus),
400
+ Duration = TimeSpan.FromSeconds(result.Duration),
401
+ Message = result.Message,
402
+ StackTrace = result.StackTrace
403
+ };
404
+
405
+ _results.Add(testResult);
406
+
407
+ var statusEmoji = result.TestStatus switch
408
+ {
409
+ TestStatus.Passed => "<color=green>✅</color>",
410
+ TestStatus.Failed => "<color=red>❌</color>",
411
+ TestStatus.Skipped => "<color=yellow>⚠️</color>",
412
+ _ => string.Empty
413
+ };
414
+
415
+ UnityCopilotPluginEditor.Instance.LogInfo("{emoji} Test finished ({counter}/{total}): {testName} - {testStatus}",
416
+ typeof(TestResultCollector), statusEmoji, _results.Count, _summary.TotalTests, result.Test.FullName, result.TestStatus);
417
+
418
+ // Update summary counts
419
+ switch (result.TestStatus)
420
+ {
421
+ case TestStatus.Passed:
422
+ _summary.PassedTests++;
423
+ break;
424
+ case TestStatus.Failed:
425
+ _summary.FailedTests++;
426
+ break;
427
+ case TestStatus.Skipped:
428
+ _summary.SkippedTests++;
429
+ break;
430
+ }
431
+
432
+ // Update duration as tests complete
433
+ _summary.Duration = DateTime.Now - startTime;
434
+ _summary.ExecutedTests = _results.Count;
435
+
436
+ _callbackOwnership.UpdateFromTestFinished(
437
+ _results.Count, _summary.MatchedTests);
438
+
439
+ // Check if all tests are complete
440
+ if (_results.Count >= _summary.TotalTests)
441
+ {
442
+ UnityCopilotPluginEditor.Instance.LogInfo("All tests completed via TestFinished. Final duration: {duration:mm\\:ss\\.fff}",
443
+ typeof(TestResultCollector), _summary.Duration);
444
+ }
445
+ }
446
+ }
447
+
448
+ void OnLogMessageReceived(string condition, string stackTrace, LogType type)
449
+ {
450
+ var entry = new TestLogEntry(type, condition, stackTrace);
451
+ lock (_logsMutex)
452
+ {
453
+ _logs.Add(entry);
454
+ }
455
+ }
456
+
457
+ TestRunResponse CreateStructuredResponse(
458
+ bool includePassingTests,
459
+ bool includeMessage,
460
+ bool includeMessageStacktrace,
461
+ bool includeLogs,
462
+ int includeLogsMinLevel,
463
+ bool includeLogsStacktrace)
464
+ {
465
+ var results = GetResults();
466
+ var summary = GetSummary();
467
+ var logs = GetLogs();
468
+
469
+ var response = new TestRunResponse
470
+ {
471
+ Summary = summary,
472
+ Results = new List<TestResultData>()
473
+ };
474
+
475
+ // Filter test results based on includePassingTests, includeMessage and includeMessageStacktrace
476
+ foreach (var result in results)
477
+ {
478
+ // Skip passing tests if includePassingTests is false
479
+ if (!includePassingTests && result.Status == TestResultStatus.Passed)
480
+ continue;
481
+
482
+ var filteredResult = new TestResultData
483
+ {
484
+ Name = result.Name,
485
+ Status = result.Status,
486
+ Duration = result.Duration,
487
+ Message = includeMessage ? result.Message : null,
488
+ StackTrace = includeMessageStacktrace ? result.StackTrace : null
489
+ };
490
+ response.Results.Add(filteredResult);
491
+ }
492
+
493
+ // Include logs if requested
494
+ if (includeLogs && logs.Any())
495
+ {
496
+ var minLogLevel = TestLogEntry.ToLogLevel((LogType)includeLogsMinLevel);
497
+ response.Logs = logs
498
+ .Where(log => log.LogLevel >= minLogLevel)
499
+ .Select(log => includeLogsStacktrace
500
+ ? log
501
+ : new TestLogEntry(log.Type, log.Condition, null, log.Timestamp))
502
+ .ToList();
503
+ }
504
+
505
+ return response;
506
+ }
507
+
508
+ public static int CountTests(ITestAdaptor test)
509
+ {
510
+ try
511
+ {
512
+ if (test == null)
513
+ return 0;
514
+
515
+ if (test.HasChildren && test.Children != null)
516
+ return test.Children.Sum(CountTests);
517
+
518
+ return test.IsSuite ? 0 : 1;
519
+ }
520
+ catch
521
+ {
522
+ return 0;
523
+ }
524
+ }
525
+
526
+ static TestResultStatus ConvertTestStatus(TestStatus testStatus)
527
+ {
528
+ return testStatus switch
529
+ {
530
+ TestStatus.Passed => TestResultStatus.Passed,
531
+ TestStatus.Failed => TestResultStatus.Failed,
532
+ TestStatus.Skipped => TestResultStatus.Skipped,
533
+ _ => TestResultStatus.Skipped
534
+ };
535
+ }
536
+ }
537
+ }