|
| 1 | +import { describe, it, expect } from 'vitest'; |
| 2 | +import { |
| 3 | + orderSessionRowsByForkLineage, |
| 4 | + forkIndentPadding, |
| 5 | + FORK_INDENT_SIZE, |
| 6 | + FORK_MAX_VISUAL_DEPTH, |
| 7 | +} from './forkLineage'; |
| 8 | + |
| 9 | +type Row = { id: string; parentSessionId: string | null; forkDepth: number }; |
| 10 | +const row = (id: string, parentSessionId: string | null = null): Row => ({ id, parentSessionId, forkDepth: 0 }); |
| 11 | +const ids = (rows: Row[]) => rows.map(r => r.id); |
| 12 | +const depths = (rows: Row[]) => rows.map(r => r.forkDepth); |
| 13 | + |
| 14 | +describe('orderSessionRowsByForkLineage', () => { |
| 15 | + it('leaves a fork-free list in original order at depth 0', () => { |
| 16 | + const out = orderSessionRowsByForkLineage([row('a'), row('b'), row('c')]); |
| 17 | + expect(ids(out)).toEqual(['a', 'b', 'c']); |
| 18 | + expect(depths(out)).toEqual([0, 0, 0]); |
| 19 | + }); |
| 20 | + |
| 21 | + it('nests a child directly under its parent at depth 1', () => { |
| 22 | + // Input is newest-first: forked child 'b' sorts above its parent 'a'. |
| 23 | + const out = orderSessionRowsByForkLineage([row('b', 'a'), row('a'), row('c')]); |
| 24 | + expect(ids(out)).toEqual(['a', 'b', 'c']); |
| 25 | + expect(depths(out)).toEqual([0, 1, 0]); |
| 26 | + }); |
| 27 | + |
| 28 | + it('nests a multi-level fork chain with increasing depth', () => { |
| 29 | + const out = orderSessionRowsByForkLineage([row('c', 'b'), row('b', 'a'), row('a')]); |
| 30 | + expect(ids(out)).toEqual(['a', 'b', 'c']); |
| 31 | + expect(depths(out)).toEqual([0, 1, 2]); |
| 32 | + }); |
| 33 | + |
| 34 | + it('keeps a child at depth 0 when its parent is not in the same section', () => { |
| 35 | + const out = orderSessionRowsByForkLineage([row('b', 'parent-in-another-group'), row('c')]); |
| 36 | + expect(ids(out)).toEqual(['b', 'c']); |
| 37 | + expect(depths(out)).toEqual([0, 0]); |
| 38 | + }); |
| 39 | + |
| 40 | + it('places multiple children under one parent, preserving their order', () => { |
| 41 | + const out = orderSessionRowsByForkLineage([row('c1', 'p'), row('c2', 'p'), row('p')]); |
| 42 | + expect(ids(out)).toEqual(['p', 'c1', 'c2']); |
| 43 | + expect(depths(out)).toEqual([0, 1, 1]); |
| 44 | + }); |
| 45 | + |
| 46 | + it('does not loop or drop rows on a parent cycle', () => { |
| 47 | + // a -> b -> a, both present (pathological metadata). |
| 48 | + const out = orderSessionRowsByForkLineage([row('a', 'b'), row('b', 'a')]); |
| 49 | + expect(out).toHaveLength(2); |
| 50 | + expect(ids(out).sort()).toEqual(['a', 'b']); |
| 51 | + }); |
| 52 | + |
| 53 | + it('returns the same row reference when depth is unchanged (deep-equal stability)', () => { |
| 54 | + const a = row('a'); |
| 55 | + const b = row('b'); |
| 56 | + const out = orderSessionRowsByForkLineage([a, b]); |
| 57 | + expect(out[0]).toBe(a); |
| 58 | + expect(out[1]).toBe(b); |
| 59 | + }); |
| 60 | +}); |
| 61 | + |
| 62 | +describe('forkIndentPadding', () => { |
| 63 | + it('adds no indent at depth 0', () => { |
| 64 | + expect(forkIndentPadding(0, 16)).toBe(16); |
| 65 | + }); |
| 66 | + |
| 67 | + it('adds one indent step per level', () => { |
| 68 | + expect(forkIndentPadding(1, 16)).toBe(16 + FORK_INDENT_SIZE); |
| 69 | + expect(forkIndentPadding(2, 16)).toBe(16 + 2 * FORK_INDENT_SIZE); |
| 70 | + }); |
| 71 | + |
| 72 | + it('caps the visual indent at FORK_MAX_VISUAL_DEPTH', () => { |
| 73 | + expect(forkIndentPadding(99, 16)).toBe(16 + FORK_MAX_VISUAL_DEPTH * FORK_INDENT_SIZE); |
| 74 | + }); |
| 75 | +}); |
0 commit comments