From 068e7175287c3b57d25e4c12cc6bc788c4f3b210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Tue, 2 Sep 2025 17:09:16 +0200 Subject: [PATCH 1/8] fix: partial line handling in docker events listener Improves the handling of stdout data from the docker events process by buffering incomplete lines and only processing complete lines. This prevents errors when event data arrives in chunks that do not align with line boundaries. --- src/utils/container-status.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/utils/container-status.ts b/src/utils/container-status.ts index 782323a..93a6575 100644 --- a/src/utils/container-status.ts +++ b/src/utils/container-status.ts @@ -130,9 +130,16 @@ function listenToContainerStatus( throw new Error("Failed to get stdout from docker events process"); } + let buffer = ""; dockerEvents.stdout.on("data", (data: Buffer) => { - const lines = data.toString().split("\n").filter(Boolean); - for (const line of lines) { + buffer += data.toString(); + + // Process all complete lines + let newlineIndex = buffer.indexOf("\n"); + while (newlineIndex !== -1) { + const line = buffer.substring(0, newlineIndex).trim(); + buffer = buffer.substring(newlineIndex + 1); + const json = safeJsonParse(line); const parsed = DockerEventsSchema.safeParse(json); if (!parsed.success) { @@ -154,6 +161,8 @@ function listenToContainerStatus( onStatusChange("stopped"); break; } + + newlineIndex = buffer.indexOf("\n"); } }); } catch (error) { From a57a5c5ee1aaf8c067874e25510573e30022196d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Wed, 3 Sep 2025 10:33:20 +0200 Subject: [PATCH 2/8] refactor into a testable JsonlStream interface --- src/test/jsonl-stream.test.ts | 118 ++++++++++++++++++++++++++++++++++ src/utils/container-status.ts | 63 +++++++----------- src/utils/jsonl-stream.ts | 44 +++++++++++++ 3 files changed, 186 insertions(+), 39 deletions(-) create mode 100644 src/test/jsonl-stream.test.ts create mode 100644 src/utils/jsonl-stream.ts diff --git a/src/test/jsonl-stream.test.ts b/src/test/jsonl-stream.test.ts new file mode 100644 index 0000000..2215720 --- /dev/null +++ b/src/test/jsonl-stream.test.ts @@ -0,0 +1,118 @@ +import * as assert from "node:assert"; + +import { LogLevel } from "vscode"; +import type { LogOutputChannel, Event } from "vscode"; + +import { createJsonlStream } from "../utils/jsonl-stream.js"; + +const setup = () => { + const mockOutputChannel: LogOutputChannel = { + name: "test", + logLevel: LogLevel.Info, + onDidChangeLogLevel: {} as Event, + append: () => {}, + appendLine: () => {}, + replace: () => {}, + clear: () => {}, + show: () => {}, + hide: () => {}, + dispose: () => {}, + trace: () => {}, + debug: () => {}, + info: () => {}, + warn: () => {}, + error: () => {}, + }; + + const jsonlStream = createJsonlStream(mockOutputChannel); + + const callbackCalls: unknown[] = []; + const callback = (data: unknown) => { + callbackCalls.push(data); + }; + jsonlStream.on(callback); + + return { + jsonlStream, + callbackCalls, + }; +}; + +suite("JSONL Streams", () => { + test("should parse and emit complete JSONL messages", () => { + const { jsonlStream, callbackCalls } = setup(); + + // Test with multiple JSON objects in a single write + const testData = Buffer.from('{"key1":"value1"}\n{"key2":"value2"}\n'); + + jsonlStream.write(testData); + + assert.strictEqual(callbackCalls.length, 2); + assert.deepStrictEqual(callbackCalls[0], { key1: "value1" }); + assert.deepStrictEqual(callbackCalls[1], { key2: "value2" }); + }); + + test("should handle incomplete JSONL messages across multiple writes", () => { + const { jsonlStream, callbackCalls } = setup(); + + // First write with partial message + const firstChunk = Buffer.from('{"key":"value'); + jsonlStream.write(firstChunk); + + // Shouldn't emit anything yet + assert.strictEqual(callbackCalls.length, 0); + + // Complete the message in second write + const secondChunk = Buffer.from('1"}\n'); + jsonlStream.write(secondChunk); + + // Now it should emit the complete message + assert.strictEqual(callbackCalls.length, 1); + assert.deepStrictEqual(callbackCalls[0], { key: "value1" }); + }); + + test("should handle multiple messages in chunks", () => { + const { jsonlStream, callbackCalls } = setup(); + + // Write first message and part of second + const firstChunk = Buffer.from('{"first":1}\n{"second":'); + jsonlStream.write(firstChunk); + + // First message should be emitted + assert.strictEqual(callbackCalls.length, 1); + assert.deepStrictEqual(callbackCalls[0], { first: 1 }); + + // Complete second message and add third + const secondChunk = Buffer.from('2}\n{"third":3}\n'); + jsonlStream.write(secondChunk); + + // Should have all three messages now + assert.strictEqual(callbackCalls.length, 3); + assert.deepStrictEqual(callbackCalls[1], { second: 2 }); + assert.deepStrictEqual(callbackCalls[2], { third: 3 }); + }); + + test("should ignore invalid JSON lines", () => { + const { jsonlStream, callbackCalls } = setup(); + + const testData = Buffer.from('not json\n{"valid":true}\n{invalid}\n'); + + jsonlStream.write(testData); + + // Should only emit the valid JSON object + assert.strictEqual(callbackCalls.length, 1); + assert.deepStrictEqual(callbackCalls[0], { valid: true }); + }); + + test("should handle empty lines", () => { + const { jsonlStream, callbackCalls } = setup(); + + const testData = Buffer.from('\n\n{"key":"value"}\n\n'); + + jsonlStream.write(testData); + + // Should only emit the valid JSON object + assert.strictEqual(callbackCalls.length, 1); + assert.deepStrictEqual(callbackCalls[0], { key: "value" }); + }); +}); diff --git a/src/utils/container-status.ts b/src/utils/container-status.ts index 93a6575..4e800d4 100644 --- a/src/utils/container-status.ts +++ b/src/utils/container-status.ts @@ -4,6 +4,7 @@ import type { Disposable, LogOutputChannel } from "vscode"; import * as z from "zod/v4-mini"; import { createEmitter } from "./emitter.ts"; +import { createJsonlStream } from "./jsonl-stream.ts"; export type ContainerStatus = "running" | "stopping" | "stopped"; @@ -63,14 +64,6 @@ const DockerEventsSchema = z.object({ }), }); -function safeJsonParse(text: string): unknown { - try { - return JSON.parse(text); - } catch { - return undefined; - } -} - function listenToContainerStatus( containerName: string, outputChannel: LogOutputChannel, @@ -130,41 +123,33 @@ function listenToContainerStatus( throw new Error("Failed to get stdout from docker events process"); } - let buffer = ""; - dockerEvents.stdout.on("data", (data: Buffer) => { - buffer += data.toString(); - - // Process all complete lines - let newlineIndex = buffer.indexOf("\n"); - while (newlineIndex !== -1) { - const line = buffer.substring(0, newlineIndex).trim(); - buffer = buffer.substring(newlineIndex + 1); - - const json = safeJsonParse(line); - const parsed = DockerEventsSchema.safeParse(json); - if (!parsed.success) { - continue; - } - - if (parsed.data.Actor.Attributes.name !== containerName) { - continue; - } + const jsonlStream = createJsonlStream(outputChannel); + jsonlStream.on((json) => { + const parsed = DockerEventsSchema.safeParse(json); + if (!parsed.success) { + return; + } - switch (parsed.data.Action) { - case "start": - onStatusChange("running"); - break; - case "kill": - onStatusChange("stopping"); - break; - case "die": - onStatusChange("stopped"); - break; - } + if (parsed.data.Actor.Attributes.name !== containerName) { + return; + } - newlineIndex = buffer.indexOf("\n"); + switch (parsed.data.Action) { + case "start": + onStatusChange("running"); + break; + case "kill": + onStatusChange("stopping"); + break; + case "die": + onStatusChange("stopped"); + break; } }); + + dockerEvents.stdout.on("data", (data: Buffer) => { + jsonlStream.write(data); + }); } catch (error) { // If we can't spawn the process, try again after a delay scheduleRestart(); diff --git a/src/utils/jsonl-stream.ts b/src/utils/jsonl-stream.ts new file mode 100644 index 0000000..cdd450a --- /dev/null +++ b/src/utils/jsonl-stream.ts @@ -0,0 +1,44 @@ +import type { LogOutputChannel } from "vscode"; + +import type { Callback } from "./emitter.ts"; +import { createEmitter } from "./emitter.ts"; + +interface JsonlStream { + write(data: Buffer): void; + on(callback: Callback): void; +} + +function safeJsonParse(text: string): unknown { + try { + return JSON.parse(text); + } catch { + return undefined; + } +} + +export const createJsonlStream = (outputChannel: LogOutputChannel): JsonlStream => { + const emitter = createEmitter(outputChannel) + let buffer = ""; + return { + write(data) { + buffer += data.toString(); + + // Process all complete lines + let newlineIndex = buffer.indexOf("\n"); + while (newlineIndex !== -1) { + const line = buffer.substring(0, newlineIndex).trim(); + buffer = buffer.substring(newlineIndex + 1); + + const json = safeJsonParse(line); + if (json) { + void emitter.emit(json) + } + + newlineIndex = buffer.indexOf("\n"); + } + }, + on(callback) { + emitter.on(callback) + }, + } +}; From 06168b2aed4d1f6f358978dccd2c11a275017244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Wed, 3 Sep 2025 11:22:16 +0200 Subject: [PATCH 3/8] wip --- src/test/jsonl-stream.test.ts | 24 ++------------ src/utils/container-status.ts | 12 +++---- src/utils/jsonl-stream.ts | 62 ++++++++++++++++++++--------------- 3 files changed, 44 insertions(+), 54 deletions(-) diff --git a/src/test/jsonl-stream.test.ts b/src/test/jsonl-stream.test.ts index 2215720..db4c1a8 100644 --- a/src/test/jsonl-stream.test.ts +++ b/src/test/jsonl-stream.test.ts @@ -3,34 +3,16 @@ import * as assert from "node:assert"; import { LogLevel } from "vscode"; import type { LogOutputChannel, Event } from "vscode"; -import { createJsonlStream } from "../utils/jsonl-stream.js"; +import { JsonlStream } from "../utils/jsonl-stream.js"; const setup = () => { - const mockOutputChannel: LogOutputChannel = { - name: "test", - logLevel: LogLevel.Info, - onDidChangeLogLevel: {} as Event, - append: () => {}, - appendLine: () => {}, - replace: () => {}, - clear: () => {}, - show: () => {}, - hide: () => {}, - dispose: () => {}, - trace: () => {}, - debug: () => {}, - info: () => {}, - warn: () => {}, - error: () => {}, - }; - - const jsonlStream = createJsonlStream(mockOutputChannel); + const jsonlStream = new JsonlStream(); const callbackCalls: unknown[] = []; const callback = (data: unknown) => { callbackCalls.push(data); }; - jsonlStream.on(callback); + jsonlStream.onJson(callback); return { jsonlStream, diff --git a/src/utils/container-status.ts b/src/utils/container-status.ts index 4e800d4..4093e11 100644 --- a/src/utils/container-status.ts +++ b/src/utils/container-status.ts @@ -4,7 +4,7 @@ import type { Disposable, LogOutputChannel } from "vscode"; import * as z from "zod/v4-mini"; import { createEmitter } from "./emitter.ts"; -import { createJsonlStream } from "./jsonl-stream.ts"; +import { JsonlStream } from "./jsonl-stream.ts"; export type ContainerStatus = "running" | "stopping" | "stopped"; @@ -123,8 +123,8 @@ function listenToContainerStatus( throw new Error("Failed to get stdout from docker events process"); } - const jsonlStream = createJsonlStream(outputChannel); - jsonlStream.on((json) => { + const jsonlStream = new JsonlStream(); + jsonlStream.onJson((json) => { const parsed = DockerEventsSchema.safeParse(json); if (!parsed.success) { return; @@ -134,6 +134,8 @@ function listenToContainerStatus( return; } + outputChannel.debug(`[container.status]: ${parsed.data.Action}`); + switch (parsed.data.Action) { case "start": onStatusChange("running"); @@ -147,9 +149,7 @@ function listenToContainerStatus( } }); - dockerEvents.stdout.on("data", (data: Buffer) => { - jsonlStream.write(data); - }); + dockerEvents.stdout.pipe(jsonlStream); } catch (error) { // If we can't spawn the process, try again after a delay scheduleRestart(); diff --git a/src/utils/jsonl-stream.ts b/src/utils/jsonl-stream.ts index cdd450a..44bad39 100644 --- a/src/utils/jsonl-stream.ts +++ b/src/utils/jsonl-stream.ts @@ -1,44 +1,52 @@ -import type { LogOutputChannel } from "vscode"; +import { Writable } from "node:stream"; -import type { Callback } from "./emitter.ts"; -import { createEmitter } from "./emitter.ts"; - -interface JsonlStream { - write(data: Buffer): void; - on(callback: Callback): void; -} - -function safeJsonParse(text: string): unknown { +/** + * Safely parses a JSON string, returning null if parsing fails. + * @param str - The JSON string to parse. + * @returns The parsed object or null if invalid. + */ +export function safeJsonParse(str: string): unknown { try { - return JSON.parse(text); + return JSON.parse(str); } catch { return undefined; } } -export const createJsonlStream = (outputChannel: LogOutputChannel): JsonlStream => { - const emitter = createEmitter(outputChannel) - let buffer = ""; - return { - write(data) { - buffer += data.toString(); +/** + * Writable stream that buffers data until a newline, + * parses each line as JSON, and emits the parsed object. + */ +export class JsonlStream extends Writable { + constructor() { + let buffer = ""; + super({ + write: (chunk, _encoding, callback) => { + buffer += String(chunk); - // Process all complete lines let newlineIndex = buffer.indexOf("\n"); while (newlineIndex !== -1) { const line = buffer.substring(0, newlineIndex).trim(); buffer = buffer.substring(newlineIndex + 1); const json = safeJsonParse(line); - if (json) { - void emitter.emit(json) - } + if (json !== null) { + this.emit("json", json); + } newlineIndex = buffer.indexOf("\n"); } - }, - on(callback) { - emitter.on(callback) - }, - } -}; + + callback(); + }, + }); + } + + /** + * Registers a listener for parsed JSON objects. + * @param listener - Function called with each parsed object. + */ + onJson(callback: (json: unknown) => void) { + this.on("json", callback); + } +} From 221cc6cddbdc2a464c9902f9e3232ce9098c3b74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Wed, 3 Sep 2025 11:24:10 +0200 Subject: [PATCH 4/8] wip --- src/test/jsonl-stream.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/test/jsonl-stream.test.ts b/src/test/jsonl-stream.test.ts index db4c1a8..8b3c987 100644 --- a/src/test/jsonl-stream.test.ts +++ b/src/test/jsonl-stream.test.ts @@ -9,10 +9,9 @@ const setup = () => { const jsonlStream = new JsonlStream(); const callbackCalls: unknown[] = []; - const callback = (data: unknown) => { + jsonlStream.onJson((data: unknown) => { callbackCalls.push(data); - }; - jsonlStream.onJson(callback); + }); return { jsonlStream, From fa01831ad167007e3c9d49639e4eb976f3b6fdf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Wed, 3 Sep 2025 11:25:06 +0200 Subject: [PATCH 5/8] wip --- src/test/jsonl-stream.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/test/jsonl-stream.test.ts b/src/test/jsonl-stream.test.ts index 8b3c987..29b0486 100644 --- a/src/test/jsonl-stream.test.ts +++ b/src/test/jsonl-stream.test.ts @@ -1,8 +1,5 @@ import * as assert from "node:assert"; -import { LogLevel } from "vscode"; -import type { LogOutputChannel, Event } from "vscode"; - import { JsonlStream } from "../utils/jsonl-stream.js"; const setup = () => { From 5ca7181a54811143554efb878c2133e632d00227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Wed, 3 Sep 2025 11:33:00 +0200 Subject: [PATCH 6/8] fix --- src/utils/jsonl-stream.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/jsonl-stream.ts b/src/utils/jsonl-stream.ts index 44bad39..40c9029 100644 --- a/src/utils/jsonl-stream.ts +++ b/src/utils/jsonl-stream.ts @@ -30,7 +30,7 @@ export class JsonlStream extends Writable { buffer = buffer.substring(newlineIndex + 1); const json = safeJsonParse(line); - if (json !== null) { + if (json !== undefined) { this.emit("json", json); } From 5a1266d676ceab9b0eec734eb710814e065855bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Wed, 3 Sep 2025 13:32:01 +0200 Subject: [PATCH 7/8] rename --- ...ream.test.ts => json-lines-stream.test.ts} | 38 +++++++++---------- src/utils/container-status.ts | 4 +- .../{jsonl-stream.ts => json-lines-stream.ts} | 6 +-- 3 files changed, 24 insertions(+), 24 deletions(-) rename src/test/{jsonl-stream.test.ts => json-lines-stream.test.ts} (72%) rename src/utils/{jsonl-stream.ts => json-lines-stream.ts} (85%) diff --git a/src/test/jsonl-stream.test.ts b/src/test/json-lines-stream.test.ts similarity index 72% rename from src/test/jsonl-stream.test.ts rename to src/test/json-lines-stream.test.ts index 29b0486..623b753 100644 --- a/src/test/jsonl-stream.test.ts +++ b/src/test/json-lines-stream.test.ts @@ -1,48 +1,48 @@ import * as assert from "node:assert"; -import { JsonlStream } from "../utils/jsonl-stream.js"; +import { JsonLinesStream } from "../utils/json-lines-stream.js"; const setup = () => { - const jsonlStream = new JsonlStream(); + const stream = new JsonLinesStream(); const callbackCalls: unknown[] = []; - jsonlStream.onJson((data: unknown) => { + stream.onJson((data: unknown) => { callbackCalls.push(data); }); return { - jsonlStream, + stream, callbackCalls, }; }; -suite("JSONL Streams", () => { - test("should parse and emit complete JSONL messages", () => { - const { jsonlStream, callbackCalls } = setup(); +suite("JsonLinesStream Test Suite", () => { + test("should parse and emit complete JsonLines messages", () => { + const { stream, callbackCalls } = setup(); // Test with multiple JSON objects in a single write const testData = Buffer.from('{"key1":"value1"}\n{"key2":"value2"}\n'); - jsonlStream.write(testData); + stream.write(testData); assert.strictEqual(callbackCalls.length, 2); assert.deepStrictEqual(callbackCalls[0], { key1: "value1" }); assert.deepStrictEqual(callbackCalls[1], { key2: "value2" }); }); - test("should handle incomplete JSONL messages across multiple writes", () => { - const { jsonlStream, callbackCalls } = setup(); + test("should handle incomplete JsonLines messages across multiple writes", () => { + const { stream, callbackCalls } = setup(); // First write with partial message const firstChunk = Buffer.from('{"key":"value'); - jsonlStream.write(firstChunk); + stream.write(firstChunk); // Shouldn't emit anything yet assert.strictEqual(callbackCalls.length, 0); // Complete the message in second write const secondChunk = Buffer.from('1"}\n'); - jsonlStream.write(secondChunk); + stream.write(secondChunk); // Now it should emit the complete message assert.strictEqual(callbackCalls.length, 1); @@ -50,11 +50,11 @@ suite("JSONL Streams", () => { }); test("should handle multiple messages in chunks", () => { - const { jsonlStream, callbackCalls } = setup(); + const { stream, callbackCalls } = setup(); // Write first message and part of second const firstChunk = Buffer.from('{"first":1}\n{"second":'); - jsonlStream.write(firstChunk); + stream.write(firstChunk); // First message should be emitted assert.strictEqual(callbackCalls.length, 1); @@ -62,7 +62,7 @@ suite("JSONL Streams", () => { // Complete second message and add third const secondChunk = Buffer.from('2}\n{"third":3}\n'); - jsonlStream.write(secondChunk); + stream.write(secondChunk); // Should have all three messages now assert.strictEqual(callbackCalls.length, 3); @@ -71,11 +71,11 @@ suite("JSONL Streams", () => { }); test("should ignore invalid JSON lines", () => { - const { jsonlStream, callbackCalls } = setup(); + const { stream, callbackCalls } = setup(); const testData = Buffer.from('not json\n{"valid":true}\n{invalid}\n'); - jsonlStream.write(testData); + stream.write(testData); // Should only emit the valid JSON object assert.strictEqual(callbackCalls.length, 1); @@ -83,11 +83,11 @@ suite("JSONL Streams", () => { }); test("should handle empty lines", () => { - const { jsonlStream, callbackCalls } = setup(); + const { stream, callbackCalls } = setup(); const testData = Buffer.from('\n\n{"key":"value"}\n\n'); - jsonlStream.write(testData); + stream.write(testData); // Should only emit the valid JSON object assert.strictEqual(callbackCalls.length, 1); diff --git a/src/utils/container-status.ts b/src/utils/container-status.ts index 4093e11..4b1a09e 100644 --- a/src/utils/container-status.ts +++ b/src/utils/container-status.ts @@ -4,7 +4,7 @@ import type { Disposable, LogOutputChannel } from "vscode"; import * as z from "zod/v4-mini"; import { createEmitter } from "./emitter.ts"; -import { JsonlStream } from "./jsonl-stream.ts"; +import { JsonLinesStream } from "./json-lines-stream.ts"; export type ContainerStatus = "running" | "stopping" | "stopped"; @@ -123,7 +123,7 @@ function listenToContainerStatus( throw new Error("Failed to get stdout from docker events process"); } - const jsonlStream = new JsonlStream(); + const jsonlStream = new JsonLinesStream(); jsonlStream.onJson((json) => { const parsed = DockerEventsSchema.safeParse(json); if (!parsed.success) { diff --git a/src/utils/jsonl-stream.ts b/src/utils/json-lines-stream.ts similarity index 85% rename from src/utils/jsonl-stream.ts rename to src/utils/json-lines-stream.ts index 40c9029..f337421 100644 --- a/src/utils/jsonl-stream.ts +++ b/src/utils/json-lines-stream.ts @@ -1,9 +1,9 @@ import { Writable } from "node:stream"; /** - * Safely parses a JSON string, returning null if parsing fails. + * Safely parses a JSON string, returning undefined if parsing fails. * @param str - The JSON string to parse. - * @returns The parsed object or null if invalid. + * @returns The parsed object or undefined if invalid. */ export function safeJsonParse(str: string): unknown { try { @@ -17,7 +17,7 @@ export function safeJsonParse(str: string): unknown { * Writable stream that buffers data until a newline, * parses each line as JSON, and emits the parsed object. */ -export class JsonlStream extends Writable { +export class JsonLinesStream extends Writable { constructor() { let buffer = ""; super({ From 56179e4586b8e31f1311c249e95287b993f20bf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Pallar=C3=A9s?= Date: Wed, 3 Sep 2025 13:36:31 +0200 Subject: [PATCH 8/8] wip --- src/test/json-lines-stream.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/json-lines-stream.test.ts b/src/test/json-lines-stream.test.ts index 623b753..88d7785 100644 --- a/src/test/json-lines-stream.test.ts +++ b/src/test/json-lines-stream.test.ts @@ -17,7 +17,7 @@ const setup = () => { }; suite("JsonLinesStream Test Suite", () => { - test("should parse and emit complete JsonLines messages", () => { + test("should parse and emit complete JSON lines messages", () => { const { stream, callbackCalls } = setup(); // Test with multiple JSON objects in a single write @@ -30,7 +30,7 @@ suite("JsonLinesStream Test Suite", () => { assert.deepStrictEqual(callbackCalls[1], { key2: "value2" }); }); - test("should handle incomplete JsonLines messages across multiple writes", () => { + test("should handle incomplete JSON lines messages across multiple writes", () => { const { stream, callbackCalls } = setup(); // First write with partial message