wip: Campaign LA LA2 probe mode + idle policy — INCOMPLETE, stopped mid-task

Agent was stopped for token budget. Landed here: probe flag through
LiveSessionConnectOptions + the StartCore short-circuit, the mode field
with JsonRequired-to-semantic-validation move, host exit-code mapping,
and 34 passing tests including 3 new probe tests (agent last reported
green before the stop). NOT DONE: the idle-policy unit tests (next
step), full-suite verification, and the WSL run.

Build/test state UNVERIFIED at this commit. Next session: finish idle
policy tests, run Runtime+Headless Release suites Windows and WSL, then
dispatch the Opus dual-lens review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-14 16:33:00 +02:00
parent 498f1c1182
commit c601942467
14 changed files with 830 additions and 35 deletions

View file

@ -389,6 +389,81 @@ 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>
/// The probe short-circuit fires even when the server never returns a
/// CharacterList at all (GetCharacters returns null) — a probe is a
/// connectivity check, not itself a character-selection operation, so it
/// must not fall through to the NoCharacters path.
/// </summary>
[Fact]
public void Start_ProbeWithoutCharacterListStillCompletesGracefully()
{
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.ProbeComplete, result.Status);
Assert.Empty(host.Rosters);
Assert.Equal(0, operations.EnterWorldCount);
Assert.False(controller.IsInWorld);
}
[Theory]
[InlineData("index")]
[InlineData("id")]
@ -1141,14 +1216,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,