From 24b3e3e0dcb602c5dafac8d3f83036c218d3ea6d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 20:02:14 +0000 Subject: [PATCH 1/3] chore: add temporary bug verification tests Agent-Logs-Url: https://github.com/ZON-Format/zon-TS/sessions/e5aab0b1-0f14-4a0f-bb3d-279a7a7bf62b Co-authored-by: ronibhakta1 <77425964+ronibhakta1@users.noreply.github.com> --- package-lock.json | 4 +- src/__tests__/test_bugs.test.ts | 70 +++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/test_bugs.test.ts diff --git a/package-lock.json b/package-lock.json index 617113a..c3309c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "zon-format", - "version": "1.3.0", + "version": "1.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "zon-format", - "version": "1.3.0", + "version": "1.3.1", "license": "MIT", "dependencies": { "@azure/openai": "^2.0.0", diff --git a/src/__tests__/test_bugs.test.ts b/src/__tests__/test_bugs.test.ts new file mode 100644 index 0000000..0b45c24 --- /dev/null +++ b/src/__tests__/test_bugs.test.ts @@ -0,0 +1,70 @@ +import { encode, decode } from '../index'; + +// Bug 1: Boolean-like keys in inline ZON objects +test('Inline object with f key round-trips', () => { + const data = {"f": 1}; + const encoded = encode(data); + console.log('Bug1 Encoded:', JSON.stringify(encoded)); + const decoded = decode(encoded); + console.log('Bug1 Decoded:', JSON.stringify(decoded)); + // This should be {"f": 1} not {false: 1} + expect(decoded).toEqual({"f": 1}); + expect(Object.keys(decoded as any)[0]).toBe('f'); +}); + +test('Inline nested object with f key round-trips', () => { + const data = {"a": {"b": {"c": {"d": {"e": {"f": 1}}}}}}; + const encoded = encode(data); + console.log('Bug1 nested Encoded:', JSON.stringify(encoded)); + const decoded = decode(encoded) as any; + console.log('Bug1 nested Decoded:', JSON.stringify(decoded)); + const inner = decoded?.a?.b?.c?.d?.e; + expect(inner).toBeDefined(); + expect(Object.keys(inner)).toContain('f'); + expect(Object.keys(inner)).not.toContain('false'); +}); + +test('Object with null/none/nil keys round-trips', () => { + const data = {"null": 1, "none": 2, "nil": 3}; + const encoded = encode(data); + console.log('null-key Encoded:', JSON.stringify(encoded)); + const decoded = decode(encoded) as any; + console.log('null-key Decoded:', JSON.stringify(decoded)); + expect(decoded).toHaveProperty('null'); + expect(decoded).toHaveProperty('none'); + expect(decoded).toHaveProperty('nil'); +}); + +// Bug 2: Dictionary header values with commas +test('Dictionary values with commas round-trip', () => { + const data = [ + {"Car": "Toyota Celica ST 185", "Year": "1996"}, + {"Car": "Toyota Corolla, AWD", "Year": "1997"}, + {"Car": "Toyota Corolla, AWD", "Year": "1998"}, + {"Car": "Toyota Corolla, AWD", "Year": "1999"}, + {"Car": "Toyota Corolla, AWD", "Year": "2000"}, + {"Car": "Toyota Corolla, AWD", "Year": "2001"}, + ]; + const encoded = encode(data); + console.log('Bug2 Encoded:', encoded); + const decoded = decode(encoded) as any[]; + console.log('Bug2 Decoded:', JSON.stringify(decoded[1])); + expect(decoded[1].Car).toBe("Toyota Corolla, AWD"); + expect(decoded[1].Year).toBe("1997"); +}); + +test('Dictionary values with colons round-trip', () => { + const data = [ + {"key": "value:one", "other": "a"}, + {"key": "value:one", "other": "b"}, + {"key": "value:one", "other": "c"}, + {"key": "value:one", "other": "d"}, + {"key": "value:one", "other": "e"}, + {"key": "value:two", "other": "f"}, + ]; + const encoded = encode(data); + console.log('Bug2b Encoded:', encoded); + const decoded = decode(encoded) as any[]; + console.log('Bug2b Decoded:', JSON.stringify(decoded[0])); + expect(decoded[0].key).toBe("value:one"); +}); From e60b76d066366f1ab9d568fc0bef9ee164bc8d57 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 23 Apr 2026 20:05:09 +0000 Subject: [PATCH 2/3] fix: port three round-trip bug fixes from Python ZON PR#9 to TypeScript - Fix 1 (encoder): Quote boolean/null keyword keys (t, f, true, false, null, none, nil) in _formatZonNode to prevent round-trip corruption - Fix 2 (decoder): Add parseKey() that always preserves keys as strings, replacing parseValue() at all key-parsing call sites - Fix 3 (encoder): Quote dictionary header values that contain commas or other special characters using _formatValue() - Fix 4 (decoder): Use _splitByDelimiter() + _parseValue() for dictionary header value parsing instead of naive vals.split(',') - Add tests: boolean-keys.test.ts and dict-header-quoting.test.ts Agent-Logs-Url: https://github.com/ZON-Format/zon-TS/sessions/e5aab0b1-0f14-4a0f-bb3d-279a7a7bf62b Co-authored-by: ronibhakta1 <77425964+ronibhakta1@users.noreply.github.com> --- src/__tests__/boolean-keys.test.ts | 109 ++++++++++++++++++++++ src/__tests__/dict-header-quoting.test.ts | 59 ++++++++++++ src/__tests__/test_bugs.test.ts | 70 -------------- src/core/decoder.ts | 16 ++-- src/core/encoder.ts | 7 +- src/core/utils.ts | 35 +++++++ 6 files changed, 218 insertions(+), 78 deletions(-) create mode 100644 src/__tests__/boolean-keys.test.ts create mode 100644 src/__tests__/dict-header-quoting.test.ts delete mode 100644 src/__tests__/test_bugs.test.ts diff --git a/src/__tests__/boolean-keys.test.ts b/src/__tests__/boolean-keys.test.ts new file mode 100644 index 0000000..0556660 --- /dev/null +++ b/src/__tests__/boolean-keys.test.ts @@ -0,0 +1,109 @@ +/** + * Tests for boolean-like dictionary keys. + * + * Verifies the fix for a bug where dictionary keys like "f", "t", + * "true", "false", "null" were incorrectly parsed as boolean/null values + * instead of being preserved as strings. + */ + +import { encode, decode } from '../index'; + +describe('Boolean-like dictionary keys', () => { + test('Key "f" should not become false', () => { + const data = { f: 1 }; + const encoded = encode(data); + const decoded = decode(encoded) as any; + expect(decoded).toEqual({ f: 1 }); + expect(Object.keys(decoded)).toContain('f'); + expect(Object.keys(decoded)).not.toContain('false'); + }); + + test('Key "t" should not become true', () => { + const data = { t: 1 }; + const encoded = encode(data); + const decoded = decode(encoded) as any; + expect(decoded).toEqual({ t: 1 }); + expect(Object.keys(decoded)).toContain('t'); + expect(Object.keys(decoded)).not.toContain('true'); + }); + + test('Key "true" should not become boolean true', () => { + const data = { true: 1 }; + const encoded = encode(data); + const decoded = decode(encoded) as any; + expect(decoded).toEqual({ true: 1 }); + expect(Object.keys(decoded)).toContain('true'); + }); + + test('Key "false" should not become boolean false', () => { + const data = { false: 1 }; + const encoded = encode(data); + const decoded = decode(encoded) as any; + expect(decoded).toEqual({ false: 1 }); + expect(Object.keys(decoded)).toContain('false'); + }); + + test('Key "null" should not become null', () => { + const data = { null: 1 }; + const encoded = encode(data); + const decoded = decode(encoded) as any; + expect(decoded).toEqual({ null: 1 }); + expect(Object.keys(decoded)).toContain('null'); + }); + + test('Key "none" should not become null', () => { + const data = { none: 1 }; + const encoded = encode(data); + const decoded = decode(encoded) as any; + expect(decoded).toEqual({ none: 1 }); + expect(Object.keys(decoded)).toContain('none'); + }); + + test('Key "nil" should not become null', () => { + const data = { nil: 1 }; + const encoded = encode(data); + const decoded = decode(encoded) as any; + expect(decoded).toEqual({ nil: 1 }); + expect(Object.keys(decoded)).toContain('nil'); + }); + + test('Case variants of boolean-like keys are preserved', () => { + const cases = [ + { F: 1 }, + { T: 1 }, + { True: 1 }, + { False: 1 }, + { TRUE: 1 }, + { FALSE: 1 }, + { NULL: 1 }, + { NONE: 1 }, + { Null: 1 }, + ]; + for (const data of cases) { + const encoded = encode(data); + const decoded = decode(encoded) as any; + expect(decoded).toEqual(data); + } + }); + + test('Multiple boolean-like keys in same object', () => { + const data = { t: 1, f: 2, true: 3, false: 4, null: 5 }; + const encoded = encode(data); + const decoded = decode(encoded) as any; + expect(decoded.t).toBe(1); + expect(decoded.f).toBe(2); + expect(decoded.true).toBe(3); + expect(decoded.false).toBe(4); + expect(decoded.null).toBe(5); + }); + + test('Nested object with boolean-like key round-trips', () => { + const data = { a: { b: { c: { d: { e: { f: 1 } } } } } }; + const encoded = encode(data); + const decoded = decode(encoded) as any; + expect(decoded).toEqual(data); + const inner = decoded?.a?.b?.c?.d?.e; + expect(Object.keys(inner)).toContain('f'); + expect(Object.keys(inner)).not.toContain('false'); + }); +}); diff --git a/src/__tests__/dict-header-quoting.test.ts b/src/__tests__/dict-header-quoting.test.ts new file mode 100644 index 0000000..4db9b52 --- /dev/null +++ b/src/__tests__/dict-header-quoting.test.ts @@ -0,0 +1,59 @@ +/** + * Tests for dictionary header value quoting. + * + * Verifies the fix for a bug where dictionary compression header values + * containing commas or colons were truncated at the delimiter, causing + * incorrect round-trips. + */ + +import { encode, decode } from '../index'; + +describe('Dictionary header value quoting', () => { + test('Dictionary values with commas round-trip correctly', () => { + const data = [ + { Car: 'Toyota Celica ST 185', Year: '1996' }, + { Car: 'Toyota Corolla, AWD', Year: '1997' }, + { Car: 'Toyota Corolla, AWD', Year: '1998' }, + { Car: 'Toyota Corolla, AWD', Year: '1999' }, + { Car: 'Toyota Corolla, AWD', Year: '2000' }, + { Car: 'Toyota Corolla, AWD', Year: '2001' }, + ]; + const encoded = encode(data); + const decoded = decode(encoded) as any[]; + expect(decoded[0].Car).toBe('Toyota Celica ST 185'); + expect(decoded[1].Car).toBe('Toyota Corolla, AWD'); + expect(decoded[1].Year).toBe('1997'); + }); + + test('Dictionary values with colons round-trip correctly', () => { + const data = [ + { key: 'value:one', other: 'a' }, + { key: 'value:one', other: 'b' }, + { key: 'value:one', other: 'c' }, + { key: 'value:one', other: 'd' }, + { key: 'value:one', other: 'e' }, + { key: 'value:two', other: 'f' }, + ]; + const encoded = encode(data); + const decoded = decode(encoded) as any[]; + expect(decoded[0].key).toBe('value:one'); + expect(decoded[5].key).toBe('value:two'); + }); + + test('All rows preserved after dictionary decompression', () => { + const data = [ + { Car: 'Toyota Celica ST 185', Year: '1996' }, + { Car: 'Toyota Corolla, AWD', Year: '1997' }, + { Car: 'Toyota Corolla, AWD', Year: '1998' }, + { Car: 'Toyota Corolla, AWD', Year: '1999' }, + { Car: 'Toyota Corolla, AWD', Year: '2000' }, + { Car: 'Toyota Corolla, AWD', Year: '2001' }, + ]; + const encoded = encode(data); + const decoded = decode(encoded) as any[]; + expect(decoded).toHaveLength(data.length); + for (let i = 0; i < data.length; i++) { + expect(decoded[i]).toEqual(data[i]); + } + }); +}); diff --git a/src/__tests__/test_bugs.test.ts b/src/__tests__/test_bugs.test.ts deleted file mode 100644 index 0b45c24..0000000 --- a/src/__tests__/test_bugs.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { encode, decode } from '../index'; - -// Bug 1: Boolean-like keys in inline ZON objects -test('Inline object with f key round-trips', () => { - const data = {"f": 1}; - const encoded = encode(data); - console.log('Bug1 Encoded:', JSON.stringify(encoded)); - const decoded = decode(encoded); - console.log('Bug1 Decoded:', JSON.stringify(decoded)); - // This should be {"f": 1} not {false: 1} - expect(decoded).toEqual({"f": 1}); - expect(Object.keys(decoded as any)[0]).toBe('f'); -}); - -test('Inline nested object with f key round-trips', () => { - const data = {"a": {"b": {"c": {"d": {"e": {"f": 1}}}}}}; - const encoded = encode(data); - console.log('Bug1 nested Encoded:', JSON.stringify(encoded)); - const decoded = decode(encoded) as any; - console.log('Bug1 nested Decoded:', JSON.stringify(decoded)); - const inner = decoded?.a?.b?.c?.d?.e; - expect(inner).toBeDefined(); - expect(Object.keys(inner)).toContain('f'); - expect(Object.keys(inner)).not.toContain('false'); -}); - -test('Object with null/none/nil keys round-trips', () => { - const data = {"null": 1, "none": 2, "nil": 3}; - const encoded = encode(data); - console.log('null-key Encoded:', JSON.stringify(encoded)); - const decoded = decode(encoded) as any; - console.log('null-key Decoded:', JSON.stringify(decoded)); - expect(decoded).toHaveProperty('null'); - expect(decoded).toHaveProperty('none'); - expect(decoded).toHaveProperty('nil'); -}); - -// Bug 2: Dictionary header values with commas -test('Dictionary values with commas round-trip', () => { - const data = [ - {"Car": "Toyota Celica ST 185", "Year": "1996"}, - {"Car": "Toyota Corolla, AWD", "Year": "1997"}, - {"Car": "Toyota Corolla, AWD", "Year": "1998"}, - {"Car": "Toyota Corolla, AWD", "Year": "1999"}, - {"Car": "Toyota Corolla, AWD", "Year": "2000"}, - {"Car": "Toyota Corolla, AWD", "Year": "2001"}, - ]; - const encoded = encode(data); - console.log('Bug2 Encoded:', encoded); - const decoded = decode(encoded) as any[]; - console.log('Bug2 Decoded:', JSON.stringify(decoded[1])); - expect(decoded[1].Car).toBe("Toyota Corolla, AWD"); - expect(decoded[1].Year).toBe("1997"); -}); - -test('Dictionary values with colons round-trip', () => { - const data = [ - {"key": "value:one", "other": "a"}, - {"key": "value:one", "other": "b"}, - {"key": "value:one", "other": "c"}, - {"key": "value:one", "other": "d"}, - {"key": "value:one", "other": "e"}, - {"key": "value:two", "other": "f"}, - ]; - const encoded = encode(data); - console.log('Bug2b Encoded:', encoded); - const decoded = decode(encoded) as any[]; - console.log('Bug2b Decoded:', JSON.stringify(decoded[0])); - expect(decoded[0].key).toBe("value:one"); -}); diff --git a/src/core/decoder.ts b/src/core/decoder.ts index 40b5053..4de8857 100644 --- a/src/core/decoder.ts +++ b/src/core/decoder.ts @@ -1,6 +1,6 @@ import { TABLE_MARKER, META_SEPARATOR, MAX_DOCUMENT_SIZE, MAX_LINE_LENGTH, MAX_ARRAY_LENGTH, MAX_OBJECT_KEYS, MAX_NESTING_DEPTH } from './constants'; import { ZonDecodeError } from './exceptions'; -import { parseValue } from './utils'; +import { parseValue, parseKey } from './utils'; import { extractVersion, stripVersion, type ZonDocumentMetadata } from './versioning'; export interface DecodeOptions { @@ -94,7 +94,7 @@ export class ZonDecoder { const dictMatch = trimmedLine.match(/^([\w\.]+)\[(\d+)\]:(.+)$/); if (dictMatch && !trimmedLine.startsWith(TABLE_MARKER)) { const [, col, , vals] = dictMatch; - pendingDictionaries.set(col, vals.split(',')); + pendingDictionaries.set(col, this._splitByDelimiter(vals, ',').map(v => this._parseValue(v))); continue; } @@ -612,7 +612,7 @@ export class ZonDecoder { continue; } - const key = this._parseValue(keyStr); + const key = this._parseKey(keyStr); const val = this._parseZonNode(valStr, depth + 1); obj[key] = val; } @@ -745,7 +745,7 @@ export class ZonDecoder { // Handle implicit array item starting with dash in value? // No, _parseZonNode handles that recursively. - const key = this._parseValue(keyStr); + const key = this._parseKey(keyStr); const val = this._parseZonNode(valStr, depth + 1); obj[key] = val; } @@ -823,7 +823,7 @@ export class ZonDecoder { const colonIdx = this._findDelimiter(item, ':'); const keyStr = item.substring(0, colonIdx).trim(); const valStr = item.substring(colonIdx + 1).trim(); - const key = this._parseValue(keyStr); + const key = this._parseKey(keyStr); const val = this._parseZonNode(valStr, depth + 1); obj[key] = val; } @@ -856,7 +856,7 @@ export class ZonDecoder { const colonIdx = this._findDelimiter(line.trim(), ':'); const keyStr = line.substring(0, colonIdx).trim(); const valStr = line.substring(colonIdx + 1).trim(); - const key = this._parseValue(keyStr); + const key = this._parseKey(keyStr); const val = this._parseZonNode(valStr, depth + 1); obj[key] = val; } @@ -1043,6 +1043,10 @@ export class ZonDecoder { return parsed; } + private _parseKey(val: string): string { + return parseKey(val); + } + /** diff --git a/src/core/encoder.ts b/src/core/encoder.ts index 73e769c..0e59b07 100644 --- a/src/core/encoder.ts +++ b/src/core/encoder.ts @@ -449,7 +449,9 @@ export class ZonEncoder { const lines: string[] = []; for (const [col, values] of dictionaries) { - lines.push(`${col}[${values.length}]:${values.join(',')}`); + // Quote dictionary values that contain special characters + const formattedValues = values.map(v => this._formatValue(v)); + lines.push(`${col}[${values.length}]:${formattedValues.join(',')}`); } const dictCols = Array.from(dictionaries.keys()); @@ -628,7 +630,8 @@ export class ZonEncoder { const items: string[] = []; for (const k of keys) { let kStr = String(k); - if (/[,:\{\}\[\]"]/.test(kStr)) { + // Quote keys with special chars OR boolean/null keywords + if (/[,:\{\}\[\]"]/.test(kStr) || /^(true|false|t|f|null|none|nil)$/i.test(kStr)) { kStr = JSON.stringify(kStr); } diff --git a/src/core/utils.ts b/src/core/utils.ts index 512b133..d67f671 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -21,6 +21,41 @@ export function quoteString(s: string): string { return `"${zon}"`; } +/** + * Parses a ZON dictionary key string. + * + * Unlike parseValue, this does NOT convert boolean keywords (t, f, true, false) + * or null keywords to their typed equivalents. Keys are always strings. + * + * @param val - The key string to parse + * @returns The parsed key as a string + */ +export function parseKey(val: string): string { + const trimmed = val.trim(); + + if (trimmed.startsWith('"')) { + try { + return JSON.parse(trimmed); + } catch { + if (trimmed.endsWith('"')) { + const inner = trimmed.slice(1, -1); + const fixed = inner.replace(/""/g, '\\"'); + try { + return JSON.parse(`"${fixed}"`); + } catch { + return trimmed.slice(1, -1).replace(/""/g, '"'); + } + } + } + } + + if (trimmed.startsWith("'") && trimmed.endsWith("'")) { + return trimmed.slice(1, -1); + } + + return trimmed; +} + /** * Parses a ZON value string into its corresponding primitive type. * Handles booleans, nulls, numbers, arrays, objects, and quoted strings. From 0350521a84849ecec83d99e7c350548c4fe2692b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:22:37 +0000 Subject: [PATCH 3/3] test: update boolean-keys fixtures to use nested objects, exercising _formatZonNode/_parseKey paths Per coderabbitai review: top-level `{ f: 1 }` encoded as implicit KV (f:1) and bypassed the _formatZonNode / _parseKey code paths. Updated all fixtures to `{ wrapper: { keyword: 1 } }` so encode produces `wrapper{"keyword":1}`, going through the patched code. Also replaced a broken array-of-mixed-keys test with a multi-sibling-object test that encodes via _formatZonNode. Agent-Logs-Url: https://github.com/ZON-Format/zon-TS/sessions/d685654f-1fcf-4efb-892d-ca37bbc8657e Co-authored-by: ronibhakta1 <77425964+ronibhakta1@users.noreply.github.com> --- src/__tests__/boolean-keys.test.ts | 102 +++++++++++++++++------------ 1 file changed, 60 insertions(+), 42 deletions(-) diff --git a/src/__tests__/boolean-keys.test.ts b/src/__tests__/boolean-keys.test.ts index 0556660..6a63abc 100644 --- a/src/__tests__/boolean-keys.test.ts +++ b/src/__tests__/boolean-keys.test.ts @@ -4,97 +4,103 @@ * Verifies the fix for a bug where dictionary keys like "f", "t", * "true", "false", "null" were incorrectly parsed as boolean/null values * instead of being preserved as strings. + * + * Fixtures wrap the boolean-like key inside a nested object or array-of-objects + * so that encode/decode exercises _formatZonNode / _parseKey — the code paths + * that contain the actual fix. */ import { encode, decode } from '../index'; describe('Boolean-like dictionary keys', () => { test('Key "f" should not become false', () => { - const data = { f: 1 }; + const data = { wrapper: { f: 1 } }; const encoded = encode(data); const decoded = decode(encoded) as any; - expect(decoded).toEqual({ f: 1 }); - expect(Object.keys(decoded)).toContain('f'); - expect(Object.keys(decoded)).not.toContain('false'); + expect(decoded.wrapper).toEqual({ f: 1 }); + expect(Object.keys(decoded.wrapper)).toContain('f'); + expect(Object.keys(decoded.wrapper)).not.toContain('false'); }); test('Key "t" should not become true', () => { - const data = { t: 1 }; + const data = { wrapper: { t: 1 } }; const encoded = encode(data); const decoded = decode(encoded) as any; - expect(decoded).toEqual({ t: 1 }); - expect(Object.keys(decoded)).toContain('t'); - expect(Object.keys(decoded)).not.toContain('true'); + expect(decoded.wrapper).toEqual({ t: 1 }); + expect(Object.keys(decoded.wrapper)).toContain('t'); + expect(Object.keys(decoded.wrapper)).not.toContain('true'); }); test('Key "true" should not become boolean true', () => { - const data = { true: 1 }; + const data = { wrapper: { true: 1 } }; const encoded = encode(data); const decoded = decode(encoded) as any; - expect(decoded).toEqual({ true: 1 }); - expect(Object.keys(decoded)).toContain('true'); + expect(decoded.wrapper).toEqual({ true: 1 }); + expect(Object.keys(decoded.wrapper)).toContain('true'); }); test('Key "false" should not become boolean false', () => { - const data = { false: 1 }; + const data = { wrapper: { false: 1 } }; const encoded = encode(data); const decoded = decode(encoded) as any; - expect(decoded).toEqual({ false: 1 }); - expect(Object.keys(decoded)).toContain('false'); + expect(decoded.wrapper).toEqual({ false: 1 }); + expect(Object.keys(decoded.wrapper)).toContain('false'); }); test('Key "null" should not become null', () => { - const data = { null: 1 }; + const data = { wrapper: { null: 1 } }; const encoded = encode(data); const decoded = decode(encoded) as any; - expect(decoded).toEqual({ null: 1 }); - expect(Object.keys(decoded)).toContain('null'); + expect(decoded.wrapper).toEqual({ null: 1 }); + expect(Object.keys(decoded.wrapper)).toContain('null'); }); test('Key "none" should not become null', () => { - const data = { none: 1 }; + const data = { wrapper: { none: 1 } }; const encoded = encode(data); const decoded = decode(encoded) as any; - expect(decoded).toEqual({ none: 1 }); - expect(Object.keys(decoded)).toContain('none'); + expect(decoded.wrapper).toEqual({ none: 1 }); + expect(Object.keys(decoded.wrapper)).toContain('none'); }); test('Key "nil" should not become null', () => { - const data = { nil: 1 }; + const data = { wrapper: { nil: 1 } }; const encoded = encode(data); const decoded = decode(encoded) as any; - expect(decoded).toEqual({ nil: 1 }); - expect(Object.keys(decoded)).toContain('nil'); + expect(decoded.wrapper).toEqual({ nil: 1 }); + expect(Object.keys(decoded.wrapper)).toContain('nil'); }); test('Case variants of boolean-like keys are preserved', () => { - const cases = [ - { F: 1 }, - { T: 1 }, - { True: 1 }, - { False: 1 }, - { TRUE: 1 }, - { FALSE: 1 }, - { NULL: 1 }, - { NONE: 1 }, - { Null: 1 }, + const cases: Array<[string, Record]> = [ + ['F', { F: 1 }], + ['T', { T: 1 }], + ['True', { True: 1 }], + ['False', { False: 1 }], + ['TRUE', { TRUE: 1 }], + ['FALSE', { FALSE: 1 }], + ['NULL', { NULL: 1 }], + ['NONE', { NONE: 1 }], + ['Null', { Null: 1 }], ]; - for (const data of cases) { + for (const [keyName, inner] of cases) { + const data = { wrapper: inner }; const encoded = encode(data); const decoded = decode(encoded) as any; - expect(decoded).toEqual(data); + expect(Object.keys(decoded.wrapper)).toContain(keyName); + expect(decoded.wrapper[keyName]).toBe(1); } }); - test('Multiple boolean-like keys in same object', () => { - const data = { t: 1, f: 2, true: 3, false: 4, null: 5 }; + test('Multiple boolean-like keys in same nested object', () => { + const data = { wrapper: { t: 1, f: 2, true: 3, false: 4, null: 5 } }; const encoded = encode(data); const decoded = decode(encoded) as any; - expect(decoded.t).toBe(1); - expect(decoded.f).toBe(2); - expect(decoded.true).toBe(3); - expect(decoded.false).toBe(4); - expect(decoded.null).toBe(5); + expect(decoded.wrapper.t).toBe(1); + expect(decoded.wrapper.f).toBe(2); + expect(decoded.wrapper.true).toBe(3); + expect(decoded.wrapper.false).toBe(4); + expect(decoded.wrapper.null).toBe(5); }); test('Nested object with boolean-like key round-trips', () => { @@ -106,4 +112,16 @@ describe('Boolean-like dictionary keys', () => { expect(Object.keys(inner)).toContain('f'); expect(Object.keys(inner)).not.toContain('false'); }); + + test('Multiple sibling objects each with a boolean-like key round-trip', () => { + const data = { a: { f: 1 }, b: { t: 2 }, c: { null: 3 } }; + const encoded = encode(data); + const decoded = decode(encoded) as any; + expect(Object.keys(decoded.a)).toContain('f'); + expect(decoded.a.f).toBe(1); + expect(Object.keys(decoded.b)).toContain('t'); + expect(decoded.b.t).toBe(2); + expect(Object.keys(decoded.c)).toContain('null'); + expect(decoded.c.null).toBe(3); + }); });