merge: Campaign LA LA2 - probe and idle review-closed
# Conflicts: # docs/plans/2026-08-14-launcher-campaign.md # src/AcDream.Headless/Hosting/HeadlessSessionHost.cs
This commit is contained in:
commit
e01b2cd12f
16 changed files with 1361 additions and 69 deletions
|
|
@ -389,6 +389,96 @@ public sealed class LiveSessionControllerTests
|
|||
Assert.Equal(1, operations.DisposeCounts[operations.Sessions[0]]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign LA slice LA2: the probe short-circuit — connect, receive
|
||||
/// CharacterList, report the roster, then gracefully disconnect via the
|
||||
/// SAME StopCore teardown <see cref="Start_NoAvailableCharacterTearsDownExactScope"/>
|
||||
/// exercises, returning <see cref="LiveSessionStartStatus.ProbeComplete"/>
|
||||
/// instead of ever reaching TrySelectCharacter/ApplySelectedCharacter/
|
||||
/// EnterWorld. Asserted directly against the operations fake:
|
||||
/// <see cref="TestOperations.EnterWorldCount"/> stays zero and "enter:*"/
|
||||
/// "selected"/"activate"/"entered" never appear in the call trace.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Start_ProbeReportsRosterThenGracefullyDisconnectsWithoutSelectionOrEnterWorld()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var operations = new TestOperations(calls);
|
||||
var host = new TestHost(calls);
|
||||
var controller = new LiveSessionController(operations);
|
||||
|
||||
LiveSessionStartResult result = controller.Start(
|
||||
LiveOptions(probe: true),
|
||||
host);
|
||||
|
||||
Assert.Equal(LiveSessionStartStatus.ProbeComplete, result.Status);
|
||||
Assert.Null(result.Selection);
|
||||
Assert.Equal(
|
||||
[
|
||||
"reset", "resolve", "create", "bind", "report-connecting",
|
||||
"connect", "report-connected", "roster", "deactivate",
|
||||
"detach-events", "dispose-session", "detach-session", "reset",
|
||||
],
|
||||
calls);
|
||||
Assert.DoesNotContain("selected", calls);
|
||||
Assert.DoesNotContain("activate", calls);
|
||||
Assert.DoesNotContain("entered", calls);
|
||||
Assert.Equal(0, operations.EnterWorldCount);
|
||||
LiveSessionRosterReport roster = Assert.Single(host.Rosters);
|
||||
Assert.Equal("Canonical", roster.AccountName);
|
||||
Assert.False(controller.IsInWorld);
|
||||
Assert.Null(controller.CurrentSession);
|
||||
Assert.Equal(1, operations.DisposeCounts[operations.Sessions[0]]);
|
||||
|
||||
// The same 4-stage graceful teardown the NoCharacters path uses —
|
||||
// the probe's scope fully converges without requiring
|
||||
// controller.Dispose().
|
||||
LiveSessionOwnershipSnapshot ownership = controller.CaptureOwnership();
|
||||
Assert.Equal(RuntimeTeardownStage.Complete, ownership.LastTeardownStages);
|
||||
Assert.False(ownership.HasActiveSession);
|
||||
Assert.False(ownership.HasRetiredSession);
|
||||
Assert.False(ownership.HasPendingOperation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign LA LA2 review fix: ProbeComplete proves a real CharacterList
|
||||
/// was received and reported, not merely that the socket connected. A
|
||||
/// missing roster follows the existing NoCharacters non-success path and
|
||||
/// still drains the exact pre-world teardown transaction.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Start_ProbeWithoutCharacterListIsNonSuccessAndTearsDownGracefully()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var operations = new TestOperations(calls) { Characters = null };
|
||||
var host = new TestHost(calls);
|
||||
var controller = new LiveSessionController(operations);
|
||||
|
||||
LiveSessionStartResult result = controller.Start(
|
||||
LiveOptions(probe: true),
|
||||
host);
|
||||
|
||||
Assert.Equal(LiveSessionStartStatus.NoCharacters, result.Status);
|
||||
Assert.Empty(host.Rosters);
|
||||
Assert.Equal(
|
||||
[
|
||||
"reset", "resolve", "create", "bind", "report-connecting",
|
||||
"connect", "report-connected", "deactivate",
|
||||
"detach-events", "dispose-session", "detach-session", "reset",
|
||||
],
|
||||
calls);
|
||||
Assert.Equal(0, operations.EnterWorldCount);
|
||||
Assert.False(controller.IsInWorld);
|
||||
Assert.Null(controller.CurrentSession);
|
||||
Assert.Equal(1, operations.DisposeCounts[operations.Sessions[0]]);
|
||||
|
||||
LiveSessionOwnershipSnapshot ownership = controller.CaptureOwnership();
|
||||
Assert.Equal(RuntimeTeardownStage.Complete, ownership.LastTeardownStages);
|
||||
Assert.False(ownership.HasActiveSession);
|
||||
Assert.False(ownership.HasRetiredSession);
|
||||
Assert.False(ownership.HasPendingOperation);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("index")]
|
||||
[InlineData("id")]
|
||||
|
|
@ -1141,14 +1231,16 @@ public sealed class LiveSessionControllerTests
|
|||
private static LiveSessionConnectOptions LiveOptions(
|
||||
bool live = true,
|
||||
string? user = "user",
|
||||
LiveSessionCharacterSelector? selector = null) =>
|
||||
LiveSessionCharacterSelector? selector = null,
|
||||
bool probe = false) =>
|
||||
new(
|
||||
live,
|
||||
"127.0.0.1",
|
||||
9000,
|
||||
user ?? string.Empty,
|
||||
"password",
|
||||
selector);
|
||||
selector,
|
||||
probe);
|
||||
|
||||
private static CharacterList.Parsed AvailableCharacters() => new(
|
||||
0u,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue