@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.
- package/CHANGELOG.md +29 -8
- package/package.json +2 -2
- package/vendor/plugin/com.atelierai.unity.copilot/CHANGELOG.md +4 -4
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetConfig.cs +132 -132
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetDependencyResolver.cs +237 -237
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/DependencyResolver/NuGetDownloader.cs +82 -82
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Resource/MenuItems.cs +247 -247
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Resource/ToolGroups.cs +153 -153
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/SystemTool/Skills.Create.SkillBody.cs +155 -155
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Console.GetLogs.cs +95 -95
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Docs.HttpFetcher.cs +139 -139
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Instance.ListAll.cs +72 -72
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/PathReadHelper.cs +106 -106
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Script.Validate.cs +252 -252
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Tests/TestResultCollector.cs +537 -537
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/Tests.cs +400 -400
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/API/Tool/ToolGroups.SetEnabled.cs +92 -92
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Branding/ProductInfo.cs +62 -62
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/CopilotServerManager.cs +887 -1034
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Skills/Skill_InitialSetup.cs +197 -197
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.Editor.cs +201 -201
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Startup.cs +67 -67
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/CopilotListWindowBase.cs +393 -393
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/CopilotWindowBase.cs +113 -113
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.Connection.cs +3 -3
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotFeatures.cs +195 -195
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CopilotServer.cs +22 -22
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.CreateGUI.cs +354 -354
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowEditor.cs +78 -78
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/MainWindowInitializer.cs +47 -47
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UI/Window/SerializationCheckWindow.cs +190 -190
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Build.cs +91 -91
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Config.cs +298 -298
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.Static.cs +296 -296
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/UnityCopilotPluginEditor.cs +69 -69
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/PackageUtils.cs +168 -168
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/PrefsKeyMigration.cs +119 -119
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/SafeDefaultsGuard.cs +180 -180
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ScriptUtils.cs +229 -229
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ToolGroupRegistry.cs +461 -460
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/ToolsManifestGenerator.cs +200 -200
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityAuthoringAdapters.cs +1467 -1467
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityAuthoringSafetyReloadGuard.cs +65 -65
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/Scripts/Utils/UnityCopilotUpdateProjectSettings.cs +66 -66
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotPromptsWindow.uss +11 -11
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotResourcesWindow.uss +16 -16
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/CopilotToolsWindow.uss +36 -36
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/MainWindow.uss +183 -183
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/{_mcp-list-window.uss → _copilot-list-window.uss} +18 -18
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/_list-view.uss +77 -77
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotPromptsWindow.uxml +18 -18
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotResourcesWindow.uxml +18 -18
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/CopilotToolsWindow.uxml +18 -18
- package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uxml/MainWindow.uxml +115 -115
- package/vendor/plugin/com.atelierai.unity.copilot/Plugins/ReflectorNet.dll +0 -0
- package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.Common.dll +0 -0
- package/vendor/plugin/com.atelierai.unity.copilot/Plugins/Uco.Framework.dll +0 -0
- package/vendor/plugin/com.atelierai.unity.copilot/README.md +88 -88
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Converter/Json/Structures/EntityIdConverter.cs +120 -120
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Data/GameObjectMetadata.cs +110 -110
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Data/GameObjectMetadata.pre-Unity.6.5.cs +110 -110
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Unity/Logs/FileLogStorage.cs +547 -547
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.Build.cs +243 -243
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.Config.cs +3 -3
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPlugin.cs +295 -295
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginBuilder.cs +105 -105
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginRuntime.Static.cs +155 -155
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/UnityCopilotPluginRuntime.cs +59 -59
- package/vendor/plugin/com.atelierai.unity.copilot/Runtime/Utils/EnvironmentUtils.cs +11 -11
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AtomicApi/AtomicApiTestFixtures.cs +99 -99
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AtomicApi/TryModifyAtTests.cs +227 -227
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/AuthoringSafetyPilotTests.cs +1050 -1050
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetExtractorDevelopmentDependencyTests.cs +148 -148
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetExtractorFlatLayoutTests.cs +166 -166
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetInstallManifestTests.cs +436 -436
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetLegacyMigrationTests.cs +304 -304
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/DependencyResolver/NuGetPackageInstallerStaleVersionCleanupTests.cs +200 -200
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Environment/EnvironmentUtilsTests.cs +2 -2
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/BlacklistTypeTests.cs +416 -416
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.cs +172 -172
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataFieldScriptableObjectPopulationTests.pre-Unity.6.5.cs +169 -169
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPopulationTests.cs +182 -182
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPopulationTests.pre-Unity.6.5.cs +179 -179
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.cs +175 -175
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyPopulationTests.pre-Unity.6.5.cs +174 -174
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.cs +171 -171
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ReflectionConverter/DataPropertyScriptableObjectPopulationTests.pre-Unity.6.5.cs +170 -170
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsCreateFolderTests.cs +243 -243
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsMaterialTests.cs +101 -101
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsPrefabCreateTests.cs +348 -348
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsPrefabCreateTests.pre-Unity.6.5.cs +348 -348
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Assets/AssetsShaderTests.cs +194 -194
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/GameObject/TestJsonSchema.cs +199 -199
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/PathBasedToolTests.cs +573 -573
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/PathBasedToolTests.pre-Unity.6.5.cs +513 -513
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotCameraTests.cs +154 -154
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotCameraTests.pre-Unity.6.5.cs +154 -154
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotGameViewTests.cs +164 -164
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/Screenshot/ScreenshotSceneViewTests.cs +155 -155
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolParameterTests.cs +56 -56
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolThreadSafetyTests.cs +651 -651
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/Tool/ToolThreadSafetyTests.pre-Unity.6.5.cs +651 -651
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ToolBatchExecutionIntegrationTests.cs +407 -407
- package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/{McpPluginTests.cs → UcoPluginTests.cs} +105 -105
- package/vendor/plugin/com.atelierai.unity.copilot/package.json +1 -1
- /package/vendor/plugin/com.atelierai.unity.copilot/Editor/UI/uss/common/{_mcp-list-window.uss.meta → _copilot-list-window.uss.meta} +0 -0
- /package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/{McpPluginTests.cs.meta → UcoPluginTests.cs.meta} +0 -0
package/vendor/plugin/com.atelierai.unity.copilot/Tests/Editor/ToolBatchExecutionIntegrationTests.cs
CHANGED
|
@@ -1,407 +1,407 @@
|
|
|
1
|
-
#nullable enable
|
|
2
|
-
|
|
3
|
-
using System;
|
|
4
|
-
using System.Collections;
|
|
5
|
-
using System.Collections.Generic;
|
|
6
|
-
using System.Reflection;
|
|
7
|
-
using System.Text.Json;
|
|
8
|
-
using System.Text.Json.Nodes;
|
|
9
|
-
using System.Threading;
|
|
10
|
-
using System.Threading.Tasks;
|
|
11
|
-
using com.AtelierAI.Unity.Copilot.Editor.API;
|
|
12
|
-
using com.AtelierAI.Uco.Framework;
|
|
13
|
-
using com.AtelierAI.Uco.Framework.Common.Model;
|
|
14
|
-
using com.IvanMurzak.ReflectorNet;
|
|
15
|
-
using Microsoft.Extensions.Logging;
|
|
16
|
-
using NUnit.Framework;
|
|
17
|
-
using UnityEngine.TestTools;
|
|
18
|
-
|
|
19
|
-
namespace com.AtelierAI.Unity.Copilot.Editor.Tests
|
|
20
|
-
{
|
|
21
|
-
[TestFixture]
|
|
22
|
-
public sealed class ToolBatchExecutionIntegrationTests : BaseTest
|
|
23
|
-
{
|
|
24
|
-
private static readonly BindingFlags PrivateStatic =
|
|
25
|
-
BindingFlags.NonPublic | BindingFlags.Static;
|
|
26
|
-
|
|
27
|
-
[UnityTest]
|
|
28
|
-
public IEnumerator Execute_UsesProductionDispatchWithChildContextsAndFailFast()
|
|
29
|
-
{
|
|
30
|
-
var originalInstance = UnityCopilotPluginEditor.Instance;
|
|
31
|
-
originalInstance.
|
|
32
|
-
var originalPlugin = originalInstance.UcoPluginInstance;
|
|
33
|
-
// Console separation (COCli-07): the replacement singleton has no
|
|
34
|
-
// collector of its own; the still-installed diagnostics channel
|
|
35
|
-
// sink belongs to the original instance's collector.
|
|
36
|
-
var diagnostics = originalInstance.LogCollector;
|
|
37
|
-
Assert.IsNotNull(diagnostics,
|
|
38
|
-
"The plugin log collector must be installed to assert console-separated diagnostics.");
|
|
39
|
-
var middleware = new RecordingMiddleware();
|
|
40
|
-
var successfulRunner = new BatchTestRunner("batch-test-success", shouldFail: false, targetBound: false);
|
|
41
|
-
var failingRunner = new BatchTestRunner("batch-test-failure", shouldFail: true, targetBound: true);
|
|
42
|
-
var replacement = new TestUnityCopilotPluginEditor(
|
|
43
|
-
middleware,
|
|
44
|
-
successfulRunner,
|
|
45
|
-
failingRunner);
|
|
46
|
-
|
|
47
|
-
SetEditorSingleton(replacement);
|
|
48
|
-
try
|
|
49
|
-
{
|
|
50
|
-
replacement.
|
|
51
|
-
var manager = replacement.Tools;
|
|
52
|
-
Assert.IsNotNull(manager, "The test plugin should expose a production tool manager.");
|
|
53
|
-
|
|
54
|
-
// g-005: batch-execute carries DestructiveHint=true, so the
|
|
55
|
-
// parent call must obtain a read-only plan and confirm it.
|
|
56
|
-
// The plan short-circuits inside the outermost safety
|
|
57
|
-
// middleware and therefore never reaches the recording
|
|
58
|
-
// middleware or any child runner.
|
|
59
|
-
var planning = manager!.RunCallTool(BuildBatchRequest(planControl: true, confirmation: null));
|
|
60
|
-
yield return WaitForTask(planning);
|
|
61
|
-
var planResponse = planning.Result;
|
|
62
|
-
Assert.AreEqual(ResponseStatus.Success, planResponse.Status, planResponse.Message);
|
|
63
|
-
var plan = planResponse.Value!.StructuredContent!["result"]!["confirmationPlan"]!.AsObject();
|
|
64
|
-
var childRecords = plan["childRecords"]!.AsArray();
|
|
65
|
-
Assert.AreEqual(3, childRecords.Count);
|
|
66
|
-
Assert.AreEqual(0, middleware.Contexts.Count, "A plan must not reach later middleware.");
|
|
67
|
-
Assert.AreEqual(0, successfulRunner.InvocationCount + failingRunner.InvocationCount,
|
|
68
|
-
"A plan must not invoke a child runner.");
|
|
69
|
-
|
|
70
|
-
var request = BuildBatchRequest(planControl: false, confirmation: new ToolCallConfirmation
|
|
71
|
-
{
|
|
72
|
-
PlanId = plan["planId"]!.GetValue<string>(),
|
|
73
|
-
PlanHash = plan["planHash"]!.GetValue<string>(),
|
|
74
|
-
ExpiresAtUnixMs = plan["expiresAtUnixMs"]!.GetValue<long>(),
|
|
75
|
-
});
|
|
76
|
-
var execution = manager!.RunCallTool(request);
|
|
77
|
-
yield return WaitForTask(execution);
|
|
78
|
-
|
|
79
|
-
var response = execution.Result;
|
|
80
|
-
Assert.AreEqual(ResponseStatus.Success, response.Status, response.Message);
|
|
81
|
-
Assert.IsNotNull(response.Value, "The production batch tool should return a tool response.");
|
|
82
|
-
Assert.AreEqual(ResponseStatus.Success, response.Value!.Status);
|
|
83
|
-
|
|
84
|
-
var batchResult = DeserializeBatchResult(response.Value);
|
|
85
|
-
Assert.AreEqual(3, batchResult.TotalCommands);
|
|
86
|
-
Assert.AreEqual(1, batchResult.Succeeded);
|
|
87
|
-
Assert.AreEqual(1, batchResult.Failed);
|
|
88
|
-
Assert.IsTrue(batchResult.Aborted);
|
|
89
|
-
Assert.AreEqual(3, batchResult.Results.Length);
|
|
90
|
-
Assert.IsTrue(batchResult.Results[0].Ok);
|
|
91
|
-
Assert.IsNotNull(batchResult.Results[0].Transaction,
|
|
92
|
-
"A completed child's transaction report must survive a later runtime failure.");
|
|
93
|
-
Assert.IsTrue(batchResult.Results[0].Transaction!["completed"]!.GetValue<bool>());
|
|
94
|
-
Assert.AreEqual("none", batchResult.Results[0].Transaction!["undo"]!.GetValue<string>());
|
|
95
|
-
Assert.IsFalse(batchResult.Results[1].Ok);
|
|
96
|
-
Assert.IsFalse(batchResult.Results[1].Skipped);
|
|
97
|
-
Assert.IsTrue(batchResult.Results[2].Skipped);
|
|
98
|
-
Assert.IsFalse(batchResult.Results[2].Ok);
|
|
99
|
-
|
|
100
|
-
// The first middleware invocation is the production batch
|
|
101
|
-
// call. The next two are the child calls dispatched by the
|
|
102
|
-
// production Tool_Batch.Execute implementation.
|
|
103
|
-
Assert.AreEqual(3, middleware.Contexts.Count);
|
|
104
|
-
var parent = middleware.Contexts[0];
|
|
105
|
-
Assert.AreEqual("batch-parent", parent.CallId);
|
|
106
|
-
Assert.AreEqual("batch-trace", parent.CorrelationId);
|
|
107
|
-
Assert.IsNull(parent.ParentCallId);
|
|
108
|
-
|
|
109
|
-
var firstChild = middleware.Contexts[1];
|
|
110
|
-
var secondChild = middleware.Contexts[2];
|
|
111
|
-
Assert.AreEqual("batch-trace", firstChild.CorrelationId);
|
|
112
|
-
Assert.AreEqual("batch-trace", secondChild.CorrelationId);
|
|
113
|
-
Assert.AreEqual("batch-parent", firstChild.ParentCallId);
|
|
114
|
-
Assert.AreEqual("batch-parent", secondChild.ParentCallId);
|
|
115
|
-
Assert.AreEqual(childRecords[0]!["requestID"]!.GetValue<string>(), firstChild.RequestID);
|
|
116
|
-
Assert.AreEqual(childRecords[0]!["callId"]!.GetValue<string>(), firstChild.CallId);
|
|
117
|
-
Assert.AreEqual(childRecords[1]!["requestID"]!.GetValue<string>(), secondChild.RequestID);
|
|
118
|
-
Assert.AreEqual(childRecords[1]!["callId"]!.GetValue<string>(), secondChild.CallId);
|
|
119
|
-
Assert.AreEqual(true, firstChild.Confirm);
|
|
120
|
-
Assert.AreEqual(true, secondChild.Confirm);
|
|
121
|
-
Assert.AreEqual(childRecords[0]!["planId"]!.GetValue<string>(), firstChild.Confirmation!.PlanId);
|
|
122
|
-
Assert.AreEqual(childRecords[1]!["planId"]!.GetValue<string>(), secondChild.Confirmation!.PlanId,
|
|
123
|
-
"Execution must redeem the aggregate-issued token rather than mint a replacement.");
|
|
124
|
-
Assert.AreEqual(childRecords[1]!["planHash"]!.GetValue<string>(), secondChild.Confirmation.PlanHash);
|
|
125
|
-
Assert.AreEqual(childRecords[1]!["expiresAtUnixMs"]!.GetValue<long>(), secondChild.Confirmation.ExpiresAtUnixMs);
|
|
126
|
-
Assert.AreEqual(1, successfulRunner.InvocationCount);
|
|
127
|
-
Assert.AreEqual(1, failingRunner.InvocationCount);
|
|
128
|
-
|
|
129
|
-
// Console separation (COCli-07): the failing child's
|
|
130
|
-
// "Error Response to AI" log lives in the plugin diagnostics
|
|
131
|
-
// channel, not the Unity Console — assert it there.
|
|
132
|
-
AssertDiagnosticsContains(
|
|
133
|
-
UnityEngine.LogType.Error, "Intentional batch test failure", diagnostics);
|
|
134
|
-
}
|
|
135
|
-
finally
|
|
136
|
-
{
|
|
137
|
-
RestoreEditorSingleton(originalInstance, originalPlugin, replacement);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
[UnityTest]
|
|
142
|
-
public IEnumerator Execute_RejectsTamperedApprovedChildBeforeAnyRunner()
|
|
143
|
-
{
|
|
144
|
-
var originalInstance = UnityCopilotPluginEditor.Instance;
|
|
145
|
-
originalInstance.
|
|
146
|
-
var originalPlugin = originalInstance.UcoPluginInstance;
|
|
147
|
-
var middleware = new RecordingMiddleware { TamperApprovedChild = true };
|
|
148
|
-
var first = new BatchTestRunner("batch-test-success", shouldFail: false, targetBound: false);
|
|
149
|
-
var second = new BatchTestRunner("batch-test-failure", shouldFail: true, targetBound: true);
|
|
150
|
-
var replacement = new TestUnityCopilotPluginEditor(middleware, first, second);
|
|
151
|
-
|
|
152
|
-
SetEditorSingleton(replacement);
|
|
153
|
-
try
|
|
154
|
-
{
|
|
155
|
-
replacement.
|
|
156
|
-
var manager = replacement.Tools;
|
|
157
|
-
Assert.IsNotNull(manager);
|
|
158
|
-
|
|
159
|
-
var planning = manager!.RunCallTool(BuildBatchRequest(planControl: true, confirmation: null));
|
|
160
|
-
yield return WaitForTask(planning);
|
|
161
|
-
var planResponse = planning.Result;
|
|
162
|
-
Assert.AreEqual(ResponseStatus.Success, planResponse.Status, planResponse.Message);
|
|
163
|
-
var plan = planResponse.Value!.StructuredContent!["result"]!["confirmationPlan"]!.AsObject();
|
|
164
|
-
|
|
165
|
-
var execution = manager.RunCallTool(BuildBatchRequest(
|
|
166
|
-
planControl: false,
|
|
167
|
-
confirmation: new ToolCallConfirmation
|
|
168
|
-
{
|
|
169
|
-
PlanId = plan["planId"]!.GetValue<string>(),
|
|
170
|
-
PlanHash = plan["planHash"]!.GetValue<string>(),
|
|
171
|
-
ExpiresAtUnixMs = plan["expiresAtUnixMs"]!.GetValue<long>(),
|
|
172
|
-
}));
|
|
173
|
-
yield return WaitForTask(execution);
|
|
174
|
-
|
|
175
|
-
var response = execution.Result;
|
|
176
|
-
Assert.AreEqual(ResponseStatus.Success, response.Status, response.Message);
|
|
177
|
-
var batchResult = DeserializeBatchResult(response.Value!);
|
|
178
|
-
Assert.IsTrue(batchResult.Aborted);
|
|
179
|
-
Assert.AreEqual(0, batchResult.Succeeded);
|
|
180
|
-
Assert.AreEqual(0, first.InvocationCount + second.InvocationCount,
|
|
181
|
-
"No child runner may start after an approved child binding changes.");
|
|
182
|
-
Assert.AreEqual(ToolCallErrorCodes.ConfirmationStale, batchResult.Results[0].ErrorCode);
|
|
183
|
-
}
|
|
184
|
-
finally
|
|
185
|
-
{
|
|
186
|
-
RestoreEditorSingleton(originalInstance, originalPlugin, replacement);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
private static RequestCallTool BuildBatchRequest(bool planControl, ToolCallConfirmation? confirmation)
|
|
191
|
-
{
|
|
192
|
-
var commands = new[]
|
|
193
|
-
{
|
|
194
|
-
new Tool_Batch.BatchCommand
|
|
195
|
-
{
|
|
196
|
-
Tool = "batch-test-success",
|
|
197
|
-
Params = new Dictionary<string, JsonElement>()
|
|
198
|
-
},
|
|
199
|
-
new Tool_Batch.BatchCommand
|
|
200
|
-
{
|
|
201
|
-
Tool = "batch-test-failure",
|
|
202
|
-
Params = new Dictionary<string, JsonElement>()
|
|
203
|
-
},
|
|
204
|
-
new Tool_Batch.BatchCommand
|
|
205
|
-
{
|
|
206
|
-
Tool = "batch-test-success",
|
|
207
|
-
Params = new Dictionary<string, JsonElement>()
|
|
208
|
-
},
|
|
209
|
-
};
|
|
210
|
-
var arguments = new Dictionary<string, JsonElement>
|
|
211
|
-
{
|
|
212
|
-
["commands"] = JsonSerializer.SerializeToElement(commands),
|
|
213
|
-
["failFast"] = JsonSerializer.SerializeToElement(true),
|
|
214
|
-
};
|
|
215
|
-
// The plan and the execution share requestID, callId, and
|
|
216
|
-
// correlationId: the confirmation binding covers all of them.
|
|
217
|
-
return new RequestCallTool(
|
|
218
|
-
"batch-request",
|
|
219
|
-
Tool_Batch.BatchExecuteToolId,
|
|
220
|
-
arguments,
|
|
221
|
-
new ToolCallControl
|
|
222
|
-
{
|
|
223
|
-
Version = ToolCallControl.CurrentVersion,
|
|
224
|
-
CallId = "batch-parent",
|
|
225
|
-
CorrelationId = "batch-trace",
|
|
226
|
-
DryRun = planControl ? "plan" : null,
|
|
227
|
-
Confirm = confirmation == null ? (bool?)null : true,
|
|
228
|
-
Confirmation = confirmation,
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
private static Tool_Batch.BatchResult DeserializeBatchResult(ResponseCallTool response)
|
|
233
|
-
{
|
|
234
|
-
Assert.IsNotNull(response.StructuredContent, "The batch response should contain structured content.");
|
|
235
|
-
var resultNode = response.StructuredContent!["result"];
|
|
236
|
-
Assert.IsNotNull(resultNode, "The structured response should contain a result member.");
|
|
237
|
-
return JsonSerializer.Deserialize<Tool_Batch.BatchResult>(resultNode!.ToJsonString())
|
|
238
|
-
?? throw new AssertionException("The batch result could not be deserialized.");
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
private static void SetEditorSingleton(UnityCopilotPluginEditor replacement)
|
|
242
|
-
{
|
|
243
|
-
var instanceField = typeof(UnityCopilotPluginEditor).GetField("instance", PrivateStatic);
|
|
244
|
-
Assert.IsNotNull(instanceField, "Editor singleton field was not found.");
|
|
245
|
-
instanceField!.SetValue(null, replacement);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
private static void RestoreEditorSingleton(
|
|
249
|
-
UnityCopilotPluginEditor original,
|
|
250
|
-
IUcoPlugin? originalPlugin,
|
|
251
|
-
TestUnityCopilotPluginEditor replacement)
|
|
252
|
-
{
|
|
253
|
-
replacement.
|
|
254
|
-
replacement.Dispose();
|
|
255
|
-
|
|
256
|
-
var instanceField = typeof(UnityCopilotPluginEditor).GetField("instance", PrivateStatic);
|
|
257
|
-
Assert.IsNotNull(instanceField, "Editor singleton field was not found.");
|
|
258
|
-
instanceField!.SetValue(null, original);
|
|
259
|
-
|
|
260
|
-
var setCurrentPlugin = typeof(UnityCopilotPluginEditor).GetMethod(
|
|
261
|
-
"SetCurrentPlugin",
|
|
262
|
-
PrivateStatic);
|
|
263
|
-
Assert.IsNotNull(setCurrentPlugin, "Editor current-plugin setter was not found.");
|
|
264
|
-
setCurrentPlugin!.Invoke(null, new object?[] { originalPlugin });
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
private sealed class TestUnityCopilotPluginEditor : UnityCopilotPluginEditor
|
|
268
|
-
{
|
|
269
|
-
private readonly RecordingMiddleware _middleware;
|
|
270
|
-
private readonly IReadOnlyList<IRunTool> _runners;
|
|
271
|
-
|
|
272
|
-
public TestUnityCopilotPluginEditor(
|
|
273
|
-
RecordingMiddleware middleware,
|
|
274
|
-
params IRunTool[] runners)
|
|
275
|
-
{
|
|
276
|
-
_middleware = middleware;
|
|
277
|
-
_runners = runners;
|
|
278
|
-
ConnectionConfigForTests.KeepConnected = false;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
protected override IUcoPlugin
|
|
282
|
-
com.AtelierAI.Uco.Framework.Common.Version version,
|
|
283
|
-
Reflector reflector,
|
|
284
|
-
ILoggerProvider? loggerProvider = null,
|
|
285
|
-
Action<
|
|
286
|
-
{
|
|
287
|
-
return base.
|
|
288
|
-
version,
|
|
289
|
-
reflector,
|
|
290
|
-
loggerProvider,
|
|
291
|
-
builder =>
|
|
292
|
-
{
|
|
293
|
-
configure?.Invoke(builder);
|
|
294
|
-
builder.AddToolExecutionMiddleware(_middleware);
|
|
295
|
-
foreach (var runner in _runners)
|
|
296
|
-
builder.AddTool(runner.Name, runner);
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
private sealed class RecordingMiddleware : IToolExecutionMiddleware
|
|
302
|
-
{
|
|
303
|
-
public List<ToolCallContext> Contexts { get; } = new();
|
|
304
|
-
public bool TamperApprovedChild { get; set; }
|
|
305
|
-
|
|
306
|
-
public async Task<ResponseCallTool> InvokeAsync(
|
|
307
|
-
ToolCallContext context,
|
|
308
|
-
ToolCallNext next)
|
|
309
|
-
{
|
|
310
|
-
Contexts.Add(context.Clone());
|
|
311
|
-
var invocation = ToolCallInvocationScope.CurrentInvocation;
|
|
312
|
-
if (TamperApprovedChild
|
|
313
|
-
&& string.Equals(invocation?.Name, Tool_Batch.BatchExecuteToolId, StringComparison.Ordinal)
|
|
314
|
-
&& invocation.ApprovedPlan?.ChildRecords is AuthoringChildPlanSummary[] children
|
|
315
|
-
&& children.Length > 0)
|
|
316
|
-
{
|
|
317
|
-
children[0].ArgumentsHash = "sha256-tampered";
|
|
318
|
-
TamperApprovedChild = false;
|
|
319
|
-
}
|
|
320
|
-
return await next(context).ConfigureAwait(false);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
private sealed class BatchTestRunner : IRunTool
|
|
325
|
-
{
|
|
326
|
-
private readonly bool _shouldFail;
|
|
327
|
-
|
|
328
|
-
public BatchTestRunner(string name, bool shouldFail, bool targetBound)
|
|
329
|
-
{
|
|
330
|
-
Name = name;
|
|
331
|
-
_shouldFail = shouldFail;
|
|
332
|
-
AuthoringCapability = targetBound
|
|
333
|
-
? new AuthoringCapabilityDescriptor
|
|
334
|
-
{
|
|
335
|
-
MutationKind = AuthoringMutationKind.Delete,
|
|
336
|
-
UndoLevel = AuthoringUndoLevel.None,
|
|
337
|
-
SupportsValidation = true,
|
|
338
|
-
SupportsPlanning = true,
|
|
339
|
-
Validator = new BatchInspector(),
|
|
340
|
-
Planner = new BatchInspector(),
|
|
341
|
-
}
|
|
342
|
-
: null;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
public string Name { get; }
|
|
346
|
-
public bool Enabled { get; set; } = true;
|
|
347
|
-
public string? Title => Name;
|
|
348
|
-
public string? Description => "Test-only batch runner.";
|
|
349
|
-
public MethodInfo? Method => null;
|
|
350
|
-
public string? SkillDescription => null;
|
|
351
|
-
public string? SkillBody => null;
|
|
352
|
-
public JsonNode? InputSchema => new JsonObject();
|
|
353
|
-
public JsonNode? OutputSchema => null;
|
|
354
|
-
public UcoToolType ToolType => UcoToolType.Standard;
|
|
355
|
-
public bool? ReadOnlyHint => null;
|
|
356
|
-
public bool? DestructiveHint => AuthoringCapability == null ? (bool?)null : true;
|
|
357
|
-
public bool? IdempotentHint => null;
|
|
358
|
-
public bool? OpenWorldHint => null;
|
|
359
|
-
public AuthoringCapabilityDescriptor? AuthoringCapability { get; }
|
|
360
|
-
public int TokenCount => 0;
|
|
361
|
-
public int InvocationCount { get; private set; }
|
|
362
|
-
|
|
363
|
-
public Task<ResponseCallTool> Run(
|
|
364
|
-
string requestId,
|
|
365
|
-
IReadOnlyDictionary<string, JsonElement>? namedParameters,
|
|
366
|
-
CancellationToken cancellationToken = default)
|
|
367
|
-
{
|
|
368
|
-
_ = namedParameters;
|
|
369
|
-
_ = cancellationToken;
|
|
370
|
-
InvocationCount++;
|
|
371
|
-
if (_shouldFail)
|
|
372
|
-
{
|
|
373
|
-
return Task.FromResult(ResponseCallTool.Error(new ToolCallError(
|
|
374
|
-
ToolCallErrorCodes.ToolExecutionFailed,
|
|
375
|
-
"Intentional batch test failure.")).SetRequestID(requestId));
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
return Task.FromResult(ResponseCallTool.Success("ok").SetRequestID(requestId));
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
private sealed class BatchInspector : IAuthoringValidator, IAuthoringPlanner
|
|
383
|
-
{
|
|
384
|
-
public AuthoringValidationResult Validate(AuthoringInvocation invocation)
|
|
385
|
-
=> new AuthoringValidationResult
|
|
386
|
-
{
|
|
387
|
-
Valid = true,
|
|
388
|
-
Targets = new[]
|
|
389
|
-
{
|
|
390
|
-
new AuthoringTargetSummary
|
|
391
|
-
{
|
|
392
|
-
Kind = "batch-test",
|
|
393
|
-
Name = invocation.Name,
|
|
394
|
-
Fingerprint = AuthoringConfirmationBinding.ComputeArgumentsHash(invocation.Arguments),
|
|
395
|
-
},
|
|
396
|
-
},
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
public AuthoringPlanSummary Plan(AuthoringInvocation invocation)
|
|
400
|
-
=> new AuthoringPlanSummary
|
|
401
|
-
{
|
|
402
|
-
Targets = Validate(invocation).Targets,
|
|
403
|
-
PredictedEffects = new[] { "test failure" },
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
}
|
|
1
|
+
#nullable enable
|
|
2
|
+
|
|
3
|
+
using System;
|
|
4
|
+
using System.Collections;
|
|
5
|
+
using System.Collections.Generic;
|
|
6
|
+
using System.Reflection;
|
|
7
|
+
using System.Text.Json;
|
|
8
|
+
using System.Text.Json.Nodes;
|
|
9
|
+
using System.Threading;
|
|
10
|
+
using System.Threading.Tasks;
|
|
11
|
+
using com.AtelierAI.Unity.Copilot.Editor.API;
|
|
12
|
+
using com.AtelierAI.Uco.Framework;
|
|
13
|
+
using com.AtelierAI.Uco.Framework.Common.Model;
|
|
14
|
+
using com.IvanMurzak.ReflectorNet;
|
|
15
|
+
using Microsoft.Extensions.Logging;
|
|
16
|
+
using NUnit.Framework;
|
|
17
|
+
using UnityEngine.TestTools;
|
|
18
|
+
|
|
19
|
+
namespace com.AtelierAI.Unity.Copilot.Editor.Tests
|
|
20
|
+
{
|
|
21
|
+
[TestFixture]
|
|
22
|
+
public sealed class ToolBatchExecutionIntegrationTests : BaseTest
|
|
23
|
+
{
|
|
24
|
+
private static readonly BindingFlags PrivateStatic =
|
|
25
|
+
BindingFlags.NonPublic | BindingFlags.Static;
|
|
26
|
+
|
|
27
|
+
[UnityTest]
|
|
28
|
+
public IEnumerator Execute_UsesProductionDispatchWithChildContextsAndFailFast()
|
|
29
|
+
{
|
|
30
|
+
var originalInstance = UnityCopilotPluginEditor.Instance;
|
|
31
|
+
originalInstance.BuildUcoPluginIfNeeded();
|
|
32
|
+
var originalPlugin = originalInstance.UcoPluginInstance;
|
|
33
|
+
// Console separation (COCli-07): the replacement singleton has no
|
|
34
|
+
// collector of its own; the still-installed diagnostics channel
|
|
35
|
+
// sink belongs to the original instance's collector.
|
|
36
|
+
var diagnostics = originalInstance.LogCollector;
|
|
37
|
+
Assert.IsNotNull(diagnostics,
|
|
38
|
+
"The plugin log collector must be installed to assert console-separated diagnostics.");
|
|
39
|
+
var middleware = new RecordingMiddleware();
|
|
40
|
+
var successfulRunner = new BatchTestRunner("batch-test-success", shouldFail: false, targetBound: false);
|
|
41
|
+
var failingRunner = new BatchTestRunner("batch-test-failure", shouldFail: true, targetBound: true);
|
|
42
|
+
var replacement = new TestUnityCopilotPluginEditor(
|
|
43
|
+
middleware,
|
|
44
|
+
successfulRunner,
|
|
45
|
+
failingRunner);
|
|
46
|
+
|
|
47
|
+
SetEditorSingleton(replacement);
|
|
48
|
+
try
|
|
49
|
+
{
|
|
50
|
+
replacement.BuildUcoPluginIfNeeded();
|
|
51
|
+
var manager = replacement.Tools;
|
|
52
|
+
Assert.IsNotNull(manager, "The test plugin should expose a production tool manager.");
|
|
53
|
+
|
|
54
|
+
// g-005: batch-execute carries DestructiveHint=true, so the
|
|
55
|
+
// parent call must obtain a read-only plan and confirm it.
|
|
56
|
+
// The plan short-circuits inside the outermost safety
|
|
57
|
+
// middleware and therefore never reaches the recording
|
|
58
|
+
// middleware or any child runner.
|
|
59
|
+
var planning = manager!.RunCallTool(BuildBatchRequest(planControl: true, confirmation: null));
|
|
60
|
+
yield return WaitForTask(planning);
|
|
61
|
+
var planResponse = planning.Result;
|
|
62
|
+
Assert.AreEqual(ResponseStatus.Success, planResponse.Status, planResponse.Message);
|
|
63
|
+
var plan = planResponse.Value!.StructuredContent!["result"]!["confirmationPlan"]!.AsObject();
|
|
64
|
+
var childRecords = plan["childRecords"]!.AsArray();
|
|
65
|
+
Assert.AreEqual(3, childRecords.Count);
|
|
66
|
+
Assert.AreEqual(0, middleware.Contexts.Count, "A plan must not reach later middleware.");
|
|
67
|
+
Assert.AreEqual(0, successfulRunner.InvocationCount + failingRunner.InvocationCount,
|
|
68
|
+
"A plan must not invoke a child runner.");
|
|
69
|
+
|
|
70
|
+
var request = BuildBatchRequest(planControl: false, confirmation: new ToolCallConfirmation
|
|
71
|
+
{
|
|
72
|
+
PlanId = plan["planId"]!.GetValue<string>(),
|
|
73
|
+
PlanHash = plan["planHash"]!.GetValue<string>(),
|
|
74
|
+
ExpiresAtUnixMs = plan["expiresAtUnixMs"]!.GetValue<long>(),
|
|
75
|
+
});
|
|
76
|
+
var execution = manager!.RunCallTool(request);
|
|
77
|
+
yield return WaitForTask(execution);
|
|
78
|
+
|
|
79
|
+
var response = execution.Result;
|
|
80
|
+
Assert.AreEqual(ResponseStatus.Success, response.Status, response.Message);
|
|
81
|
+
Assert.IsNotNull(response.Value, "The production batch tool should return a tool response.");
|
|
82
|
+
Assert.AreEqual(ResponseStatus.Success, response.Value!.Status);
|
|
83
|
+
|
|
84
|
+
var batchResult = DeserializeBatchResult(response.Value);
|
|
85
|
+
Assert.AreEqual(3, batchResult.TotalCommands);
|
|
86
|
+
Assert.AreEqual(1, batchResult.Succeeded);
|
|
87
|
+
Assert.AreEqual(1, batchResult.Failed);
|
|
88
|
+
Assert.IsTrue(batchResult.Aborted);
|
|
89
|
+
Assert.AreEqual(3, batchResult.Results.Length);
|
|
90
|
+
Assert.IsTrue(batchResult.Results[0].Ok);
|
|
91
|
+
Assert.IsNotNull(batchResult.Results[0].Transaction,
|
|
92
|
+
"A completed child's transaction report must survive a later runtime failure.");
|
|
93
|
+
Assert.IsTrue(batchResult.Results[0].Transaction!["completed"]!.GetValue<bool>());
|
|
94
|
+
Assert.AreEqual("none", batchResult.Results[0].Transaction!["undo"]!.GetValue<string>());
|
|
95
|
+
Assert.IsFalse(batchResult.Results[1].Ok);
|
|
96
|
+
Assert.IsFalse(batchResult.Results[1].Skipped);
|
|
97
|
+
Assert.IsTrue(batchResult.Results[2].Skipped);
|
|
98
|
+
Assert.IsFalse(batchResult.Results[2].Ok);
|
|
99
|
+
|
|
100
|
+
// The first middleware invocation is the production batch
|
|
101
|
+
// call. The next two are the child calls dispatched by the
|
|
102
|
+
// production Tool_Batch.Execute implementation.
|
|
103
|
+
Assert.AreEqual(3, middleware.Contexts.Count);
|
|
104
|
+
var parent = middleware.Contexts[0];
|
|
105
|
+
Assert.AreEqual("batch-parent", parent.CallId);
|
|
106
|
+
Assert.AreEqual("batch-trace", parent.CorrelationId);
|
|
107
|
+
Assert.IsNull(parent.ParentCallId);
|
|
108
|
+
|
|
109
|
+
var firstChild = middleware.Contexts[1];
|
|
110
|
+
var secondChild = middleware.Contexts[2];
|
|
111
|
+
Assert.AreEqual("batch-trace", firstChild.CorrelationId);
|
|
112
|
+
Assert.AreEqual("batch-trace", secondChild.CorrelationId);
|
|
113
|
+
Assert.AreEqual("batch-parent", firstChild.ParentCallId);
|
|
114
|
+
Assert.AreEqual("batch-parent", secondChild.ParentCallId);
|
|
115
|
+
Assert.AreEqual(childRecords[0]!["requestID"]!.GetValue<string>(), firstChild.RequestID);
|
|
116
|
+
Assert.AreEqual(childRecords[0]!["callId"]!.GetValue<string>(), firstChild.CallId);
|
|
117
|
+
Assert.AreEqual(childRecords[1]!["requestID"]!.GetValue<string>(), secondChild.RequestID);
|
|
118
|
+
Assert.AreEqual(childRecords[1]!["callId"]!.GetValue<string>(), secondChild.CallId);
|
|
119
|
+
Assert.AreEqual(true, firstChild.Confirm);
|
|
120
|
+
Assert.AreEqual(true, secondChild.Confirm);
|
|
121
|
+
Assert.AreEqual(childRecords[0]!["planId"]!.GetValue<string>(), firstChild.Confirmation!.PlanId);
|
|
122
|
+
Assert.AreEqual(childRecords[1]!["planId"]!.GetValue<string>(), secondChild.Confirmation!.PlanId,
|
|
123
|
+
"Execution must redeem the aggregate-issued token rather than mint a replacement.");
|
|
124
|
+
Assert.AreEqual(childRecords[1]!["planHash"]!.GetValue<string>(), secondChild.Confirmation.PlanHash);
|
|
125
|
+
Assert.AreEqual(childRecords[1]!["expiresAtUnixMs"]!.GetValue<long>(), secondChild.Confirmation.ExpiresAtUnixMs);
|
|
126
|
+
Assert.AreEqual(1, successfulRunner.InvocationCount);
|
|
127
|
+
Assert.AreEqual(1, failingRunner.InvocationCount);
|
|
128
|
+
|
|
129
|
+
// Console separation (COCli-07): the failing child's
|
|
130
|
+
// "Error Response to AI" log lives in the plugin diagnostics
|
|
131
|
+
// channel, not the Unity Console — assert it there.
|
|
132
|
+
AssertDiagnosticsContains(
|
|
133
|
+
UnityEngine.LogType.Error, "Intentional batch test failure", diagnostics);
|
|
134
|
+
}
|
|
135
|
+
finally
|
|
136
|
+
{
|
|
137
|
+
RestoreEditorSingleton(originalInstance, originalPlugin, replacement);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
[UnityTest]
|
|
142
|
+
public IEnumerator Execute_RejectsTamperedApprovedChildBeforeAnyRunner()
|
|
143
|
+
{
|
|
144
|
+
var originalInstance = UnityCopilotPluginEditor.Instance;
|
|
145
|
+
originalInstance.BuildUcoPluginIfNeeded();
|
|
146
|
+
var originalPlugin = originalInstance.UcoPluginInstance;
|
|
147
|
+
var middleware = new RecordingMiddleware { TamperApprovedChild = true };
|
|
148
|
+
var first = new BatchTestRunner("batch-test-success", shouldFail: false, targetBound: false);
|
|
149
|
+
var second = new BatchTestRunner("batch-test-failure", shouldFail: true, targetBound: true);
|
|
150
|
+
var replacement = new TestUnityCopilotPluginEditor(middleware, first, second);
|
|
151
|
+
|
|
152
|
+
SetEditorSingleton(replacement);
|
|
153
|
+
try
|
|
154
|
+
{
|
|
155
|
+
replacement.BuildUcoPluginIfNeeded();
|
|
156
|
+
var manager = replacement.Tools;
|
|
157
|
+
Assert.IsNotNull(manager);
|
|
158
|
+
|
|
159
|
+
var planning = manager!.RunCallTool(BuildBatchRequest(planControl: true, confirmation: null));
|
|
160
|
+
yield return WaitForTask(planning);
|
|
161
|
+
var planResponse = planning.Result;
|
|
162
|
+
Assert.AreEqual(ResponseStatus.Success, planResponse.Status, planResponse.Message);
|
|
163
|
+
var plan = planResponse.Value!.StructuredContent!["result"]!["confirmationPlan"]!.AsObject();
|
|
164
|
+
|
|
165
|
+
var execution = manager.RunCallTool(BuildBatchRequest(
|
|
166
|
+
planControl: false,
|
|
167
|
+
confirmation: new ToolCallConfirmation
|
|
168
|
+
{
|
|
169
|
+
PlanId = plan["planId"]!.GetValue<string>(),
|
|
170
|
+
PlanHash = plan["planHash"]!.GetValue<string>(),
|
|
171
|
+
ExpiresAtUnixMs = plan["expiresAtUnixMs"]!.GetValue<long>(),
|
|
172
|
+
}));
|
|
173
|
+
yield return WaitForTask(execution);
|
|
174
|
+
|
|
175
|
+
var response = execution.Result;
|
|
176
|
+
Assert.AreEqual(ResponseStatus.Success, response.Status, response.Message);
|
|
177
|
+
var batchResult = DeserializeBatchResult(response.Value!);
|
|
178
|
+
Assert.IsTrue(batchResult.Aborted);
|
|
179
|
+
Assert.AreEqual(0, batchResult.Succeeded);
|
|
180
|
+
Assert.AreEqual(0, first.InvocationCount + second.InvocationCount,
|
|
181
|
+
"No child runner may start after an approved child binding changes.");
|
|
182
|
+
Assert.AreEqual(ToolCallErrorCodes.ConfirmationStale, batchResult.Results[0].ErrorCode);
|
|
183
|
+
}
|
|
184
|
+
finally
|
|
185
|
+
{
|
|
186
|
+
RestoreEditorSingleton(originalInstance, originalPlugin, replacement);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private static RequestCallTool BuildBatchRequest(bool planControl, ToolCallConfirmation? confirmation)
|
|
191
|
+
{
|
|
192
|
+
var commands = new[]
|
|
193
|
+
{
|
|
194
|
+
new Tool_Batch.BatchCommand
|
|
195
|
+
{
|
|
196
|
+
Tool = "batch-test-success",
|
|
197
|
+
Params = new Dictionary<string, JsonElement>()
|
|
198
|
+
},
|
|
199
|
+
new Tool_Batch.BatchCommand
|
|
200
|
+
{
|
|
201
|
+
Tool = "batch-test-failure",
|
|
202
|
+
Params = new Dictionary<string, JsonElement>()
|
|
203
|
+
},
|
|
204
|
+
new Tool_Batch.BatchCommand
|
|
205
|
+
{
|
|
206
|
+
Tool = "batch-test-success",
|
|
207
|
+
Params = new Dictionary<string, JsonElement>()
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
var arguments = new Dictionary<string, JsonElement>
|
|
211
|
+
{
|
|
212
|
+
["commands"] = JsonSerializer.SerializeToElement(commands),
|
|
213
|
+
["failFast"] = JsonSerializer.SerializeToElement(true),
|
|
214
|
+
};
|
|
215
|
+
// The plan and the execution share requestID, callId, and
|
|
216
|
+
// correlationId: the confirmation binding covers all of them.
|
|
217
|
+
return new RequestCallTool(
|
|
218
|
+
"batch-request",
|
|
219
|
+
Tool_Batch.BatchExecuteToolId,
|
|
220
|
+
arguments,
|
|
221
|
+
new ToolCallControl
|
|
222
|
+
{
|
|
223
|
+
Version = ToolCallControl.CurrentVersion,
|
|
224
|
+
CallId = "batch-parent",
|
|
225
|
+
CorrelationId = "batch-trace",
|
|
226
|
+
DryRun = planControl ? "plan" : null,
|
|
227
|
+
Confirm = confirmation == null ? (bool?)null : true,
|
|
228
|
+
Confirmation = confirmation,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private static Tool_Batch.BatchResult DeserializeBatchResult(ResponseCallTool response)
|
|
233
|
+
{
|
|
234
|
+
Assert.IsNotNull(response.StructuredContent, "The batch response should contain structured content.");
|
|
235
|
+
var resultNode = response.StructuredContent!["result"];
|
|
236
|
+
Assert.IsNotNull(resultNode, "The structured response should contain a result member.");
|
|
237
|
+
return JsonSerializer.Deserialize<Tool_Batch.BatchResult>(resultNode!.ToJsonString())
|
|
238
|
+
?? throw new AssertionException("The batch result could not be deserialized.");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private static void SetEditorSingleton(UnityCopilotPluginEditor replacement)
|
|
242
|
+
{
|
|
243
|
+
var instanceField = typeof(UnityCopilotPluginEditor).GetField("instance", PrivateStatic);
|
|
244
|
+
Assert.IsNotNull(instanceField, "Editor singleton field was not found.");
|
|
245
|
+
instanceField!.SetValue(null, replacement);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private static void RestoreEditorSingleton(
|
|
249
|
+
UnityCopilotPluginEditor original,
|
|
250
|
+
IUcoPlugin? originalPlugin,
|
|
251
|
+
TestUnityCopilotPluginEditor replacement)
|
|
252
|
+
{
|
|
253
|
+
replacement.DisposeUcoPluginInstance();
|
|
254
|
+
replacement.Dispose();
|
|
255
|
+
|
|
256
|
+
var instanceField = typeof(UnityCopilotPluginEditor).GetField("instance", PrivateStatic);
|
|
257
|
+
Assert.IsNotNull(instanceField, "Editor singleton field was not found.");
|
|
258
|
+
instanceField!.SetValue(null, original);
|
|
259
|
+
|
|
260
|
+
var setCurrentPlugin = typeof(UnityCopilotPluginEditor).GetMethod(
|
|
261
|
+
"SetCurrentPlugin",
|
|
262
|
+
PrivateStatic);
|
|
263
|
+
Assert.IsNotNull(setCurrentPlugin, "Editor current-plugin setter was not found.");
|
|
264
|
+
setCurrentPlugin!.Invoke(null, new object?[] { originalPlugin });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
private sealed class TestUnityCopilotPluginEditor : UnityCopilotPluginEditor
|
|
268
|
+
{
|
|
269
|
+
private readonly RecordingMiddleware _middleware;
|
|
270
|
+
private readonly IReadOnlyList<IRunTool> _runners;
|
|
271
|
+
|
|
272
|
+
public TestUnityCopilotPluginEditor(
|
|
273
|
+
RecordingMiddleware middleware,
|
|
274
|
+
params IRunTool[] runners)
|
|
275
|
+
{
|
|
276
|
+
_middleware = middleware;
|
|
277
|
+
_runners = runners;
|
|
278
|
+
ConnectionConfigForTests.KeepConnected = false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
protected override IUcoPlugin BuildUcoPlugin(
|
|
282
|
+
com.AtelierAI.Uco.Framework.Common.Version version,
|
|
283
|
+
Reflector reflector,
|
|
284
|
+
ILoggerProvider? loggerProvider = null,
|
|
285
|
+
Action<IUcoPluginBuilder>? configure = null)
|
|
286
|
+
{
|
|
287
|
+
return base.BuildUcoPlugin(
|
|
288
|
+
version,
|
|
289
|
+
reflector,
|
|
290
|
+
loggerProvider,
|
|
291
|
+
builder =>
|
|
292
|
+
{
|
|
293
|
+
configure?.Invoke(builder);
|
|
294
|
+
builder.AddToolExecutionMiddleware(_middleware);
|
|
295
|
+
foreach (var runner in _runners)
|
|
296
|
+
builder.AddTool(runner.Name, runner);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private sealed class RecordingMiddleware : IToolExecutionMiddleware
|
|
302
|
+
{
|
|
303
|
+
public List<ToolCallContext> Contexts { get; } = new();
|
|
304
|
+
public bool TamperApprovedChild { get; set; }
|
|
305
|
+
|
|
306
|
+
public async Task<ResponseCallTool> InvokeAsync(
|
|
307
|
+
ToolCallContext context,
|
|
308
|
+
ToolCallNext next)
|
|
309
|
+
{
|
|
310
|
+
Contexts.Add(context.Clone());
|
|
311
|
+
var invocation = ToolCallInvocationScope.CurrentInvocation;
|
|
312
|
+
if (TamperApprovedChild
|
|
313
|
+
&& string.Equals(invocation?.Name, Tool_Batch.BatchExecuteToolId, StringComparison.Ordinal)
|
|
314
|
+
&& invocation.ApprovedPlan?.ChildRecords is AuthoringChildPlanSummary[] children
|
|
315
|
+
&& children.Length > 0)
|
|
316
|
+
{
|
|
317
|
+
children[0].ArgumentsHash = "sha256-tampered";
|
|
318
|
+
TamperApprovedChild = false;
|
|
319
|
+
}
|
|
320
|
+
return await next(context).ConfigureAwait(false);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private sealed class BatchTestRunner : IRunTool
|
|
325
|
+
{
|
|
326
|
+
private readonly bool _shouldFail;
|
|
327
|
+
|
|
328
|
+
public BatchTestRunner(string name, bool shouldFail, bool targetBound)
|
|
329
|
+
{
|
|
330
|
+
Name = name;
|
|
331
|
+
_shouldFail = shouldFail;
|
|
332
|
+
AuthoringCapability = targetBound
|
|
333
|
+
? new AuthoringCapabilityDescriptor
|
|
334
|
+
{
|
|
335
|
+
MutationKind = AuthoringMutationKind.Delete,
|
|
336
|
+
UndoLevel = AuthoringUndoLevel.None,
|
|
337
|
+
SupportsValidation = true,
|
|
338
|
+
SupportsPlanning = true,
|
|
339
|
+
Validator = new BatchInspector(),
|
|
340
|
+
Planner = new BatchInspector(),
|
|
341
|
+
}
|
|
342
|
+
: null;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
public string Name { get; }
|
|
346
|
+
public bool Enabled { get; set; } = true;
|
|
347
|
+
public string? Title => Name;
|
|
348
|
+
public string? Description => "Test-only batch runner.";
|
|
349
|
+
public MethodInfo? Method => null;
|
|
350
|
+
public string? SkillDescription => null;
|
|
351
|
+
public string? SkillBody => null;
|
|
352
|
+
public JsonNode? InputSchema => new JsonObject();
|
|
353
|
+
public JsonNode? OutputSchema => null;
|
|
354
|
+
public UcoToolType ToolType => UcoToolType.Standard;
|
|
355
|
+
public bool? ReadOnlyHint => null;
|
|
356
|
+
public bool? DestructiveHint => AuthoringCapability == null ? (bool?)null : true;
|
|
357
|
+
public bool? IdempotentHint => null;
|
|
358
|
+
public bool? OpenWorldHint => null;
|
|
359
|
+
public AuthoringCapabilityDescriptor? AuthoringCapability { get; }
|
|
360
|
+
public int TokenCount => 0;
|
|
361
|
+
public int InvocationCount { get; private set; }
|
|
362
|
+
|
|
363
|
+
public Task<ResponseCallTool> Run(
|
|
364
|
+
string requestId,
|
|
365
|
+
IReadOnlyDictionary<string, JsonElement>? namedParameters,
|
|
366
|
+
CancellationToken cancellationToken = default)
|
|
367
|
+
{
|
|
368
|
+
_ = namedParameters;
|
|
369
|
+
_ = cancellationToken;
|
|
370
|
+
InvocationCount++;
|
|
371
|
+
if (_shouldFail)
|
|
372
|
+
{
|
|
373
|
+
return Task.FromResult(ResponseCallTool.Error(new ToolCallError(
|
|
374
|
+
ToolCallErrorCodes.ToolExecutionFailed,
|
|
375
|
+
"Intentional batch test failure.")).SetRequestID(requestId));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return Task.FromResult(ResponseCallTool.Success("ok").SetRequestID(requestId));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private sealed class BatchInspector : IAuthoringValidator, IAuthoringPlanner
|
|
383
|
+
{
|
|
384
|
+
public AuthoringValidationResult Validate(AuthoringInvocation invocation)
|
|
385
|
+
=> new AuthoringValidationResult
|
|
386
|
+
{
|
|
387
|
+
Valid = true,
|
|
388
|
+
Targets = new[]
|
|
389
|
+
{
|
|
390
|
+
new AuthoringTargetSummary
|
|
391
|
+
{
|
|
392
|
+
Kind = "batch-test",
|
|
393
|
+
Name = invocation.Name,
|
|
394
|
+
Fingerprint = AuthoringConfirmationBinding.ComputeArgumentsHash(invocation.Arguments),
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
public AuthoringPlanSummary Plan(AuthoringInvocation invocation)
|
|
400
|
+
=> new AuthoringPlanSummary
|
|
401
|
+
{
|
|
402
|
+
Targets = Validate(invocation).Targets,
|
|
403
|
+
PredictedEffects = new[] { "test failure" },
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|