diff --git a/NucleicBroker.Tests/OutboundWriterTests.cs b/NucleicBroker.Tests/OutboundWriterTests.cs index 68dc7d0..d96e6dd 100644 --- a/NucleicBroker.Tests/OutboundWriterTests.cs +++ b/NucleicBroker.Tests/OutboundWriterTests.cs @@ -33,6 +33,24 @@ public sealed class OutboundWriterTests Assert.Equal("onetwo", decoded); } + [Fact] + public async Task FramesLinesWithBareLineFeed() + { + // The wire is LF-framed on every host (docs/WINDOWS_PORT.md §3.3). TextWriter.NewLine + // would otherwise be CRLF on Windows, hiding a stray \r inside every line hostd parses. + // Forcing CRLF on the sink reproduces the Windows default on any platform, so this + // pins the framing even when CI runs the suite on Linux. + var captured = new StringWriter { NewLine = "\r\n" }; + var writer = new OutboundWriter(captured); + writer.EnqueueJson("""{"a":1}"""); + writer.EnqueueProcOutput(1, stderr: false, "one"u8); + await writer.CompleteAsync(); + + var text = captured.ToString(); + Assert.DoesNotContain('\r', text); + Assert.EndsWith("\n", text); + } + [Fact] public async Task StderrAndStdoutChunksNeverMerge() { diff --git a/NucleicBroker/OutboundWriter.cs b/NucleicBroker/OutboundWriter.cs index 982a2ed..967b987 100644 --- a/NucleicBroker/OutboundWriter.cs +++ b/NucleicBroker/OutboundWriter.cs @@ -26,6 +26,10 @@ public sealed class OutboundWriter public OutboundWriter(TextWriter output) { this.output = output; + // NDJSON is LF-framed on the wire (docs/WINDOWS_PORT.md §3.3). TextWriter.NewLine + // defaults to Environment.NewLine — CRLF here — which would append a stray \r to + // every line hostd's JSONRPCConnection reads. We own this writer, so pin it. + this.output.NewLine = "\n"; loop = Task.Run(DrainAsync); }