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 ConsolePump → ChatCommandRouter.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