// Test logEvent normalizer — canonical shape for UI/download/replay/analyze. // New design: legacy logs (no type) DROPPED. Not in pipeline. Kept only for // human scroll in LogsView (raw). download/copy/replay/analyze = typed only. const { normalizeEvent, serializeEvents } = require('../logEvent.js'); describe('logEvent normalizer', () => { test('new schema passes through', () => { const raw = { id: 'l1', ts: 1000, type: 'next_turn', message: 'Bob turn', encounterId: 'e1', encounterName: 'Enc', encounterPath: 'encounters/e1', participantId: 'p1', participantName: 'Bob', delta: { wrapped: true, prevRound: 1 }, undo: { currentTurnParticipantId: 'p0', round: 1, turnOrderIds: ['p0','p1'] }, undone: false, snapshot: { round: 2, currentTurnParticipantId: 'p1', turnOrderIds: ['p1'], activeIds: ['p1'] }, }; const e = normalizeEvent(raw); expect(e.id).toBe('l1'); expect(e.type).toBe('next_turn'); expect(e.snapshot.round).toBe(2); expect(e.delta.wrapped).toBe(true); expect(e.undo.round).toBe(1); expect(e.participantName).toBe('Bob'); }); test('old bloated schema (payload/undo_payload) lifts undo, drops payload', () => { const raw = { id: 'l1b', ts: 1100, type: 'damage', message: 'dmg', encounterPath: 'encounters/e1', payload: { participants: [{ id: 'p1', currentHp: 30 }] }, undo_payload: { encounterPath: 'encounters/e1', updates: { round: 1 }, redo: { round: 2 } }, snapshot: { round: 2, currentTurnParticipantId: 'p1', turnOrderIds: ['p1'], activeIds: ['p1'] }, }; const e = normalizeEvent(raw); expect(e.type).toBe('damage'); expect(e.undo).toBeTruthy(); expect(e.delta).toBeNull(); }); test('legacy (no type) dropped → null', () => { const raw = { id: 'l2', timestamp: 500, message: 'old', encounterName: 'Enc', undo: { encounterPath: 'enc/e1', updates: {} } }; expect(normalizeEvent(raw)).toBeNull(); }); test('explicit unknown type dropped → null', () => { expect(normalizeEvent({ id: 'l3', type: 'unknown' })).toBeNull(); }); test('no type at all dropped → null', () => { expect(normalizeEvent({ id: 'l4' })).toBeNull(); expect(normalizeEvent({ path: 'logs/abc' })).toBeNull(); }); test('null in null out', () => { expect(normalizeEvent(null)).toBeNull(); }); test('serialize drops all-legacy input → empty', () => { const raw = [ { id: 'c', timestamp: 300 }, { id: 'a', timestamp: 100 }, { id: 'b', timestamp: 200 }, ]; expect(JSON.parse(serializeEvents(raw))).toEqual([]); }); test('serialize sorts typed events ascending by ts', () => { const raw = [ { id: 'c', type: 'next_turn', ts: 300 }, { id: 'a', type: 'start_encounter', ts: 100 }, { id: 'b', type: 'damage', ts: 200 }, ]; const arr = JSON.parse(serializeEvents(raw)); expect(arr.map(e => e.id)).toEqual(['a', 'b', 'c']); }); test('serialize filters nulls + legacy, keeps typed', () => { const arr = JSON.parse(serializeEvents([null, { id: 'x', type: 'damage' }, { id: 'legacy' }, null])); expect(arr).toHaveLength(1); expect(arr[0].id).toBe('x'); }); });