Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions Assets/Tests/Editor/DomainReloadDetectionServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,9 @@ public void CompleteDomainReload_WhenLegacyReloadStateExists_MigratesRecoveryFla
}

[Test]
public void CompleteDomainReload_WhenLegacyStateOnlySaysRunning_IgnoresStaleRunningFlag()
public void CompleteDomainReload_WhenLegacyStateOnlySaysRunning_DoesNotRestoreRunningSession()
{
// Verifies that stale running-only JSON does not opt into recovery after the migration.
// Verifies that stale running-only JSON is not restored into SessionState.
UnityCliLoopEditorLegacySessionState legacySessionState = new(
isServerRunning: true,
isAfterCompile: false,
Expand All @@ -131,7 +131,7 @@ public void CompleteDomainReload_WhenLegacyStateOnlySaysRunning_IgnoresStaleRunn

Assert.That(_sessionStateService.GetIsServerRunning(), Is.False);
ServerReadinessState state = _stateStore.Read();
Assert.That(state.Phase, Is.EqualTo("stopped"));
Assert.That(state.Phase, Is.EqualTo("recovering"));
}

[Test]
Expand Down Expand Up @@ -162,7 +162,7 @@ public void CompleteDomainReload_WhenLegacyReloadStateWasMigrated_DoesNotReapply
Assert.That(_sessionStateService.GetIsAfterCompile(), Is.False);
Assert.That(_sessionStateService.GetIsReconnecting(), Is.False);
ServerReadinessState state = _stateStore.Read();
Assert.That(state.Phase, Is.EqualTo("stopped"));
Assert.That(state.Phase, Is.EqualTo("recovering"));
}

private static ServerReadinessStateStore CreateTestStateStore()
Expand Down
70 changes: 67 additions & 3 deletions Assets/Tests/Editor/DomainReloadRecoveryUseCaseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,27 @@ public void ExecuteBeforeDomainReload_ShouldPreferInstanceState_WhenInstanceIsRu
}

[Test]
public void CompleteDomainReload_WhenServerWasNotRunning_ShouldPublishStoppedState()
public void CompleteDomainReload_WhenServerWasNotRunning_ShouldPublishRecoveringState()
{
// Verifies that a domain reload with no server to recover does not leave CLI waiters in recovering state.
// Verifies that no-server domain reload completion matches the automatic recovery that follows it.
_sessionStateService.SetIsServerRunning(false);
_domainReloadDetectionService.StartDomainReload("test-correlation", serverIsRunning: false);

_domainReloadDetectionService.CompleteDomainReload("test-correlation");

ServerReadinessState state = _stateStore.Read();
Assert.That(state.Phase, Is.EqualTo("recovering"));
}

[Test]
public void CompleteDomainReload_WhenServerWasManuallyStopped_ShouldPublishStoppedState()
{
// Verifies that explicit Stop Server remains terminal across Domain Reload completion.
_sessionStateService.MarkServerManuallyStopped();
_domainReloadDetectionService.StartDomainReload("test-correlation", serverIsRunning: false);

_domainReloadDetectionService.CompleteDomainReload("test-correlation");

ServerReadinessState state = _stateStore.Read();
Assert.That(state.Phase, Is.EqualTo("stopped"));
}
Expand All @@ -130,6 +143,41 @@ public async Task RestoreServerStateIfNeededAsync_WhenRecoveryDoesNotStartServer
Is.EqualTo("Unity CLI Loop server recovery finished, but no running server instance is available."));
}

[Test]
public async Task RestoreServerStateIfNeededAsync_WhenNoServerWasRunning_ShouldStillStartRecovery()
{
// Verifies launch-time reload recovery starts the server even when no previous bridge session existed.
_sessionStateService.SetIsServerRunning(false);
_sessionStateService.SetIsAfterCompile(false);
TestRecoveryCoordinator recoveryCoordinator = new(recoverServer: true);
SessionRecoveryService service = new(
recoveryCoordinator,
_domainReloadDetectionService,
_sessionStateService);

ValidationResult result = await service.RestoreServerStateIfNeededAsync(CancellationToken.None);

Assert.That(result.IsValid, Is.True);
Assert.That(recoveryCoordinator.StartRecoveryCallCount, Is.EqualTo(1));
}

[Test]
public async Task RestoreServerStateIfNeededAsync_WhenServerWasManuallyStopped_ShouldSkipRecovery()
{
// Verifies explicit Stop Server is preserved across Domain Reload.
_sessionStateService.MarkServerManuallyStopped();
TestRecoveryCoordinator recoveryCoordinator = new(recoverServer: true);
SessionRecoveryService service = new(
recoveryCoordinator,
_domainReloadDetectionService,
_sessionStateService);

ValidationResult result = await service.RestoreServerStateIfNeededAsync(CancellationToken.None);

Assert.That(result.IsValid, Is.True);
Assert.That(recoveryCoordinator.StartRecoveryCallCount, Is.EqualTo(0));
}

private static ServerReadinessStateStore CreateTestStateStore()
{
string projectRoot = System.IO.Path.Combine(
Expand Down Expand Up @@ -160,10 +208,26 @@ private static DomainReloadRecoveryUseCase CreateUseCase(
/// </summary>
private sealed class TestRecoveryCoordinator : IUnityCliLoopServerRecoveryCoordinator
{
public IUnityCliLoopServerInstance CurrentServer => null;
private readonly bool _recoverServer;
private readonly TestServerInstance _server = new();

public TestRecoveryCoordinator(bool recoverServer = false)
{
_recoverServer = recoverServer;
}

public int StartRecoveryCallCount { get; private set; }

public IUnityCliLoopServerInstance CurrentServer => _server.IsRunning ? _server : null;

public Task StartRecoveryIfNeededAsync(bool isAfterCompile, CancellationToken cancellationToken)
{
StartRecoveryCallCount++;
if (_recoverServer)
{
_server.StartServer();
}

return Task.CompletedTask;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public void GetFlags_WhenSessionStateIsEmpty_ReturnsFalseDefaults()
{
// Verifies that transient runtime flags do not opt into stale recovery by default.
Assert.That(_sessionStateService.GetIsServerRunning(), Is.False);
Assert.That(_sessionStateService.GetIsServerManuallyStopped(), Is.False);
Assert.That(_sessionStateService.GetIsAfterCompile(), Is.False);
Assert.That(_sessionStateService.GetIsDomainReloadInProgress(), Is.False);
Assert.That(_sessionStateService.GetIsReconnecting(), Is.False);
Expand All @@ -50,6 +51,7 @@ public void GetFlags_WhenServiceAndRepositoryAreRecreated_ReadsExistingSessionVa
UnityCliLoopEditorSessionStateTestFactory.CreateService();

Assert.That(recreatedService.GetIsServerRunning(), Is.True);
Assert.That(recreatedService.GetIsServerManuallyStopped(), Is.False);
Assert.That(recreatedService.GetIsAfterCompile(), Is.True);
Assert.That(recreatedService.GetIsDomainReloadInProgress(), Is.True);
Assert.That(recreatedService.GetIsReconnecting(), Is.True);
Expand Down Expand Up @@ -89,6 +91,7 @@ public void ClearAll_WhenFlagsAreSet_ClearsEveryTransientFlag()
// Verifies that test and shutdown cleanup can reset all runtime SessionState flags together.
_sessionStateService.MarkDomainReloadStarted(serverIsRunning: true);
_sessionStateService.SetShouldAutoScanThirdPartyToolMigration(true);
_sessionStateService.SetIsServerManuallyStopped(true);

_sessionStateService.ClearAll();

Expand All @@ -99,6 +102,20 @@ public void ClearAll_WhenFlagsAreSet_ClearsEveryTransientFlag()
Assert.That(_sessionStateService.GetShowReconnectingUI(), Is.False);
Assert.That(_sessionStateService.GetShowPostCompileReconnectingUI(), Is.False);
Assert.That(_sessionStateService.GetShouldAutoScanThirdPartyToolMigration(), Is.False);
Assert.That(_sessionStateService.GetIsServerManuallyStopped(), Is.False);
}

[Test]
public void MarkServerManuallyStopped_WhenServiceIsRecreated_PreservesManualStop()
{
// Verifies that explicit Stop Server survives Domain Reload service recreation.
_sessionStateService.MarkServerManuallyStopped();

UnityCliLoopEditorSessionStateService recreatedService =
UnityCliLoopEditorSessionStateTestFactory.CreateService();

Assert.That(recreatedService.GetIsServerRunning(), Is.False);
Assert.That(recreatedService.GetIsServerManuallyStopped(), Is.True);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ internal static UnityCliLoopEditorSessionStateSnapshot CaptureSnapshot(
internal readonly struct UnityCliLoopEditorSessionStateSnapshot
{
private readonly bool _isServerRunning;
private readonly bool _isServerManuallyStopped;
private readonly bool _isAfterCompile;
private readonly bool _isDomainReloadInProgress;
private readonly bool _isReconnecting;
Expand All @@ -36,6 +37,7 @@ internal readonly struct UnityCliLoopEditorSessionStateSnapshot
private UnityCliLoopEditorSessionStateSnapshot(UnityCliLoopEditorSessionStateService service)
{
_isServerRunning = service.GetIsServerRunning();
_isServerManuallyStopped = service.GetIsServerManuallyStopped();
_isAfterCompile = service.GetIsAfterCompile();
_isDomainReloadInProgress = service.GetIsDomainReloadInProgress();
_isReconnecting = service.GetIsReconnecting();
Expand All @@ -53,6 +55,7 @@ internal static UnityCliLoopEditorSessionStateSnapshot Capture(
internal void Restore(UnityCliLoopEditorSessionStateService service)
{
service.SetIsServerRunning(_isServerRunning);
service.SetIsServerManuallyStopped(_isServerManuallyStopped);
service.SetIsAfterCompile(_isAfterCompile);
service.SetIsDomainReloadInProgress(_isDomainReloadInProgress);
service.SetIsReconnecting(_isReconnecting);
Expand Down
121 changes: 121 additions & 0 deletions Assets/Tests/Editor/UnityCliLoopServerControllerStartupLockTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,103 @@ public async Task ProbeReadinessWithTimeoutAsync_WhenProbeDoesNotComplete_Throws
Assert.That(readinessProbe.CallCount, Is.EqualTo(1));
}

[Test]
public async Task RestoreServerStateIfNeeded_WhenServerWasManuallyStopped_ShouldSkipStartupRecovery()
{
// Tests that explicit Stop Server is preserved when startup recovery runs after Domain Reload.
_sessionStateService.MarkServerManuallyStopped();
TestServerInstanceFactory serverInstanceFactory = new();
UnityCliLoopServerLifecycleRegistryService lifecycleRegistry =
new UnityCliLoopServerLifecycleRegistryService();
ServerReadinessStateStore stateStore = CreateTestStateStore();
UnityCliLoopServerControllerService service = new(
serverInstanceFactory,
lifecycleRegistry,
new DomainReloadDetectionFileService(_sessionStateService, stateStore),
_sessionStateService,
stateStore,
new TestReadinessProbe(),
new TestDomainReloadLifecycle());

await service.RestoreServerStateIfNeeded();

Assert.That(serverInstanceFactory.LastCreated, Is.Null);
}

[Test]
public async Task StopServerWithUseCaseAsync_WhenStoppedByUser_ShouldMarkManualStop()
{
// Tests that the manual Stop Server path records explicit user stop intent.
UnityCliLoopServerControllerService service = CreateControllerService();
TestServerInstance runningServer = new();
runningServer.StartServer();
service.RegisterRecoveredServer(runningServer);

await service.StopServerWithUseCaseAsync();

Assert.That(_sessionStateService.GetIsServerRunning(), Is.False);
Assert.That(_sessionStateService.GetIsServerManuallyStopped(), Is.True);
}

[Test]
public async Task StartServerWithUseCaseAsync_WhenRestartCleanupStartFails_ShouldNotMarkManualStop()
{
// Tests that internal restart cleanup is not mistaken for explicit user stop intent.
UnityEngine.TestTools.LogAssert.Expect(
UnityEngine.LogType.Error,
"Server startup failed: Failed to start server: start failed");
TestServerInstanceFactory serverInstanceFactory = new(throwOnCreate: true);
UnityCliLoopServerLifecycleRegistryService lifecycleRegistry =
new UnityCliLoopServerLifecycleRegistryService();
ServerReadinessStateStore stateStore = CreateTestStateStore();
UnityCliLoopServerControllerService service = new(
serverInstanceFactory,
lifecycleRegistry,
new DomainReloadDetectionFileService(_sessionStateService, stateStore),
_sessionStateService,
stateStore,
new TestReadinessProbe(),
new TestDomainReloadLifecycle());
TestServerInstance runningServer = new();
runningServer.StartServer();
service.RegisterRecoveredServer(runningServer);

await service.StartServerWithUseCaseAsync();

Assert.That(_sessionStateService.GetIsServerRunning(), Is.False);
Assert.That(_sessionStateService.GetIsServerManuallyStopped(), Is.False);
}

[Test]
public async Task StartServerWithUseCaseAsync_WhenRestartCleanupStopFails_ShouldNotStartNewServer()
{
// Tests that restart cleanup failure does not get hidden behind a second startup attempt.
UnityEngine.TestTools.LogAssert.Expect(
UnityEngine.LogType.Error,
"Server shutdown failed: Failed to stop server: dispose failed");
TestServerInstanceFactory serverInstanceFactory = new();
UnityCliLoopServerLifecycleRegistryService lifecycleRegistry =
new UnityCliLoopServerLifecycleRegistryService();
ServerReadinessStateStore stateStore = CreateTestStateStore();
UnityCliLoopServerControllerService service = new(
serverInstanceFactory,
lifecycleRegistry,
new DomainReloadDetectionFileService(_sessionStateService, stateStore),
_sessionStateService,
stateStore,
new TestReadinessProbe(),
new TestDomainReloadLifecycle());
TestServerInstance runningServer = new(throwOnDispose: true);
runningServer.StartServer();
service.RegisterRecoveredServer(runningServer);

await service.StartServerWithUseCaseAsync();

Assert.That(serverInstanceFactory.LastCreated, Is.Null);
Assert.That(_sessionStateService.GetIsServerRunning(), Is.True);
Assert.That(_sessionStateService.GetIsServerManuallyStopped(), Is.False);
}

private UnityCliLoopServerControllerService CreateControllerService()
{
return CreateControllerService(new TestReadinessProbe());
Expand Down Expand Up @@ -223,10 +320,22 @@ public void PrepareForDomainReload()
/// </summary>
private sealed class TestServerInstanceFactory : IUnityCliLoopServerInstanceFactory
{
private readonly bool _throwOnCreate;

public TestServerInstanceFactory(bool throwOnCreate = false)
{
_throwOnCreate = throwOnCreate;
}

public TestServerInstance LastCreated { get; private set; }

public IUnityCliLoopServerInstance Create()
{
if (_throwOnCreate)
{
throw new System.InvalidOperationException("start failed");
}

LastCreated = new TestServerInstance();
return LastCreated;
}
Expand All @@ -237,6 +346,13 @@ public IUnityCliLoopServerInstance Create()
/// </summary>
private sealed class TestServerInstance : IUnityCliLoopServerInstance
{
private readonly bool _throwOnDispose;

public TestServerInstance(bool throwOnDispose = false)
{
_throwOnDispose = throwOnDispose;
}

public bool IsRunning { get; private set; }

public string Endpoint => "test";
Expand All @@ -253,6 +369,11 @@ public void StopServer()

public void Dispose()
{
if (_throwOnDispose)
{
throw new System.InvalidOperationException("dispose failed");
}

IsRunning = false;
}
}
Expand Down
Binary file modified Packages/src/Cli~/dist/darwin-amd64/uloop
Binary file not shown.
Binary file modified Packages/src/Cli~/dist/darwin-arm64/uloop
Binary file not shown.
Binary file modified Packages/src/Cli~/dist/windows-amd64/uloop.exe
Binary file not shown.
Loading
Loading