/ src / browser / shape.test.ts
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  });