Files
orca/src/shared/node-bounded-json-stringify.test.ts
T

110 lines
3.7 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
JsonStringifyByteLimitError,
stringifyJsonWithinByteLimit
} from './node-bounded-json-stringify'
describe('stringifyJsonWithinByteLimit', () => {
it('matches native JSON for nested values, escaping, and omitted fields', () => {
const shared = { label: 'same object' }
const value = {
text: 'quote " slash \\ control\n emoji 🐋 lone \ud800',
nested: [1, undefined, Number.NaN, { omitted: undefined, kept: true }],
repeated: [shared, shared]
}
const native = JSON.stringify(value)
const result = stringifyJsonWithinByteLimit(value, Buffer.byteLength(native))
expect(result.serialized).toBe(native)
expect(result.byteLength).toBe(Buffer.byteLength(native, 'utf8'))
expect(() => stringifyJsonWithinByteLimit(value, result.byteLength - 1)).toThrow(
JsonStringifyByteLimitError
)
})
it('matches native indented JSON and counts whitespace before materializing it', () => {
const value = { nested: [{ quote: '"', unicode: '🐋' }], empty: {} }
const native = JSON.stringify(value, null, 2)
expect(stringifyJsonWithinByteLimit(value, Buffer.byteLength(native), 2)).toEqual({
byteLength: Buffer.byteLength(native),
serialized: native
})
expect(() => stringifyJsonWithinByteLimit(value, Buffer.byteLength(native) - 1, 2)).toThrow(
JsonStringifyByteLimitError
)
})
it('matches native numeric and string indentation normalization', () => {
const value = { nested: { value: true } }
for (const space of [Number.NaN, -1, 20, '🐋'.repeat(6)]) {
const native = JSON.stringify(value, null, space)
expect(stringifyJsonWithinByteLimit(value, Buffer.byteLength(native), space)).toEqual({
byteLength: Buffer.byteLength(native),
serialized: native
})
}
})
it('stops visiting indented collections when whitespace crosses the limit', () => {
let visits = 0
const value = Array.from({ length: 10_000 }, () => ({
toJSON() {
visits += 1
return 1
}
}))
expect(() => stringifyJsonWithinByteLimit(value, 64, 2)).toThrow(JsonStringifyByteLimitError)
expect(visits).toBeLessThan(value.length)
})
it('measures values after toJSON without invoking it twice', () => {
let calls = 0
const value = {
toJSON() {
calls += 1
return { rendered: 'value' }
}
}
expect(stringifyJsonWithinByteLimit(value, 100).serialized).toBe('{"rendered":"value"}')
expect(calls).toBe(1)
})
it('rejects a large root string before materializing escaped JSON', () => {
const value = '\n'.repeat(1024 * 1024)
expect(() => stringifyJsonWithinByteLimit(value, 1024)).toThrow(JsonStringifyByteLimitError)
})
it('stops visiting a large collection as soon as it crosses the limit', () => {
let visits = 0
const value = Array.from({ length: 10_000 }, () => ({
toJSON() {
visits += 1
return 1
}
}))
expect(() => stringifyJsonWithinByteLimit(value, 64)).toThrow(JsonStringifyByteLimitError)
expect(visits).toBeLessThan(value.length)
})
it('measures raw JSON values before native serialization materializes them', () => {
const createRawJson = (JSON as { rawJSON?: (value: string) => unknown }).rawJSON
if (!createRawJson) {
return
}
const value = createRawJson(JSON.stringify('x'.repeat(1024)))
expect(() => stringifyJsonWithinByteLimit(value, 32)).toThrow(JsonStringifyByteLimitError)
})
it('rejects invalid limits and unserializable roots like native JSON', () => {
expect(() => stringifyJsonWithinByteLimit('value', -1)).toThrow(RangeError)
expect(() => stringifyJsonWithinByteLimit(undefined, 100)).toThrow(TypeError)
})
})