import { describe, expect, it } from 'vitest' import type { Applog } from './datom-types.ts' import { compareApplogsByTs, deriveDeterministicEntityID, isLaterByTsAndPv, mintNewEntity, 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) }) }) describe('deriveDeterministicEntityID — natural-key identity', () => { it('is deterministic: same key ⇒ same id (write & read converge on one soul)', () => { const a = deriveDeterministicEntityID({ type: 'segment', mediaId: 'm1', startMs: 0, endMs: 100 }) const b = deriveDeterministicEntityID({ type: 'segment', mediaId: 'm1', startMs: 0, endMs: 100 }) expect(a).toBe(b) }) it('is stable across key property order (safe-stable-stringify)', () => { const a = deriveDeterministicEntityID({ mediaId: 'm1', type: 'segment' }) const b = deriveDeterministicEntityID({ type: 'segment', mediaId: 'm1' }) expect(a).toBe(b) }) it('different keys ⇒ different ids', () => { expect(deriveDeterministicEntityID('#rust')).not.toBe(deriveDeterministicEntityID('#typescript')) }) it('honors the requested length', () => { expect(deriveDeterministicEntityID('#rust', 7)).toHaveLength(7) expect(deriveDeterministicEntityID('#rust', 10)).toHaveLength(10) }) }) describe('mintNewEntity — occurrence identity', () => { it('re-minting the same logical seed with a fresh ts yields a DIFFERENT id (the re-mint trap)', () => { // Documents why an existing entity must be resolved & its `en` reused rather than // re-minted: a timestamp in the seed forks a new soul on every call. const first = mintNewEntity({ msgId: 42, ts: '2026-06-16T00:00:00.000Z' }) const editedLater = mintNewEntity({ msgId: 42, ts: '2026-06-16T00:00:01.000Z' }) expect(first).not.toBe(editedLater) }) it('is deterministic for an identical seed (same ts ⇒ same id)', () => { const seed = { msgId: 42, ts: '2026-06-16T00:00:00.000Z' } expect(mintNewEntity(seed)).toBe(mintNewEntity({ ...seed })) }) it('honors the requested length', () => { expect(mintNewEntity({ a: 1 }, 6)).toHaveLength(6) }) })