feat(runtime): share chat commands and run login sequence

This commit is contained in:
Erik 2026-08-14 20:27:45 +02:00
parent 5535d0adac
commit 41b15efd4d
57 changed files with 1739 additions and 345 deletions

View file

@ -106,6 +106,44 @@ public sealed class LauncherOrchestratorTests : IDisposable
Assert.Contains("+Acdream", inWorld.Status, StringComparison.Ordinal);
}
[Fact]
public async Task LoginCommandFailureIsVisibleButDoesNotMakeTheSessionTerminal()
{
var statusSources = new QueueStatusSourceFactory();
using LauncherOrchestrator orchestrator = CreateOrchestrator(
statusSourceFactory: statusSources);
_ = await orchestrator.LaunchAsync(
"Local ACE",
"testaccount",
"+Acdream",
LaunchMode.Headless);
QueueStatusSource source = Assert.Single(statusSources.Created);
source.Enqueue(Connected("s1"));
source.Enqueue(EnteredWorld("s1", "+Acdream"));
source.Enqueue(new LoginCommandFailedStatusEvent
{
V = 1,
E = "loginCommandFailed",
T = DateTimeOffset.UtcNow,
SessionId = "s1",
CommandIndex = 2,
Command = "/version",
Error = "not available in the headless host",
});
orchestrator.PollStatus();
LauncherSessionSnapshot session = Assert.Single(
orchestrator.GetSnapshot().Sessions);
Assert.Equal(LauncherActivityState.InWorld, session.State);
Assert.Equal(
"Login command 2 failed: not available in the headless host",
session.Error);
Assert.Equal(session.Error, session.Status);
Assert.Null(session.ExitCode);
}
[Fact]
public async Task AccountGuiSelectDoesNotRequireACachedCharacterOrEmitASelector()
{

View file

@ -77,6 +77,32 @@ public sealed class StatusEventParserTests
Assert.Equal("boom", failed.Error);
}
[Fact]
public void ParsesLoginCommandFailed()
{
var failed = Assert.IsType<LoginCommandFailedStatusEvent>(
StatusEventParser.Parse(
"""{"v":1,"e":"loginCommandFailed","t":"2026-08-14T12:00:05Z","sessionId":"s1","commandIndex":2,"command":"/version","error":"unsupported headless command"}"""));
Assert.Equal(2, failed.CommandIndex);
Assert.Equal("/version", failed.Command);
Assert.Equal("unsupported headless command", failed.Error);
}
[Theory]
[InlineData("{\"v\":1,\"e\":\"loginCommandFailed\",\"t\":\"2026-08-14T12:00:05Z\",\"sessionId\":\"s1\",\"commandIndex\":-1,\"command\":\"/version\",\"error\":\"unsupported\"}")]
[InlineData("{\"v\":1,\"e\":\"loginCommandFailed\",\"t\":\"2026-08-14T12:00:05Z\",\"sessionId\":\"s1\",\"commandIndex\":0,\"error\":\"unsupported\"}")]
[InlineData("{\"v\":1,\"e\":\"loginCommandFailed\",\"t\":\"2026-08-14T12:00:05Z\",\"sessionId\":\"s1\",\"commandIndex\":0,\"command\":\"/version\"}")]
public void MalformedLoginCommandFailureUsesTheKnownEventFailurePath(string line)
{
var malformed = Assert.IsType<MalformedStatusEvent>(
StatusEventParser.Parse(line));
Assert.Equal("loginCommandFailed", malformed.E);
Assert.Equal("s1", malformed.SessionId);
Assert.False(string.IsNullOrWhiteSpace(malformed.Error));
}
[Fact]
public void ParsesDisconnectedAndExited()
{