Files
jina-ai--reader/tests/unit/json-parse-stream.test.ts
wehub-resource-sync f1ba9c6c36
/ test (push) Failing after 1s
/ build-and-push-to-ghcr (push) Has been skipped
chore: import upstream snapshot with attribution
2026-07-13 12:23:39 +08:00

276 lines
9.3 KiB
TypeScript

/**
* Unit tests for JSONParserStream and JSONAccumulation.
*
* JSONParserStream parses JSON incrementally, emitting structured events.
* JSONAccumulation consumes those events and reconstructs the value tree.
* Both expose a static .parse() helper for synchronous single-shot use.
*/
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { JSONParserStream, JSONAccumulation } from '../../build/lib/json-parse-stream.js';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function parse(input: string, opts?: ConstructorParameters<typeof JSONParserStream>[0]) {
return JSONParserStream.parse(input, opts);
}
function accumulate(input: string, focus = '', opts?: ConstructorParameters<typeof JSONParserStream>[0]) {
const events = parse(input, opts);
return JSONAccumulation.parse(events as any, focus);
}
function eventTypes(input: string, opts?: ConstructorParameters<typeof JSONParserStream>[0]) {
return parse(input, opts).map((e: any) => e.event);
}
// ---------------------------------------------------------------------------
// Primitive values
// ---------------------------------------------------------------------------
describe('JSONParserStream: string', () => {
it('emits nodeStart text, textChunk, text, nodeEnd text, end for a simple string', () => {
const evs = parse('"hello"');
const types = evs.map((e: any) => e.event);
assert.ok(types.includes('nodeStart'));
assert.ok(types.includes('text'));
assert.ok(types.includes('nodeEnd'));
assert.ok(types.includes('end'));
});
it('accumulates a simple string correctly', () => {
assert.equal(accumulate('"hello world"'), 'hello world');
});
it('accumulates an empty string', () => {
assert.equal(accumulate('""'), '');
});
it('accumulates a string with escape sequences', () => {
assert.equal(accumulate('"line1\\nline2"'), 'line1\nline2');
});
it('accumulates a string with unicode escape', () => {
assert.equal(accumulate('"\\u0041"'), 'A');
});
it('accumulates a string with escaped quote', () => {
assert.equal(accumulate('"say \\"hi\\""'), 'say "hi"');
});
it('accumulates a string with escaped backslash', () => {
assert.equal(accumulate('"a\\\\b"'), 'a\\b');
});
});
describe('JSONParserStream: numbers', () => {
it('accumulates an integer', () => {
assert.equal(accumulate('42'), 42);
});
it('accumulates zero', () => {
assert.equal(accumulate('0'), 0);
});
it('accumulates a negative number', () => {
assert.equal(accumulate('-7'), -7);
});
it('accumulates a floating-point number', () => {
assert.equal(accumulate('3.14'), 3.14);
});
it('accumulates a number in scientific notation', () => {
assert.equal(accumulate('1e3'), 1000);
});
});
describe('JSONParserStream: booleans and null', () => {
it('accumulates true', () => {
assert.equal(accumulate('true'), true);
});
it('accumulates false', () => {
assert.equal(accumulate('false'), false);
});
it('accumulates null', () => {
assert.equal(accumulate('null'), null);
});
});
// ---------------------------------------------------------------------------
// Objects
// ---------------------------------------------------------------------------
describe('JSONParserStream: objects', () => {
it('accumulates an empty object', () => {
assert.deepEqual(accumulate('{}'), {});
});
it('accumulates a flat object', () => {
assert.deepEqual(accumulate('{"a":1,"b":"two"}'), { a: 1, b: 'two' });
});
it('accumulates a nested object', () => {
assert.deepEqual(accumulate('{"x":{"y":42}}'), { x: { y: 42 } });
});
it('accumulates an object with mixed value types', () => {
assert.deepEqual(
accumulate('{"s":"str","n":1,"b":true,"nil":null}'),
{ s: 'str', n: 1, b: true, nil: null }
);
});
it('emits nodeStart/nodeEnd for object', () => {
const types = eventTypes('{"k":"v"}');
assert.ok(types.includes('nodeStart'));
assert.ok(types.includes('nodeEnd'));
});
});
// ---------------------------------------------------------------------------
// Arrays
// ---------------------------------------------------------------------------
describe('JSONParserStream: arrays', () => {
it('accumulates an empty array', () => {
assert.deepEqual(accumulate('[]'), []);
});
it('accumulates a flat array of numbers', () => {
assert.deepEqual(accumulate('[1,2,3]'), [1, 2, 3]);
});
it('accumulates an array of strings', () => {
assert.deepEqual(accumulate('["a","b","c"]'), ['a', 'b', 'c']);
});
it('accumulates a nested array', () => {
assert.deepEqual(accumulate('[[1,2],[3]]'), [[1, 2], [3]]);
});
it('accumulates an array with mixed types', () => {
assert.deepEqual(accumulate('[1,"two",true,null]'), [1, 'two', true, null]);
});
});
// ---------------------------------------------------------------------------
// Whitespace handling
// ---------------------------------------------------------------------------
describe('JSONParserStream: whitespace', () => {
it('ignores leading whitespace', () => {
assert.equal(accumulate(' 42'), 42);
});
it('ignores whitespace around object braces', () => {
assert.deepEqual(accumulate('{ "a" : 1 }'), { a: 1 });
});
it('handles newlines and tabs', () => {
assert.deepEqual(accumulate('{\n\t"k":\n\t1\n}'), { k: 1 });
});
});
// ---------------------------------------------------------------------------
// Error handling
// ---------------------------------------------------------------------------
describe('JSONParserStream: error handling', () => {
it('throws a SyntaxError for invalid JSON by default', () => {
assert.throws(() => parse('{invalid}'), SyntaxError);
});
it('does not throw and returns events when swallowErrors is true', () => {
// With swallowErrors, parse should not throw even on invalid JSON
let threw = false;
try {
parse('{invalid}', { swallowErrors: true });
} catch (err) {
threw = true;
}
assert.equal(threw, false);
});
it('handles abrupt termination when expectAbruptTerminationOfInput is true', () => {
const evs = parse('{"key":"val', { expectAbruptTerminationOfInput: true });
const types = evs.map((e: any) => e.event);
assert.ok(types.includes('nodeEnd'));
});
});
// ---------------------------------------------------------------------------
// Contaminated input
// ---------------------------------------------------------------------------
describe('JSONParserStream: expectContaminated', () => {
it('extracts JSON from contaminated object context (preamble text before {)', () => {
const evs = parse('some preamble {"a":1}', { expectContaminated: 'object' });
const acc = JSONAccumulation.parse(evs as any);
assert.deepEqual(acc, { a: 1 });
});
it('extracts JSON from contaminated array context', () => {
const evs = parse('noise [1,2,3]', { expectContaminated: 'array' });
const acc = JSONAccumulation.parse(evs as any);
assert.deepEqual(acc, [1, 2, 3]);
});
});
// ---------------------------------------------------------------------------
// JSONAccumulation: focus path
// ---------------------------------------------------------------------------
describe('JSONAccumulation: focus path', () => {
it('focuses on a top-level key', () => {
const evs = parse('{"a":1,"b":2}');
assert.equal(JSONAccumulation.parse(evs as any, 'b'), 2);
});
it('focuses on a nested key', () => {
const evs = parse('{"x":{"y":{"z":99}}}');
assert.equal(JSONAccumulation.parse(evs as any, 'x.y.z'), 99);
});
it('focuses on an array element', () => {
const evs = parse('[10,20,30]');
assert.equal(JSONAccumulation.parse(evs as any, '[1]'), 20);
});
it('returns undefined for a non-existent focus path', () => {
const evs = parse('{"a":1}');
assert.equal(JSONAccumulation.parse(evs as any, 'nonexistent'), undefined);
});
it('returns the full value when focus is empty string', () => {
const evs = parse('{"k":"v"}');
assert.deepEqual(JSONAccumulation.parse(evs as any, ''), { k: 'v' });
});
});
// ---------------------------------------------------------------------------
// Offset metadata
// ---------------------------------------------------------------------------
describe('JSONParserStream: offset metadata', () => {
it('each event has a numeric offset field', () => {
const evs = parse('"hi"');
for (const ev of evs as any[]) {
if (ev.event === 'raw') continue;
assert.equal(typeof ev.offset, 'number');
}
});
it('each event has a numeric byteOffset field', () => {
const evs = parse('"hi"');
for (const ev of evs as any[]) {
if (ev.event === 'raw') continue;
assert.equal(typeof ev.byteOffset, 'number');
}
});
});