fix(headless): S6 thread standardOutputIsTerminal instead of reading Console inside HeadlessProcessHost
HeadlessProcessHost read System.Console.IsOutputRedirected directly to pick the console renderer's color mode, which only Program.cs (the executable's own entry point) should ever touch -- the same reasoning that already put the stdin probe there. Resolve standardOutputIsTerminal next to the existing !Console.IsInputRedirected probe in Program.cs and thread it through HeadlessEntryPoint.Run into HeadlessProcessHost's constructor as a plain parameter. StandardOutputIsTerminalParameterControlsColorNotTheRealConsole was shown to fail with the parameter still unused (useColor still reading the real Console.IsOutputRedirected, which the test host always redirects): the standardOutputIsTerminal:true case expected dimmed lifecycle output but got none, since the real console read forced useColor=false regardless of what the test passed in. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
3328b2f2b3
commit
2ff1e1228c
4 changed files with 72 additions and 5 deletions
|
|
@ -47,7 +47,8 @@ internal static class HeadlessEntryPoint
|
|||
TextWriter output,
|
||||
TextWriter error,
|
||||
CancellationToken cancellationToken,
|
||||
bool standardInputIsTerminal = false)
|
||||
bool standardInputIsTerminal = false,
|
||||
bool standardOutputIsTerminal = false)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(arguments);
|
||||
ArgumentNullException.ThrowIfNull(standardInput);
|
||||
|
|
@ -85,7 +86,8 @@ internal static class HeadlessEntryPoint
|
|||
output,
|
||||
directCredentials:
|
||||
commandLine.DirectCredentials,
|
||||
consoleEnabled: consoleEnabled);
|
||||
consoleEnabled: consoleEnabled,
|
||||
standardOutputIsTerminal: standardOutputIsTerminal);
|
||||
return (int)host.RunAsync(cancellationToken)
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
|
|
|
|||
|
|
@ -37,7 +37,8 @@ internal sealed class HeadlessProcessHost : IDisposable
|
|||
TimeProvider? timeProvider = null,
|
||||
IHeadlessProcessContentFactory? contentFactory = null,
|
||||
HeadlessDirectCredentials? directCredentials = null,
|
||||
bool consoleEnabled = false)
|
||||
bool consoleEnabled = false,
|
||||
bool standardOutputIsTerminal = false)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(configuration);
|
||||
ArgumentNullException.ThrowIfNull(paths);
|
||||
|
|
@ -163,7 +164,7 @@ internal sealed class HeadlessProcessHost : IDisposable
|
|||
HeadlessSessionHost session = _sessions[0];
|
||||
var renderer = new HeadlessConsoleRenderer(
|
||||
diagnostics,
|
||||
useColor: !System.Console.IsOutputRedirected);
|
||||
useColor: standardOutputIsTerminal);
|
||||
consoleRendererSubscription =
|
||||
session.Runtime.Subscribe(renderer);
|
||||
HeadlessConsoleController controller = new(
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ try
|
|||
Console.Out,
|
||||
Console.Error,
|
||||
cancellation.Token,
|
||||
standardInputIsTerminal: !Console.IsInputRedirected);
|
||||
standardInputIsTerminal: !Console.IsInputRedirected,
|
||||
standardOutputIsTerminal: !Console.IsOutputRedirected);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using AcDream.Core.Chat;
|
||||
|
|
@ -8,6 +9,7 @@ using AcDream.Headless.Configuration;
|
|||
using AcDream.Headless.Credentials;
|
||||
using AcDream.Headless.Diagnostics;
|
||||
using AcDream.Headless.Hosting;
|
||||
using AcDream.Headless.Platform;
|
||||
using AcDream.Plugin.Abstractions;
|
||||
using AcDream.Runtime;
|
||||
using AcDream.Runtime.Chat;
|
||||
|
|
@ -520,6 +522,48 @@ public sealed class HeadlessConsoleTests
|
|||
Assert.Equal(["[vt] navigation route loaded"], printed);
|
||||
}
|
||||
|
||||
// ── HeadlessProcessHost: end-to-end console wiring ───────────────────
|
||||
|
||||
/// <summary>
|
||||
/// S6: <c>standardOutputIsTerminal</c> is threaded in as a constructor
|
||||
/// parameter, not read from the real <c>System.Console</c> inside
|
||||
/// <see cref="HeadlessProcessHost"/> — proven by flipping only the
|
||||
/// parameter (this test process's OWN stdout is redirected by the test
|
||||
/// host either way) and observing the renderer's dim-vs-plain choice
|
||||
/// follow it.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(true, true)]
|
||||
[InlineData(false, false)]
|
||||
public async Task StandardOutputIsTerminalParameterControlsColorNotTheRealConsole(
|
||||
bool standardOutputIsTerminal, bool expectDimmed)
|
||||
{
|
||||
var operations = new FixtureSessionOperations();
|
||||
var configuration = new HeadlessConfiguration
|
||||
{
|
||||
Version = 1,
|
||||
Sessions = [Descriptor()],
|
||||
};
|
||||
using var diagnostics = new StringWriter();
|
||||
using var input = new System.IO.StringReader("/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,
|
||||
standardOutputIsTerminal: standardOutputIsTerminal);
|
||||
|
||||
await host.RunAsync(CancellationToken.None).WaitAsync(TimeSpan.FromSeconds(10));
|
||||
|
||||
string text = diagnostics.ToString();
|
||||
Assert.Contains("entered world", text);
|
||||
Assert.Equal(expectDimmed, text.Contains("[2m"));
|
||||
}
|
||||
|
||||
private static bool WaitForEndOfInput(HeadlessConsoleController controller) =>
|
||||
controller.Reader.EndOfInput.Wait(TimeSpan.FromSeconds(5));
|
||||
|
||||
|
|
@ -625,4 +669,23 @@ public sealed class HeadlessConsoleTests
|
|||
base.Dispose(disposing);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// S3/S6/S7: a real-clock <see cref="TimeProvider"/>, distinct from
|
||||
/// <see cref="TimeProvider.System"/>, for a <see cref="HeadlessProcessHost"/>
|
||||
/// integration test that runs the actual scheduler loop on its own
|
||||
/// dedicated thread. Real elapsed time (not a manually-stepped fake) is
|
||||
/// deliberate here: <see cref="HeadlessProcessHost.RunAsync"/> owns its
|
||||
/// own background thread, and stepping a manual clock from the test
|
||||
/// thread while that thread's scheduler loop waits on a
|
||||
/// <see cref="ITimer"/> armed from the SAME provider would race the two
|
||||
/// threads for no benefit — the default 15 ms turn period already makes
|
||||
/// these tests fast.
|
||||
/// </summary>
|
||||
private sealed class FakeTimeProvider : TimeProvider
|
||||
{
|
||||
public override long GetTimestamp() => Stopwatch.GetTimestamp();
|
||||
|
||||
public override long TimestampFrequency => Stopwatch.Frequency;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue