// Undo roundtrip: every op that writes a log entry with undo_payload must // restore prior state. Apply op (writes encounter + log) → read back log's // undo_payload.updates → apply undo on top of newEnc → assert deepEqual original. // // Rewritten for lean log shape: `undo` is id-based inverse; expandUndo(entry, enc) // builds full patch from current enc doc. Apply updates → assert equals before. const shared = require('@ttrpg/shared'); const { mockCtx } = require('./_helpers'); const { expandUndo } = shared; const { makeParticipant, startEncounter, nextTurn, togglePause, addParticipant, removeParticipant, toggleParticipantActive, applyHpChange, toggleCondition, reorderParticipants, endEncounter, } = shared; function p(id, init, extra = {}) { return makeParticipant({ id, name: id, type: 'monster', initiative: init, maxHp: 100, currentHp: 100, ...extra, }); } function enc(ps) { return { name:'t', participants:ps, isStarted:false, isPaused:false, round:0, currentTurnParticipantId:null, turnOrderIds:[] }; } const snap = (e) => JSON.parse(JSON.stringify(e)); // Last log entry's expanded undo patch (built from id-based undo + enc doc). function lastUndo(storage, enc) { const logs = storage.logs(); const last = logs[logs.length - 1]; expect(last.undo).toBeTruthy(); const expanded = expandUndo(last, enc); expect(expanded).toBeTruthy(); return expanded.updates; } describe('undo roundtrip', () => { test('startEncounter undo restores pre-start', async () => { const { storage, ctx } = mockCtx(); const before = enc([p('a',10),p('b',20)]); const newEnc = await startEncounter(before, ctx); expect(storage.logs()).toHaveLength(1); const undo = lastUndo(storage, newEnc); // undo restores isStarted/isPaused/round/current/turnOrderIds. // participants[] may be reordered (1-list sort on start) — undo snapshot // captures turn-state fields, not participant order. const after = { ...newEnc, ...undo }; expect(after.isStarted).toBe(before.isStarted); expect(after.isPaused).toBe(before.isPaused); expect(after.round).toBe(before.round); expect(after.currentTurnParticipantId).toBe(before.currentTurnParticipantId); expect(after.turnOrderIds).toEqual(before.turnOrderIds); }); test('nextTurn undo restores prior currentTurn/round', async () => { const { storage, ctx } = mockCtx(); let e = enc([p('a',10),p('b',20),p('c',5)]); e = await startEncounter(e, ctx); const before = snap(e); const newEnc = await nextTurn(e, ctx); const undo = lastUndo(storage, newEnc); const after = { ...newEnc, ...undo }; expect(snap(after)).toEqual(before); }); test('togglePause undo restores prior paused state', async () => { const { storage, ctx } = mockCtx(); let e = enc([p('a',10),p('b',20)]); e = await startEncounter(e, ctx); const before = snap(e); const newEnc = await togglePause(e, ctx); const undo = lastUndo(storage, newEnc); const after = { ...newEnc, ...undo }; expect(snap(after)).toEqual(before); }); test('applyHpChange undo restores prior participants', async () => { const { storage, ctx } = mockCtx(); let e = enc([p('a',10,{maxHp:100,currentHp:100}),p('b',20)]); e = await startEncounter(e, ctx); const before = snap(e); const newEnc = await applyHpChange(e, 'a', 'damage', 20, ctx); const undo = lastUndo(storage, newEnc); const after = { ...newEnc, ...undo }; expect(snap(after)).toEqual(before); }); test('toggleCondition undo restores prior participants', async () => { const { storage, ctx } = mockCtx(); let e = enc([p('a',10),p('b',20)]); e = await startEncounter(e, ctx); const before = snap(e); const newEnc = await toggleCondition(e, 'a', 'stunned', ctx); const undo = lastUndo(storage, newEnc); const after = { ...newEnc, ...undo }; expect(snap(after)).toEqual(before); }); test('toggleParticipantActive undo restores prior participants + turn order', async () => { const { storage, ctx } = mockCtx(); let e = enc([p('a',10),p('b',20),p('c',5)]); e = await startEncounter(e, ctx); const before = snap(e); const newEnc = await toggleParticipantActive(e, 'b', ctx); const undo = lastUndo(storage, newEnc); const after = { ...newEnc, ...undo }; expect(snap(after)).toEqual(before); }); test('addParticipant undo restores prior participants', async () => { const { storage, ctx } = mockCtx(); let e = enc([p('a',10),p('b',20)]); e = await startEncounter(e, ctx); const before = snap(e); const np = makeParticipant({ id:'z', name:'z', type:'monster', initiative:15, maxHp:50, currentHp:50 }); const newEnc = await addParticipant(e, np, ctx); const undo = lastUndo(storage, newEnc); const after = { ...newEnc, ...undo }; expect(snap(after)).toEqual(before); }); test('removeParticipant undo restores prior participants + turn order', async () => { const { storage, ctx } = mockCtx(); let e = enc([p('a',10),p('b',20),p('c',5)]); e = await startEncounter(e, ctx); const before = snap(e); const newEnc = await removeParticipant(e, 'b', ctx); const undo = lastUndo(storage, newEnc); const after = { ...newEnc, ...undo }; expect(snap(after)).toEqual(before); }); test('endEncounter undo restores prior state', async () => { const { storage, ctx } = mockCtx(); let e = enc([p('a',10),p('b',20)]); e = await startEncounter(e, ctx); const before = snap(e); const newEnc = await endEncounter(e, ctx); const undo = lastUndo(storage, newEnc); const after = { ...newEnc, ...undo }; expect(snap(after)).toEqual(before); }); test('reorderParticipants undo restores prior order (BUG-7 fixed)', async () => { // Unstarted encounter: no current-turn pointer, same-init reorder is valid. const { storage, ctx } = mockCtx(); const ps = [p('a',10),p('b',20),p('c',20)]; // b,c tie let e = enc(ps); const before = snap(e); const newEnc = await reorderParticipants(e, 'c', 'b', ctx); expect(storage.logs()).toHaveLength(1); // logged (was a no-log bug before) const undo = lastUndo(storage, newEnc); const after = { ...newEnc, ...undo }; expect(snap(after)).toEqual(before); }); });