import { describe, it, expect } from 'vitest'; import { buildEventLookup, splitYearLetters, CLUSTER_PREVIEW } from './eventClustering'; import { makeEntry } from './test-factories'; import type { components } from '$lib/generated/api'; type TimelineDTO = components['schemas']['TimelineDTO']; type TimelineEntryDTO = components['schemas']['TimelineEntryDTO']; const EV_A = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'; const EV_B = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'; const makeEvent = (overrides: Partial = {}): TimelineEntryDTO => makeEntry({ kind: 'EVENT', type: 'PERSONAL', documentId: undefined, ...overrides }); describe('eventClustering — buildEventLookup', () => { it('maps a curated year-band event to its title, excluding undated-bucket events (#7)', () => { const timeline: TimelineDTO = { years: [ { year: 1916, entries: [makeEvent({ eventId: EV_A, title: 'Ein gewaltiger Stadtbrand' })] } ], undated: [makeEvent({ eventId: EV_B, title: 'Briefe von der Front' })] }; const lookup = buildEventLookup(timeline); expect(lookup.get(EV_A)).toBe('Ein gewaltiger Stadtbrand'); // An undated event renders as a plain pill in the undated bucket — out of clustering // scope. Including it here would scatter its dated letters into orphaned ✉ cross-year // cards detached from the pill (#7), so it must NOT enter the lookup. expect(lookup.has(EV_B)).toBe(false); expect(lookup.size).toBe(1); }); it('ignores derived events (no eventId) and letters', () => { const timeline: TimelineDTO = { years: [ { year: 1916, entries: [ makeEvent({ eventId: undefined, title: 'Geburt' }), // derived makeEntry({ kind: 'LETTER', documentId: 'doc-1' }) ] } ], undated: [] }; expect(buildEventLookup(timeline).size).toBe(0); }); it('excludes a HISTORICAL event so its letters stay loose, keeping its WorldBand (REQ-014)', () => { const timeline: TimelineDTO = { years: [ { year: 1916, entries: [makeEvent({ eventId: EV_A, type: 'HISTORICAL', title: 'Somme' })] } ], undated: [] }; const lookup = buildEventLookup(timeline); expect(lookup.has(EV_A)).toBe(false); expect(lookup.size).toBe(0); }); }); describe('eventClustering — splitYearLetters', () => { it('exposes a CLUSTER_PREVIEW of 5', () => { expect(CLUSTER_PREVIEW).toBe(5); }); it('clusters letters by linkedEventId with matching counts', () => { const lookup = new Map([[EV_A, 'Stadtbrand']]); const letters = [ makeEntry({ kind: 'LETTER', documentId: 'd1', linkedEventId: EV_A }), makeEntry({ kind: 'LETTER', documentId: 'd2', linkedEventId: EV_A }) ]; const { clusters, loose } = splitYearLetters(letters, lookup); expect(clusters).toHaveLength(1); expect(clusters[0].eventId).toBe(EV_A); expect(clusters[0].title).toBe('Stadtbrand'); expect(clusters[0].letters).toHaveLength(2); expect(loose).toHaveLength(0); }); it('keeps a letter with no linkedEventId loose', () => { const lookup = new Map([[EV_A, 'Stadtbrand']]); const letters = [makeEntry({ kind: 'LETTER', documentId: 'd1', linkedEventId: undefined })]; const { clusters, loose } = splitYearLetters(letters, lookup); expect(clusters).toHaveLength(0); expect(loose).toHaveLength(1); }); it('keeps a letter whose linkedEventId is absent from the lookup loose (filter-then-cluster, REQ-008)', () => { const lookup = new Map([[EV_A, 'Stadtbrand']]); const letters = [makeEntry({ kind: 'LETTER', documentId: 'd1', linkedEventId: EV_B })]; const { clusters, loose } = splitYearLetters(letters, lookup); expect(clusters).toHaveLength(0); expect(loose).toHaveLength(1); }); it('places each letter in exactly one place (REQ-007)', () => { const lookup = new Map([[EV_A, 'Stadtbrand']]); const letters = [ makeEntry({ kind: 'LETTER', documentId: 'd1', linkedEventId: EV_A }), makeEntry({ kind: 'LETTER', documentId: 'd2', linkedEventId: undefined }), makeEntry({ kind: 'LETTER', documentId: 'd3', linkedEventId: EV_B }) ]; const { clusters, loose } = splitYearLetters(letters, lookup); const clustered = clusters.flatMap((c) => c.letters.length).reduce((a, b) => a + b, 0); expect(clustered + loose.length).toBe(3); expect(clustered).toBe(1); expect(loose).toHaveLength(2); }); it('keeps clusters in first-seen order', () => { const lookup = new Map([ [EV_B, 'Front'], [EV_A, 'Stadtbrand'] ]); const letters = [ makeEntry({ kind: 'LETTER', documentId: 'd1', linkedEventId: EV_A }), makeEntry({ kind: 'LETTER', documentId: 'd2', linkedEventId: EV_B }) ]; const { clusters } = splitYearLetters(letters, lookup); expect(clusters.map((c) => c.eventId)).toEqual([EV_A, EV_B]); }); });