fix(launcher): Campaign LA LA3 narrow review fixes

This commit is contained in:
Erik 2026-08-14 17:06:47 +02:00
parent 26feba8186
commit 347a1a5d16
9 changed files with 495 additions and 145 deletions

View file

@ -1,3 +1,4 @@
using System.Collections.Concurrent;
using System.Threading;
using AcDream.Launcher.Core.Launching;
@ -235,6 +236,95 @@ public sealed class LauncherProcessSupervisorTests
states);
}
[Fact]
public async Task ConcurrentRunningAndExitedPublicationsRemainMonotonicAndInOrder()
{
var factory = new FakeChildProcessFactory(exitsWithinStopTimeout: true);
using var supervisor = new LauncherProcessSupervisor(factory);
using var runningPublicationEntered = new ManualResetEventSlim(false);
using var releaseRunningPublication = new ManualResetEventSlim(false);
var states = new ConcurrentQueue<LauncherSessionState>();
supervisor.StateChanged += (_, state) =>
{
if (state == LauncherSessionState.Running)
{
runningPublicationEntered.Set();
Assert.True(
releaseRunningPublication.Wait(TimeSpan.FromSeconds(5)),
"test did not release the Running publication barrier");
}
states.Enqueue(state);
};
Task startTask = Task.Run(() => supervisor.Start(Spec(), "pw"));
try
{
Assert.True(
runningPublicationEntered.Wait(TimeSpan.FromSeconds(5)),
"Running publication did not reach the test barrier");
// Commit Exited while Running's observer is deliberately
// paused. Storage reaches the terminal state immediately, but
// publication must queue behind the earlier Running event.
factory.LastCreated!.ExitForTest(17);
Assert.Equal(LauncherSessionState.Exited, supervisor.State);
}
finally
{
releaseRunningPublication.Set();
}
await startTask.WaitAsync(TimeSpan.FromSeconds(5));
Assert.Equal(
[
LauncherSessionState.Starting,
LauncherSessionState.Running,
LauncherSessionState.Exited,
],
states);
Assert.Equal(17, supervisor.ExitCode);
}
[Fact]
public void StateChangedPublicationAllowsCrossThreadReadsAndReentrantExit()
{
var factory = new FakeChildProcessFactory(exitsWithinStopTimeout: true);
using var supervisor = new LauncherProcessSupervisor(factory);
var states = new List<LauncherSessionState>();
supervisor.StateChanged += (_, state) =>
{
states.Add(state);
if (state != LauncherSessionState.Running)
{
return;
}
// A publisher that invokes callbacks while holding the state
// gate deadlocks this cross-thread read. The callback also
// raises Exited re-entrantly; it must queue after Running rather
// than recurse out of order or deadlock.
Task<LauncherSessionState> readTask = Task.Run(() => supervisor.State);
Assert.True(readTask.Wait(TimeSpan.FromSeconds(5)));
Assert.Equal(LauncherSessionState.Running, readTask.Result);
factory.LastCreated!.ExitForTest(23);
};
supervisor.Start(Spec(), "pw");
Assert.Equal(
[
LauncherSessionState.Starting,
LauncherSessionState.Running,
LauncherSessionState.Exited,
],
states);
Assert.Equal(LauncherSessionState.Exited, supervisor.State);
Assert.Equal(23, supervisor.ExitCode);
}
[Fact]
public void LauncherProcessSpecCarriesNoCredentialLikeMember()
{
@ -358,12 +448,22 @@ public sealed class LauncherProcessSupervisorTests
{
// Simulates a child that dies synchronously from inside
// Process.Start() itself (review finding F9's race).
HasExited = true;
ExitCode = 0;
Exited?.Invoke(this, EventArgs.Empty);
ExitForTest(0);
}
}
public void ExitForTest(int exitCode)
{
if (HasExited)
{
return;
}
HasExited = true;
ExitCode = exitCode;
Exited?.Invoke(this, EventArgs.Empty);
}
public bool TryRequestGracefulStop()
{
TryRequestGracefulStopCallCount++;
@ -382,9 +482,7 @@ public sealed class LauncherProcessSupervisorTests
{
KillCallCount++;
CallOrder.Add("kill");
HasExited = true;
ExitCode = -1;
Exited?.Invoke(this, EventArgs.Empty);
ExitForTest(-1);
}
public bool WaitForExit(TimeSpan timeout)
@ -392,9 +490,7 @@ public sealed class LauncherProcessSupervisorTests
if (!exitsWithinStopTimeout)
return false;
HasExited = true;
ExitCode = 0;
Exited?.Invoke(this, EventArgs.Empty);
ExitForTest(0);
return true;
}