Skip to content

Commit d050187

Browse files
Copilotericstj
andcommitted
Add test for multi-byte sequence interrupted by newline
Co-authored-by: ericstj <8918108+ericstj@users.noreply.github.com>
1 parent abb1879 commit d050187

1 file changed

Lines changed: 36 additions & 0 deletions

File tree

tests/ModelContextProtocol.Tests/Transport/PipeReaderExtensionsTests.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,42 @@ public async Task StandaloneCrNotFollowedByLf_IsIncludedInLine()
271271
Assert.IsType<JsonRpcRequest>(message);
272272
}
273273

274+
[Fact]
275+
public async Task MultiByteSequenceInterruptedByNewline_BothLinesSkipped_NextValidLineDelivered()
276+
{
277+
// '€' encodes as 3 UTF-8 bytes: 0xE2 0x82 0xAC. If a newline is injected after the
278+
// first byte, the two resulting lines both contain invalid byte sequences:
279+
// Line 1: ...0xE2\n — a truncated 3-byte lead byte; invalid JSON in both old and new impl
280+
// Line 2: 0x82 0xAC...\n — continuation bytes without a lead byte; also invalid JSON
281+
//
282+
// Both the old StreamReader-based path (which produced U+FFFD replacement chars before
283+
// passing to JsonSerializer) and the new PipeReader-based path (which passes raw bytes to
284+
// JsonSerializer) raise JsonException for each line and silently skip them. A subsequent
285+
// valid JSON line must still be delivered.
286+
var ct = TestContext.Current.CancellationToken;
287+
var pipe = new Pipe();
288+
await using var transport = new StreamServerTransport(pipe.Reader.AsStream(), Stream.Null);
289+
290+
// Build line 1: a JSON string where '€' is split after byte 0xE2, terminated with \n.
291+
byte[] euroBytes = Encoding.UTF8.GetBytes("€"); // [0xE2, 0x82, 0xAC]
292+
byte[] line1 = [.. Encoding.UTF8.GetBytes("{\"method\":\"te"), euroBytes[0], (byte)'\n'];
293+
// Build line 2: the remaining continuation bytes + rest of JSON, terminated with \n.
294+
byte[] line2 = [euroBytes[1], euroBytes[2], .. Encoding.UTF8.GetBytes("st\"}\n")];
295+
// Build line 3: a valid JSON message that must survive the two bad lines.
296+
byte[] line3 = Encoding.UTF8.GetBytes(s_testJson + "\n");
297+
298+
byte[] allBytes = [.. line1, .. line2, .. line3];
299+
await pipe.Writer.WriteAsync(allBytes, ct);
300+
await pipe.Writer.CompleteAsync();
301+
302+
// Only the valid line 3 should produce a message; lines 1 and 2 are silently skipped.
303+
var message = await transport.MessageReader.ReadAsync(ct);
304+
Assert.IsType<JsonRpcRequest>(message);
305+
306+
// No further messages.
307+
Assert.False(transport.MessageReader.TryRead(out _));
308+
}
309+
274310
[Fact]
275311
public async Task LineWithNoTerminatingNewline_IsNotDelivered()
276312
{

0 commit comments

Comments
 (0)