import { describe, expect, it } from 'vitest' import type { Applog } from './datom-types.ts' import { compareApplogsByTs, isLaterByTsAndPv, sortApplogsByTs } from './applog-utils.ts' const mkLog = (overrides: Partial): Applog => ({ cid: 'cid-default' as any, en: 'e1' as any, at: 'name', vl: 'value', ts: '2026-04-25T00:00:00.000Z', pv: null, ag: 'agent' as any, ...overrides, }) describe('compareApplogsByTs', () => { it('orders by ts asc then by cid for tiebreak (transitive)', () => { const a = mkLog({ cid: 'cid-a' as any, ts: '2026-04-25T00:00:00.000Z' }) const b = mkLog({ cid: 'cid-b' as any, ts: '2026-04-25T00:00:00.000Z' }) const c = mkLog({ cid: 'cid-c' as any, ts: '2026-04-25T00:00:00.001Z' }) const sorted = [c, b, a].slice().sort((x, y) => compareApplogsByTs(x, y)) expect(sorted.map(l => l.cid)).toEqual(['cid-a', 'cid-b', 'cid-c']) }) }) describe('sortApplogsByTs — chain stabilization', () => { const sameTs = '2026-04-25T00:00:00.000Z' it('topologically orders 3 same-ts chain logs regardless of input order', () => { // Chain: A → B → C (same en, at, ts) const a = mkLog({ cid: 'aaa' as any, pv: null, ts: sameTs }) const b = mkLog({ cid: 'bbb' as any, pv: 'aaa' as any, ts: sameTs }) const c = mkLog({ cid: 'ccc' as any, pv: 'bbb' as any, ts: sameTs }) // Note: cid lex order is a, b, c — so for asc, the cid-only sort would coincidentally // produce the right order. To prove chain awareness, use cids whose lex order differs. const z = mkLog({ cid: 'zzz' as any, pv: null, ts: sameTs }) const y = mkLog({ cid: 'yyy' as any, pv: 'zzz' as any, ts: sameTs }) const x = mkLog({ cid: 'xxx' as any, pv: 'yyy' as any, ts: sameTs }) // asc: chain root first → z, y, x const ascOrder = sortApplogsByTs([x, y, z].slice()) expect(ascOrder.map(l => l.cid)).toEqual(['zzz', 'yyy', 'xxx']) // scrambled const scrambled = sortApplogsByTs([y, z, x].slice()) expect(scrambled.map(l => l.cid)).toEqual(['zzz', 'yyy', 'xxx']) // desc: chain tail first → x, y, z const descOrder = sortApplogsByTs([z, x, y].slice(), 'desc') expect(descOrder.map(l => l.cid)).toEqual(['xxx', 'yyy', 'zzz']) }) it('does not reshuffle different (en, at) sub-groups within a same-ts cluster', () => { // Same ts, two sub-groups, each with a chain const a1 = mkLog({ cid: 'a1' as any, en: 'e1' as any, at: 'name', pv: null, ts: sameTs }) const a2 = mkLog({ cid: 'a2' as any, en: 'e1' as any, at: 'name', pv: 'a1' as any, ts: sameTs }) const b1 = mkLog({ cid: 'b1' as any, en: 'e2' as any, at: 'name', pv: null, ts: sameTs }) const b2 = mkLog({ cid: 'b2' as any, en: 'e2' as any, at: 'name', pv: 'b1' as any, ts: sameTs }) const sorted = sortApplogsByTs([a2, b2, a1, b1].slice()) // Each (en, at) sub-group must be in chain order. Cross-group order = cid-lex (preserved). const e1Logs = sorted.filter(l => l.en === 'e1') const e2Logs = sorted.filter(l => l.en === 'e2') expect(e1Logs.map(l => l.cid)).toEqual(['a1', 'a2']) expect(e2Logs.map(l => l.cid)).toEqual(['b1', 'b2']) }) it('falls back to cid-lex order for non-chain-related same-ts logs', () => { const x = mkLog({ cid: 'xxx' as any, pv: null, ts: sameTs }) const y = mkLog({ cid: 'yyy' as any, pv: null, ts: sameTs }) const sorted = sortApplogsByTs([y, x].slice()) expect(sorted.map(l => l.cid)).toEqual(['xxx', 'yyy']) }) }) describe('isLaterByTsAndPv', () => { const sameTs = '2026-04-25T00:00:00.000Z' it('uses ts when ts differs', () => { const earlier = mkLog({ cid: 'a' as any, ts: '2026-04-25T00:00:00.000Z' }) const later = mkLog({ cid: 'b' as any, ts: '2026-04-25T00:00:00.001Z' }) expect(isLaterByTsAndPv(later, earlier)).toBe(true) expect(isLaterByTsAndPv(earlier, later)).toBe(false) }) it('uses pv chain for same-ts same (en, at) logs', () => { const a = mkLog({ cid: 'a' as any, pv: null, ts: sameTs }) const b = mkLog({ cid: 'b' as any, pv: 'a' as any, ts: sameTs }) expect(isLaterByTsAndPv(b, a)).toBe(true) // b chains after a expect(isLaterByTsAndPv(a, b)).toBe(false) }) it('falls back to cid-lex for same-ts unrelated logs', () => { const x = mkLog({ cid: 'x' as any, pv: null, ts: sameTs }) const y = mkLog({ cid: 'y' as any, pv: null, ts: sameTs }) expect(isLaterByTsAndPv(y, x)).toBe(true) expect(isLaterByTsAndPv(x, y)).toBe(false) }) it('does not use pv when (en, at) differs', () => { // Edge case: cid coincidence across keys must not be interpreted as a chain link const a = mkLog({ cid: 'shared' as any, en: 'e1' as any, at: 'name', pv: null, ts: sameTs }) const b = mkLog({ cid: 'other' as any, en: 'e2' as any, at: 'name', pv: 'shared' as any, ts: sameTs }) // Same-ts, different (en, at) — should fall through to cid-lex expect(isLaterByTsAndPv(b, a)).toBe('other'.localeCompare('shared') > 0) }) })