diff --git a/Assets/Tests/Editor/JsonRpcAcceptedRequestCancellationPolicyTests.cs b/Assets/Tests/Editor/JsonRpcAcceptedRequestCancellationPolicyTests.cs new file mode 100644 index 0000000000..34819437c1 --- /dev/null +++ b/Assets/Tests/Editor/JsonRpcAcceptedRequestCancellationPolicyTests.cs @@ -0,0 +1,57 @@ +using NUnit.Framework; + +using io.github.hatayama.UnityCliLoop.Domain; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor +{ + /// + /// Tests accepted-request cancellation decisions before infrastructure parses transport details. + /// + public sealed class JsonRpcAcceptedRequestCancellationPolicyTests + { + [Test] + public void ShouldCancelOnClientDisconnect_WhenMethodIsNotCompile_ReturnsTrue() + { + // Verifies non-compile accepted work is tied to the CLI connection lifetime. + bool shouldCancel = JsonRpcAcceptedRequestCancellationPolicy.ShouldCancelOnClientDisconnect( + UnityCliLoopConstants.TOOL_NAME_GET_LOGS, + true); + + Assert.That(shouldCancel, Is.True); + } + + [Test] + public void ShouldCancelOnClientDisconnect_WhenCompileWaitsForDomainReload_ReturnsFalse() + { + // Verifies long compile requests can persist after the CLI connection closes. + bool shouldCancel = JsonRpcAcceptedRequestCancellationPolicy.ShouldCancelOnClientDisconnect( + UnityCliLoopConstants.TOOL_NAME_COMPILE, + true); + + Assert.That(shouldCancel, Is.False); + } + + [Test] + public void ShouldCancelOnClientDisconnect_WhenCompileDoesNotWaitForDomainReload_ReturnsTrue() + { + // Verifies fire-and-forget compile requests keep the usual disconnect cancellation behavior. + bool shouldCancel = JsonRpcAcceptedRequestCancellationPolicy.ShouldCancelOnClientDisconnect( + UnityCliLoopConstants.TOOL_NAME_COMPILE, + false); + + Assert.That(shouldCancel, Is.True); + } + + [Test] + public void ShouldCancelOnClientDisconnect_WhenCompileWaitPreferenceIsUnknown_ReturnsFalse() + { + // Verifies missing, null, or non-boolean transport values preserve the compile default wait contract. + bool shouldCancel = JsonRpcAcceptedRequestCancellationPolicy.ShouldCancelOnClientDisconnect( + UnityCliLoopConstants.TOOL_NAME_COMPILE, + null); + + Assert.That(shouldCancel, Is.False); + } + } +} diff --git a/Assets/Tests/Editor/JsonRpcAcceptedRequestCancellationPolicyTests.cs.meta b/Assets/Tests/Editor/JsonRpcAcceptedRequestCancellationPolicyTests.cs.meta new file mode 100644 index 0000000000..8f6ec7f1e2 --- /dev/null +++ b/Assets/Tests/Editor/JsonRpcAcceptedRequestCancellationPolicyTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7416db6e9cbd47a4b9b0c866195774e4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs b/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs index 136e155370..da87601c07 100644 --- a/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs +++ b/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs @@ -356,6 +356,46 @@ public async Task ProcessRequest_WhenCompileOmitsReloadWait_KeepsAcceptedRequest } } + [Test] + public async Task ProcessRequest_WhenCompileReloadWaitIsNotBoolean_KeepsAcceptedRequestAliveAfterDisconnect() + { + // Verifies malformed compile reload-wait params preserve the default wait contract. + UnityCliLoopToolRegistrarService previousService = ApplicationRegistrar.Service; + IToolSettingsPort toolSettingsPort = new ToolSettingsRepository(); + UnityCliLoopToolRegistrarService service = new( + new EmptyInternalToolNameProvider(), + toolSettingsPort, + new UnityCliLoopToolExecutionService(new NoOpEditorRuntimeStatePort()), + UnityCliLoopToolDiscovery.DiscoverTools); + ApplicationRegistrar.RegisterService(service); + service.RegisterCustomTool(new CompileDispatchPolicyTestTool()); + + bool cancelOnClientDisconnect = true; + try + { + string response = await JsonRpcProcessor.ProcessRequestWithEarlyResponseAsync( + BuildToolRequestWithParams( + UnityCliLoopConstants.TOOL_NAME_COMPILE, + "{\"WaitForDomainReload\":\"false\"}", + 1), + CancellationToken.None, + (_, shouldCancelOnClientDisconnect, _) => + { + cancelOnClientDisconnect = shouldCancelOnClientDisconnect; + return Task.CompletedTask; + }); + JObject parsed = JObject.Parse(response); + + Assert.That(parsed["error"], Is.Null); + Assert.That(parsed["result"], Is.Not.Null); + Assert.That(cancelOnClientDisconnect, Is.False); + } + finally + { + ApplicationRegistrar.RegisterService(previousService); + } + } + [Test] public async Task ProcessRequest_WhenCompileDoesNotWaitForDomainReload_CancelsOnClientDisconnect() { diff --git a/Packages/src/Editor/Domain/JsonRpcAcceptedRequestCancellationPolicy.cs b/Packages/src/Editor/Domain/JsonRpcAcceptedRequestCancellationPolicy.cs new file mode 100644 index 0000000000..d18ddf3e81 --- /dev/null +++ b/Packages/src/Editor/Domain/JsonRpcAcceptedRequestCancellationPolicy.cs @@ -0,0 +1,27 @@ +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Domain +{ + /// + /// Decides whether accepted JSON-RPC work should be canceled when the CLI disconnects. + /// + public static class JsonRpcAcceptedRequestCancellationPolicy + { + public static bool ShouldCancelOnClientDisconnect( + string methodName, + bool? compileWaitsForDomainReload) + { + if (methodName != UnityCliLoopConstants.TOOL_NAME_COMPILE) + { + return true; + } + + return !CompileRequestWaitsForDomainReload(compileWaitsForDomainReload); + } + + private static bool CompileRequestWaitsForDomainReload(bool? compileWaitsForDomainReload) + { + return compileWaitsForDomainReload ?? true; + } + } +} diff --git a/Packages/src/Editor/Domain/JsonRpcAcceptedRequestCancellationPolicy.cs.meta b/Packages/src/Editor/Domain/JsonRpcAcceptedRequestCancellationPolicy.cs.meta new file mode 100644 index 0000000000..9a6d35c270 --- /dev/null +++ b/Packages/src/Editor/Domain/JsonRpcAcceptedRequestCancellationPolicy.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4a6bb389836e49ddb1652ffe0cf17e65 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs b/Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs index e768fb6937..e907b81a55 100644 --- a/Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs +++ b/Packages/src/Editor/Infrastructure/Api/JsonRpcProcessor.cs @@ -283,31 +283,29 @@ private static bool ShouldCancelAcceptedRequestOnClientDisconnect(JsonRpcRequest { System.Diagnostics.Debug.Assert(request != null, "request must not be null"); - if (request.Method != UnityCliLoopConstants.TOOL_NAME_COMPILE) - { - return true; - } - - return !CompileRequestWaitsForDomainReload(request.Params); + bool? compileWaitsForDomainReload = ReadCompileRequestWaitsForDomainReload(request.Params); + return JsonRpcAcceptedRequestCancellationPolicy.ShouldCancelOnClientDisconnect( + request.Method, + compileWaitsForDomainReload); } - private static bool CompileRequestWaitsForDomainReload(JToken paramsToken) + private static bool? ReadCompileRequestWaitsForDomainReload(JToken paramsToken) { if (paramsToken is not JObject paramsObject) { - return true; + return null; } JToken waitForDomainReloadToken = paramsObject.GetValue(WaitForDomainReloadParamName, StringComparison.OrdinalIgnoreCase); if (waitForDomainReloadToken == null) { - return true; + return null; } if (waitForDomainReloadToken.Type != JTokenType.Boolean) { - return true; + return null; } return waitForDomainReloadToken.Value();