diff --git a/tests/AcDream.Headless.Tests/HeadlessConsoleTests.cs b/tests/AcDream.Headless.Tests/HeadlessConsoleTests.cs index 4a0b5f45..d8991b58 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