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