refactor(runtime): unify generation reset for direct hosts

Move canonical per-session teardown into one retryable Runtime transaction, reduce App reset to projection acknowledgements, and prove the same GameRuntime graph through deterministic no-window lifecycle, gameplay, portal, fault, reconnect, and isolation gates.\n\nCo-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-27 00:43:26 +02:00
parent 7818494116
commit a9a822f206
28 changed files with 2707 additions and 345 deletions

View file

@ -157,6 +157,7 @@ public sealed class LiveSessionControllerTests
public int ActivateCount { get; private set; }
public List<TestCommandBus> CommandBuses { get; } = [];
public List<LiveSessionCharacterSelection> Selections { get; } = [];
public List<RuntimeGenerationToken> ResetGenerations { get; } = [];
public LiveSessionBinding BindSession(WorldSession session)
{
@ -200,10 +201,12 @@ public sealed class LiveSessionControllerTests
});
}
public void ResetSessionState()
public void ResetSessionState(
RuntimeGenerationToken retiringGeneration)
{
calls.Add("reset");
ResetCount++;
ResetGenerations.Add(retiringGeneration);
OnReset?.Invoke();
if (FailResetOnce)
{
@ -449,6 +452,34 @@ public sealed class LiveSessionControllerTests
Assert.Same(operations.Sessions[1], controller.CurrentSession);
}
[Fact]
public void ResetCallbacksCarryTheExactRetiringScopeGeneration()
{
var calls = new List<string>();
var operations = new TestOperations(calls);
var host = new TestHost(calls);
var controller = new LiveSessionController(operations);
LiveSessionStartResult first =
controller.Start(LiveOptions(), host);
RuntimeGenerationToken firstGeneration = controller.Generation;
LiveSessionStartResult second =
controller.Reconnect(LiveOptions(), host);
RuntimeGenerationToken secondGeneration = controller.Generation;
controller.Stop();
Assert.Equal(LiveSessionStartStatus.Connected, first.Status);
Assert.Equal(LiveSessionStartStatus.Connected, second.Status);
Assert.Equal(
[
RuntimeGenerationToken.Initial,
firstGeneration,
secondGeneration,
],
host.ResetGenerations);
Assert.NotEqual(firstGeneration, secondGeneration);
}
[Theory]
[InlineData("detach", "stop")]
[InlineData("detach", "dispose")]

View file

@ -221,7 +221,7 @@ public sealed class LiveSessionHostTests
Func<WorldSession, ILiveSessionCommandRouting> createCommands) =>
new(controller, new LiveSessionHostBindings(
Routing: new(createEvents, createCommands),
Reset: () => calls.Add("reset"),
Reset: _ => calls.Add("reset"),
Selection: new(
id => calls.Add($"player:{id}"),
id => calls.Add($"vitals:{id}"),

View file

@ -15,7 +15,7 @@ public sealed class LiveSessionLifecycleHostTests
var host = CreateHost(calls);
LiveSessionBinding binding = host.BindSession(sessionA);
host.ResetSessionState();
host.ResetSessionState(RuntimeGenerationToken.Initial);
host.ReportConnecting("host", 9000, "user");
host.ReportConnected();
var selection = new LiveSessionCharacterSelection(2, 3u, "toon", "account");
@ -67,7 +67,7 @@ public sealed class LiveSessionLifecycleHostTests
Func<WorldSession, LiveSessionBinding>? bind = null) =>
new(new LiveSessionLifecycleBindings(
Bind: bind ?? (session => CreateBinding(session, calls)),
Reset: () => calls.Add("reset"),
Reset: _ => calls.Add("reset"),
Connecting: (host, port, user) =>
calls.Add($"connecting:{host}:{port}:{user}"),
Connected: () => calls.Add("connected"),

View file

@ -19,7 +19,7 @@ public sealed class RuntimeLiveSessionNoWindowTests
new LiveSessionRoutingFactories(
_ => new EventRoute(calls),
_ => new CommandRoute(calls)),
() => calls.Add("reset"),
_ => calls.Add("reset"),
new LiveSessionSelectionBindings(
id => calls.Add($"player:{id}"),
_ => { },