@atelierai/uco 1.0.7 → 1.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/package.json +1 -1
  3. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetConfig.cs +132 -132
  4. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetDependencyResolver.cs +237 -237
  5. package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetDownloader.cs +82 -82
  6. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Resource/MenuItems.cs +247 -247
  7. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Resource/ToolGroups.cs +153 -153
  8. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/SystemTool/Skills.Create.SkillBody.cs +155 -155
  9. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Console.GetLogs.cs +95 -95
  10. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Docs.HttpFetcher.cs +139 -139
  11. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Instance.ListAll.cs +72 -72
  12. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/PathReadHelper.cs +106 -106
  13. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Script.Validate.cs +252 -252
  14. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Tests/TestResultCollector.cs +537 -537
  15. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Tests.cs +400 -400
  16. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Branding/ProductInfo.cs +62 -62
  17. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/CopilotServerManager.cs +1034 -1034
  18. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Skills/Skill_InitialSetup.cs +197 -197
  19. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.Editor.cs +201 -201
  20. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.cs +67 -67
  21. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/CopilotListWindowBase.cs +393 -393
  22. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/CopilotWindowBase.cs +113 -113
  23. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.Connection.cs +3 -3
  24. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotFeatures.cs +195 -195
  25. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotServer.cs +20 -20
  26. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CreateGUI.cs +354 -354
  27. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.cs +78 -78
  28. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowInitializer.cs +47 -47
  29. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/SerializationCheckWindow.cs +190 -190
  30. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Build.cs +91 -91
  31. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Config.cs +298 -298
  32. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Static.cs +296 -296
  33. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.cs +69 -69
  34. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/PackageUtils.cs +168 -168
  35. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/PrefsKeyMigration.cs +119 -119
  36. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/SafeDefaultsGuard.cs +180 -180
  37. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ScriptUtils.cs +229 -229
  38. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ToolsManifestGenerator.cs +200 -200
  39. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityAuthoringAdapters.cs +1467 -1467
  40. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityAuthoringSafetyReloadGuard.cs +65 -65
  41. package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityCopilotUpdateProjectSettings.cs +66 -66
  42. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/ReflectorNet.dll +0 -0
  43. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.Common.dll +0 -0
  44. package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.dll +0 -0
  45. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Converter/Json/Structures/EntityIdConverter.cs +120 -120
  46. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Data/GameObjectMetadata.cs +110 -110
  47. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Data/GameObjectMetadata.pre-Unity.6.5.cs +110 -110
  48. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Unity/Logs/FileLogStorage.cs +547 -547
  49. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.Build.cs +243 -243
  50. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.Config.cs +3 -3
  51. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.cs +295 -295
  52. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginBuilder.cs +105 -105
  53. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginRuntime.Static.cs +155 -155
  54. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginRuntime.cs +59 -59
  55. package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Utils/EnvironmentUtils.cs +11 -11
  56. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AtomicApi/AtomicApiTestFixtures.cs +99 -99
  57. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AtomicApi/TryModifyAtTests.cs +227 -227
  58. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AuthoringSafetyPilotTests.cs +1050 -1050
  59. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetLegacyMigrationTests.cs +304 -304
  60. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Environment/EnvironmentUtilsTests.cs +1 -1
  61. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/BlacklistTypeTests.cs +416 -416
  62. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.cs +172 -172
  63. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.pre-Unity.6.5.cs +169 -169
  64. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPopulationTests.cs +182 -182
  65. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPopulationTests.pre-Unity.6.5.cs +179 -179
  66. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.cs +175 -175
  67. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.pre-Unity.6.5.cs +174 -174
  68. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.cs +171 -171
  69. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.pre-Unity.6.5.cs +170 -170
  70. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsCreateFolderTests.cs +243 -243
  71. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsMaterialTests.cs +101 -101
  72. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsPrefabCreateTests.cs +348 -348
  73. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsPrefabCreateTests.pre-Unity.6.5.cs +348 -348
  74. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsShaderTests.cs +194 -194
  75. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/PathBasedToolTests.cs +573 -573
  76. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/PathBasedToolTests.pre-Unity.6.5.cs +513 -513
  77. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotCameraTests.cs +154 -154
  78. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotCameraTests.pre-Unity.6.5.cs +154 -154
  79. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotGameViewTests.cs +164 -164
  80. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotSceneViewTests.cs +155 -155
  81. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolParameterTests.cs +56 -56
  82. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolThreadSafetyTests.cs +651 -651
  83. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolThreadSafetyTests.pre-Unity.6.5.cs +651 -651
  84. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ToolBatchExecutionIntegrationTests.cs +407 -407
  85. package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/{McpPluginTests.cs → UcoPluginTests.cs} +105 -105
  86. package/vendor/plugin/com.atelierai.unity.copilot/package.json +1 -1
  87. /package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/{McpPluginTests.cs.meta → UcoPluginTests.cs.meta} +0 -0
@@ -1,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
+ }