transform.test.ts
1 /** 2 * Tests for pipeline transform steps: select, map, filter, sort, limit. 3 */ 4 5 import { describe, it, expect } from 'vitest'; 6 import { stepSelect, stepMap, stepFilter, stepSort, stepLimit } from './steps/transform.js'; 7 8 const SAMPLE_DATA = [ 9 { title: 'Alpha', score: 10, author: 'Alice' }, 10 { title: 'Beta', score: 30, author: 'Bob' }, 11 { title: 'Gamma', score: 20, author: 'Charlie' }, 12 ]; 13 14 describe('stepSelect', () => { 15 it('selects nested path', async () => { 16 const data = { result: { items: [1, 2, 3] } }; 17 const result = await stepSelect(null, 'result.items', data, {}); 18 expect(result).toEqual([1, 2, 3]); 19 }); 20 21 it('selects array by index', async () => { 22 const data = { list: ['a', 'b', 'c'] }; 23 const result = await stepSelect(null, 'list.1', data, {}); 24 expect(result).toBe('b'); 25 }); 26 27 it('returns null for missing path', async () => { 28 const result = await stepSelect(null, 'missing.path', { foo: 1 }, {}); 29 expect(result).toBeNull(); 30 }); 31 32 it('returns data as-is for non-object', async () => { 33 const result = await stepSelect(null, 'foo', 'string-data', {}); 34 expect(result).toBe('string-data'); 35 }); 36 }); 37 38 describe('stepMap', () => { 39 it('maps array items', async () => { 40 const result = await stepMap(null, { 41 name: '${{ item.title }}', 42 rank: '${{ index + 1 }}', 43 }, SAMPLE_DATA, {}); 44 expect(result).toEqual([ 45 { name: 'Alpha', rank: 1 }, 46 { name: 'Beta', rank: 2 }, 47 { name: 'Gamma', rank: 3 }, 48 ]); 49 }); 50 51 it('handles single object', async () => { 52 const result = await stepMap(null, { 53 name: '${{ item.title }}', 54 }, { title: 'Solo' }, {}); 55 expect(result).toEqual([{ name: 'Solo' }]); 56 }); 57 58 it('returns null/undefined as-is', async () => { 59 expect(await stepMap(null, { x: '${{ item.x }}' }, null, {})).toBeNull(); 60 }); 61 62 it('supports inline select before mapping', async () => { 63 const result = await stepMap(null, { 64 select: 'posts', 65 title: '${{ item.title }}', 66 rank: '${{ index + 1 }}', 67 }, { posts: [{ title: 'One' }, { title: 'Two' }] }, {}); 68 69 expect(result).toEqual([ 70 { title: 'One', rank: 1 }, 71 { title: 'Two', rank: 2 }, 72 ]); 73 }); 74 75 it('keeps data bound to the selected source and exposes root separately', async () => { 76 const result = await stepMap(null, { 77 select: 'bids', 78 bid_price: '${{ data[index][0] }}', 79 ask_price: '${{ root.asks[index][0] }}', 80 }, { 81 bids: [['100', '2'], ['99', '3']], 82 asks: [['101', '1'], ['102', '4']], 83 }, {}); 84 85 expect(result).toEqual([ 86 { bid_price: '100', ask_price: '101' }, 87 { bid_price: '99', ask_price: '102' }, 88 ]); 89 }); 90 }); 91 92 describe('stepFilter', () => { 93 it('filters by expression', async () => { 94 const result = await stepFilter(null, 'item.score', SAMPLE_DATA, {}); 95 expect(result).toHaveLength(3); // all truthy 96 }); 97 98 it('returns non-array as-is', async () => { 99 const result = await stepFilter(null, 'item.x', 'not-array', {}); 100 expect(result).toBe('not-array'); 101 }); 102 }); 103 104 describe('stepSort', () => { 105 it('sorts ascending by key', async () => { 106 const result = await stepSort(null, 'score', SAMPLE_DATA, {}); 107 expect((result as typeof SAMPLE_DATA).map((r) => r.title)).toEqual(['Alpha', 'Gamma', 'Beta']); 108 }); 109 110 it('sorts descending', async () => { 111 const result = await stepSort(null, { by: 'score', order: 'desc' }, SAMPLE_DATA, {}); 112 expect((result as typeof SAMPLE_DATA).map((r) => r.title)).toEqual(['Beta', 'Gamma', 'Alpha']); 113 }); 114 115 it('does not mutate original', async () => { 116 const original = [...SAMPLE_DATA]; 117 await stepSort(null, 'score', SAMPLE_DATA, {}); 118 expect(SAMPLE_DATA).toEqual(original); 119 }); 120 121 it('sorts string-encoded numbers naturally by default', async () => { 122 const data = [ 123 { name: 'A', volume: '99' }, 124 { name: 'B', volume: '1000' }, 125 { name: 'C', volume: '250' }, 126 ]; 127 const result = await stepSort(null, { by: 'volume', order: 'desc' }, data, {}); 128 expect((result as typeof data).map((r) => r.name)).toEqual(['B', 'C', 'A']); 129 }); 130 131 it('handles missing fields gracefully', async () => { 132 const data = [ 133 { name: 'A', value: '10' }, 134 { name: 'B' }, 135 { name: 'C', value: '5' }, 136 ]; 137 const result = await stepSort(null, { by: 'value', order: 'asc' }, data, {}); 138 expect((result as typeof data).map((r) => r.name)).toEqual(['B', 'C', 'A']); 139 }); 140 }); 141 142 describe('stepLimit', () => { 143 it('limits array to N items', async () => { 144 const result = await stepLimit(null, '2', SAMPLE_DATA, {}); 145 expect(result).toHaveLength(2); 146 }); 147 148 it('limits using template expression', async () => { 149 const result = await stepLimit(null, '${{ args.limit }}', SAMPLE_DATA, { limit: 1 }); 150 expect(result).toHaveLength(1); 151 }); 152 153 it('returns non-array as-is', async () => { 154 expect(await stepLimit(null, '5', 'string', {})).toBe('string'); 155 }); 156 });