/** * Unit tests for rank.ts — fractional-indexing helpers (D-13). * * Pure function tests, no DB needed. * Run: pnpm --filter @familysync/api exec vitest run tests/lib/rank.test.ts */ import { describe, it, expect } from 'vitest' import { rankForAppend, rankBetween } from '../../src/lib/rank.js' describe('rankForAppend', () => { it('returns "a0" when list is empty (no existing rank)', () => { const rank = rankForAppend(null) expect(rank).toBe('a0') }) it('returns a rank string that sorts AFTER the given last rank', () => { const lastRank = 'a0' const newRank = rankForAppend(lastRank) expect(newRank > lastRank).toBe(true) }) it('multiple appends produce strictly increasing ranks', () => { let last: string | null = null const ranks: string[] = [] for (let i = 0; i < 5; i++) { const r = rankForAppend(last) ranks.push(r) last = r } // Each successive rank must be greater than the previous for (let i = 1; i < ranks.length; i++) { expect(ranks[i] > ranks[i - 1]).toBe(true) } }) }) describe('rankBetween', () => { it('returns "a0" when both prev and next are null (empty list)', () => { const rank = rankBetween(null, null) expect(rank).toBe('a0') }) it('returns a rank that sorts between two existing ranks', () => { const first = rankForAppend(null) // 'a0' const second = rankForAppend(first) // 'a1' const between = rankBetween(first, second) expect(between > first).toBe(true) expect(between < second).toBe(true) }) it('returns a rank that sorts AFTER prev when next is null', () => { const prev = 'a0' const rank = rankBetween(prev, null) expect(rank > prev).toBe(true) }) it('returns a rank that sorts BEFORE next when prev is null', () => { const next = 'a1' const rank = rankBetween(null, next) expect(rank < next).toBe(true) }) it('produces lexicographically stable ordering across multiple insertions', () => { // Simulate inserting between 'a0' and 'a1' repeatedly const a = 'a0' const b = 'a1' const c = rankBetween(a, b) const d = rankBetween(a, c) const e = rankBetween(c, b) // All four ranks should be orderable expect(a < d).toBe(true) expect(d < c).toBe(true) expect(c < e).toBe(true) expect(e < b).toBe(true) }) /** * Precision regression test — LIST-03, Pitfall 2. * * Repeated "zipper" inserts (always between the first two items) must produce * strictly increasing, unique, non-empty rank strings across many iterations. * Float-based approaches would exhaust precision around iteration 52; the * fractional-indexing string approach degrades gracefully by growing string * length instead. */ it('repeated mid-point inserts produce unique strictly-increasing ranks over 100 iterations (precision — Pitfall 2)', () => { const ranks: string[] = [rankBetween(null, null), rankBetween(null, null)] // Set up: two items with known ranks ranks[0] = rankBetween(null, null) // 'a0' ranks[1] = rankBetween(ranks[0], null) // 'a1' // Insert 100 times between the first item and the second item // This is the worst-case "zipper" pattern — always inserting at the same gap for (let i = 0; i < 100; i++) { const newRank = rankBetween(ranks[0], ranks[1]) // Must be strictly between expect(newRank > ranks[0]).toBe(true) expect(newRank < ranks[1]).toBe(true) // Must be a non-empty string expect(newRank.length).toBeGreaterThan(0) // Must be unique (not equal to any existing rank) expect(ranks).not.toContain(newRank) // New item goes at index 1 (after first, before old second) — shift old items ranks.splice(1, 0, newRank) } // Verify the final list is fully sorted ASC for (let i = 1; i < ranks.length; i++) { expect(ranks[i] > ranks[i - 1]).toBe(true) } }) it('rank between two neighbors is strictly between them (D-13 reorder contract)', () => { const prev = 'a0' const next = 'a3' const middle = rankBetween(prev, next) expect(middle > prev).toBe(true) expect(middle < next).toBe(true) }) })