shape.test.ts
1 import { describe, expect, it } from 'vitest'; 2 import { inferShape } from './shape.js'; 3 4 describe('inferShape', () => { 5 it('describes primitives at root', () => { 6 expect(inferShape('hello')).toEqual({ $: 'string' }); 7 expect(inferShape(42)).toEqual({ $: 'number' }); 8 expect(inferShape(true)).toEqual({ $: 'boolean' }); 9 expect(inferShape(null)).toEqual({ $: 'null' }); 10 }); 11 12 it('summarizes long strings with their length', () => { 13 const long = 'x'.repeat(200); 14 expect(inferShape(long, { sampleStringLen: 80 })).toEqual({ $: 'string(len=200)' }); 15 }); 16 17 it('walks nested objects and emits dotted paths', () => { 18 const shape = inferShape({ user: { id: 1, name: 'bob' } }); 19 expect(shape).toEqual({ 20 $: 'object', 21 '$.user': 'object', 22 '$.user.id': 'number', 23 '$.user.name': 'string', 24 }); 25 }); 26 27 it('quotes unsafe keys using bracket notation', () => { 28 const shape = inferShape({ 'weird key': 1, '123bad': 2 }); 29 expect(shape['$["weird key"]']).toBe('number'); 30 expect(shape['$["123bad"]']).toBe('number'); 31 }); 32 33 it('samples the first array element and reports length', () => { 34 const shape = inferShape({ items: [{ a: 1 }, { a: 2 }, { a: 3 }] }); 35 expect(shape['$.items']).toBe('array(3)'); 36 expect(shape['$.items[0]']).toBe('object'); 37 expect(shape['$.items[0].a']).toBe('number'); 38 }); 39 40 it('marks empty containers explicitly', () => { 41 const shape = inferShape({ arr: [], obj: {} }); 42 expect(shape['$.arr']).toBe('array(0)'); 43 expect(shape['$.obj']).toBe('object(empty)'); 44 }); 45 46 it('collapses subtrees past maxDepth', () => { 47 const deep = { a: { b: { c: { d: { e: { f: 'too deep' } } } } } }; 48 const shape = inferShape(deep, { maxDepth: 2 }); 49 expect(shape['$.a.b']).toMatch(/^object/); 50 expect(shape['$.a.b.c']).toBeUndefined(); 51 }); 52 53 it('truncates when the byte budget is exhausted', () => { 54 const wide: Record<string, unknown> = {}; 55 for (let i = 0; i < 500; i++) wide[`field_${i}`] = i; 56 const shape = inferShape(wide, { maxBytes: 256 }); 57 expect(shape['(truncated)']).toMatch(/256B/); 58 expect(Object.keys(shape).length).toBeLessThan(500); 59 }); 60 61 it('stops descending into an array once the budget is hit by its own descriptor', () => { 62 // Budget just large enough for `$` + one deep array descriptor, not its element. 63 const shape = inferShape({ items: [{ deep: 1 }] }, { maxBytes: 40 }); 64 expect(shape['$.items[0]']).toBeUndefined(); 65 expect(shape['(truncated)']).toBeDefined(); 66 }); 67 68 it('handles the Twitter UserTweets payload envelope', () => { 69 const payload = { 70 data: { 71 user: { 72 result: { 73 rest_id: '42', 74 timeline_v2: { 75 timeline: { 76 instructions: [ 77 { type: 'TimelinePinEntry', entries: [] }, 78 { entries: [{ entryId: 'tweet-1', content: { entryType: 'TimelineTimelineItem' } }] }, 79 ], 80 }, 81 }, 82 }, 83 }, 84 }, 85 }; 86 const shape = inferShape(payload, { maxDepth: 10 }); 87 expect(shape['$.data.user.result.rest_id']).toBe('string'); 88 expect(shape['$.data.user.result.timeline_v2.timeline.instructions']).toBe('array(2)'); 89 expect(shape['$.data.user.result.timeline_v2.timeline.instructions[0]']).toBe('object'); 90 }); 91 });