← vibesdk
src/utils/ndjson-parser/ndjson-parser.test.ts
7.0 KB
Download file
import { describe, it, expect, beforeEach } from 'vitest';
import { NDJSONStreamParser } from './ndjson-parser';
describe('NDJSONStreamParser', () => {
let parser: NDJSONStreamParser;
let receivedMessages: any[];
beforeEach(() => {
receivedMessages = [];
parser = new NDJSONStreamParser((message) => {
receivedMessages.push(message);
});
});
describe('processChunk', () => {
it('should parse a single complete JSON message', () => {
const chunk = '{"type":"test","data":"hello"}\n';
parser.processChunk(chunk);
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
type: 'test',
data: 'hello'
});
});
it('should parse multiple JSON messages in one chunk', () => {
const chunk = '{"id":1,"name":"first"}\n{"id":2,"name":"second"}\n{"id":3,"name":"third"}\n';
parser.processChunk(chunk);
expect(receivedMessages).toHaveLength(3);
expect(receivedMessages[0]).toEqual({ id: 1, name: 'first' });
expect(receivedMessages[1]).toEqual({ id: 2, name: 'second' });
expect(receivedMessages[2]).toEqual({ id: 3, name: 'third' });
});
it('should handle partial messages across chunks', () => {
const chunk1 = '{"type":"partial","data":';
const chunk2 = '"incomplete message"';
const chunk3 = ',"status":"ok"}\n';
parser.processChunk(chunk1);
expect(receivedMessages).toHaveLength(0);
parser.processChunk(chunk2);
expect(receivedMessages).toHaveLength(0);
parser.processChunk(chunk3);
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
type: 'partial',
data: 'incomplete message',
status: 'ok'
});
});
it('should handle empty lines', () => {
const chunk = '{"id":1}\n\n\n{"id":2}\n\n';
parser.processChunk(chunk);
expect(receivedMessages).toHaveLength(2);
expect(receivedMessages[0]).toEqual({ id: 1 });
expect(receivedMessages[1]).toEqual({ id: 2 });
});
it('should handle invalid JSON gracefully', () => {
const chunk = '{"valid":true}\n{invalid json}\n{"alsoValid":true}\n';
parser.processChunk(chunk);
expect(receivedMessages).toHaveLength(2);
expect(receivedMessages[0]).toEqual({ valid: true });
expect(receivedMessages[1]).toEqual({ alsoValid: true });
});
it('should handle mixed line endings', () => {
const chunk = '{"unix":true}\n{"windows":true}\r\n{"mixed":true}\r\n';
parser.processChunk(chunk);
expect(receivedMessages).toHaveLength(3);
expect(receivedMessages[0]).toEqual({ unix: true });
expect(receivedMessages[1]).toEqual({ windows: true });
expect(receivedMessages[2]).toEqual({ mixed: true });
});
it('should handle special characters in JSON', () => {
const chunk = '{"text":"Hello\\nWorld","emoji":"😊","unicode":"\\u0048\\u0065\\u006c\\u006c\\u006f"}\n';
parser.processChunk(chunk);
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
text: 'Hello\nWorld',
emoji: '😊',
unicode: 'Hello'
});
});
it('should handle nested objects', () => {
const chunk = '{"user":{"id":1,"name":"John","address":{"city":"NYC","zip":"10001"}},"active":true}\n';
parser.processChunk(chunk);
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
user: {
id: 1,
name: 'John',
address: {
city: 'NYC',
zip: '10001'
}
},
active: true
});
});
it('should handle arrays in JSON', () => {
const chunk = '{"items":[1,2,3],"tags":["a","b","c"],"matrix":[[1,2],[3,4]]}\n';
parser.processChunk(chunk);
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
items: [1, 2, 3],
tags: ['a', 'b', 'c'],
matrix: [[1, 2], [3, 4]]
});
});
it('should handle large messages split across many chunks', () => {
const largeObject = {
id: 'test-123',
data: 'x'.repeat(1000),
nested: {
level1: {
level2: {
level3: {
value: 'deep'
}
}
}
}
};
const fullMessage = JSON.stringify(largeObject) + '\n';
const chunkSize = 100;
// Split into small chunks
for (let i = 0; i < fullMessage.length; i += chunkSize) {
parser.processChunk(fullMessage.slice(i, i + chunkSize));
}
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual(largeObject);
});
it('should maintain buffer state correctly', () => {
// Send incomplete message
parser.processChunk('{"incomplete":');
// Complete the first message with newline
parser.processChunk('false}\n');
// Send another complete message
parser.processChunk('{"complete":true}\n');
expect(receivedMessages).toHaveLength(2);
expect(receivedMessages[0]).toEqual({ incomplete: false });
expect(receivedMessages[1]).toEqual({ complete: true });
});
it('should handle numeric values correctly', () => {
const chunk = '{"int":42,"float":3.14,"negative":-100,"scientific":1.23e-4,"zero":0}\n';
parser.processChunk(chunk);
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
int: 42,
float: 3.14,
negative: -100,
scientific: 0.000123,
zero: 0
});
});
it('should handle boolean and null values', () => {
const chunk = '{"true":true,"false":false,"null":null,"undefined":null}\n';
parser.processChunk(chunk);
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0]).toEqual({
true: true,
false: false,
null: null,
undefined: null
});
});
it('should process streaming data from simulated WebSocket', () => {
// Simulate WebSocket-like streaming
const messages = [
{ type: 'start', id: 1 },
{ type: 'progress', percent: 50 },
{ type: 'complete', id: 1, result: 'success' }
];
// Simulate streaming with partial chunks
parser.processChunk(JSON.stringify(messages[0]).slice(0, 10));
parser.processChunk(JSON.stringify(messages[0]).slice(10) + '\n');
parser.processChunk(JSON.stringify(messages[1]) + '\n' + JSON.stringify(messages[2]));
parser.processChunk('\n');
expect(receivedMessages).toHaveLength(3);
expect(receivedMessages).toEqual(messages);
});
});
});