From 70e86c180e94000557e6fa2720d218e2b1861010 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 7 Sep 2026 08:10:52 +0200 Subject: [PATCH] test(headless): S3 end-to-end HeadlessProcessHost console proof Existing coverage exercises HeadlessSessionHost.SubmitConsoleLine directly (bypassing the background reader thread and the scheduler) or HeadlessConsoleController in isolation (a fake submit callback, no real session). Add one test that drives the actual production wiring: a plain StringReader feeds "hello" and "/quit" through the real HeadlessProcessHost constructor (background reader thread -> per-tick ConsolePump -> ChatCommandRouter.Submit -> the wire), asserting the outbound Talk action reaches the fixture AND that /quit ends RunAsync with HeadlessExitCode.Success -- the same graceful path an external cancellation takes. Mutation: removed the session.ConsolePump assignment in HeadlessProcessHost's constructor (never wiring the drain+pump delegate). The test failed with a TimeoutException -- the queued console lines were never drained, so /quit's cancellation never fired and RunAsync ran until the test's own 10s WaitAsync bound. Co-Authored-By: Claude Fable 5.1 --- .../HeadlessConsoleTests.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/AcDream.Headless.Tests/HeadlessConsoleTests.cs b/tests/AcDream.Headless.Tests/HeadlessConsoleTests.cs index 4a0b5f45e..d8991b58f 100644 --- a/tests/AcDream.Headless.Tests/HeadlessConsoleTests.cs +++ b/tests/AcDream.Headless.Tests/HeadlessConsoleTests.cs @@ -524,6 +524,51 @@ public sealed class HeadlessConsoleTests // ── HeadlessProcessHost: end-to-end console wiring ─────────────────── + /// + /// S3: an end-to-end proof that a console line, read from a plain + /// , reaches the real session's + /// SubmitConsoleLine pipeline through the actual + /// wiring (background reader thread → + /// per-tick ConsolePumpChatCommandRouter.Submit → the + /// wire), and that /quit ends + /// through the SAME graceful path an external cancellation takes — + /// , not an error code. + /// + [Fact] + public async Task ConsoleLineReachesTheSessionAndQuitEndsTheProcessGracefully() + { + var captured = new List(); + var operations = new FixtureSessionOperations + { + GameActionCapture = body => captured.Add(body), + }; + var configuration = new HeadlessConfiguration + { + Version = 1, + Sessions = [Descriptor()], + }; + using var diagnostics = new StringWriter(); + using var input = new System.IO.StringReader( + "hello" + Environment.NewLine + "/quit" + Environment.NewLine); + using var host = new HeadlessProcessHost( + configuration, + HeadlessPathSet.Resolve(new HeadlessPathOverrides()), + input, + diagnostics, + operations, + new FakeTimeProvider(), + directCredentials: new HeadlessDirectCredentials("account", "password"), + consoleEnabled: true); + + HeadlessExitCode exitCode = await host.RunAsync(CancellationToken.None) + .WaitAsync(TimeSpan.FromSeconds(10)); + + Assert.Equal(HeadlessExitCode.Success, exitCode); + byte[] body = Assert.Single(captured); + Assert.Equal(ChatRequests.TalkOpcode, ActionOpcode(body)); + Assert.Equal("hello", TalkText(body)); + } + /// /// S6: standardOutputIsTerminal is threaded in as a constructor /// parameter, not read from the real System.Console inside