114 lines
4.7 KiB
TypeScript
114 lines
4.7 KiB
TypeScript
import { describe, expect, it, beforeEach, vi } from 'vitest';
|
|
|
|
import { useRegenSnapshots } from '@/lib/agent/client/regen-snapshots';
|
|
import type { SceneContent } from '@/lib/types/stage';
|
|
|
|
const SNAP = {
|
|
sceneId: 's1',
|
|
content: { type: 'slide', canvas: { id: 'c', elements: [] } } as unknown as SceneContent,
|
|
actions: [{ type: 'speech', id: 'a_old' } as never],
|
|
// post-edit state, so an undo can be resumed (redo)
|
|
redo: {
|
|
content: {
|
|
type: 'slide',
|
|
canvas: { id: 'c', elements: [{ id: 'e' }] },
|
|
} as unknown as SceneContent,
|
|
actions: [{ type: 'speech', id: 'a_new' } as never],
|
|
},
|
|
};
|
|
|
|
describe('regen-snapshots store', () => {
|
|
beforeEach(() => {
|
|
useRegenSnapshots.setState({ snapshots: {} });
|
|
});
|
|
|
|
it('stores a snapshot keyed by toolCallId (not yet restored)', () => {
|
|
useRegenSnapshots.getState().setSnapshot('call-1', SNAP);
|
|
const s = useRegenSnapshots.getState().snapshots['call-1'];
|
|
expect(s.sceneId).toBe('s1');
|
|
expect(s.restored).toBe(false);
|
|
});
|
|
|
|
it('restore toggles: undo applies the pre-edit snapshot, resume re-applies the post-edit (redo)', () => {
|
|
const apply = vi.fn();
|
|
useRegenSnapshots.getState().setSnapshot('call-1', SNAP);
|
|
|
|
// First click = undo → pre-edit state.
|
|
useRegenSnapshots.getState().restore('call-1', apply);
|
|
expect(apply).toHaveBeenNthCalledWith(1, 's1', {
|
|
content: SNAP.content,
|
|
actions: SNAP.actions,
|
|
});
|
|
expect(useRegenSnapshots.getState().snapshots['call-1'].restored).toBe(true);
|
|
|
|
// Second click = resume (redo) → post-edit state, toggles back.
|
|
useRegenSnapshots.getState().restore('call-1', apply);
|
|
expect(apply).toHaveBeenNthCalledWith(2, 's1', {
|
|
content: SNAP.redo.content,
|
|
actions: SNAP.redo.actions,
|
|
});
|
|
expect(useRegenSnapshots.getState().snapshots['call-1'].restored).toBe(false);
|
|
|
|
// Third click = undo again.
|
|
useRegenSnapshots.getState().restore('call-1', apply);
|
|
expect(apply).toHaveBeenCalledTimes(3);
|
|
expect(useRegenSnapshots.getState().snapshots['call-1'].restored).toBe(true);
|
|
});
|
|
|
|
it('resume of a content-only edit preserves actions (does not pass actions:[])', () => {
|
|
const apply = vi.fn();
|
|
// edit_interactive_html / action-less regen: redo patch has content, NO actions.
|
|
useRegenSnapshots
|
|
.getState()
|
|
.setSnapshot('call-1', { ...SNAP, redo: { content: SNAP.redo.content } });
|
|
useRegenSnapshots.getState().restore('call-1', apply); // undo
|
|
useRegenSnapshots.getState().restore('call-1', apply); // resume (redo)
|
|
// The redo patch must NOT carry actions — else updateScene wipes narration.
|
|
expect(apply.mock.calls[1][1]).toEqual({ content: SNAP.redo.content });
|
|
expect(apply.mock.calls[1][1]).not.toHaveProperty('actions');
|
|
});
|
|
|
|
it('resume of an actions-only redo preserves content (no content key)', () => {
|
|
const apply = vi.fn();
|
|
useRegenSnapshots
|
|
.getState()
|
|
.setSnapshot('call-1', { ...SNAP, redo: { actions: SNAP.redo.actions } });
|
|
useRegenSnapshots.getState().restore('call-1', apply);
|
|
useRegenSnapshots.getState().restore('call-1', apply);
|
|
expect(apply.mock.calls[1][1]).toEqual({ actions: SNAP.redo.actions });
|
|
expect(apply.mock.calls[1][1]).not.toHaveProperty('content');
|
|
});
|
|
|
|
it('restored snapshot without redo data cannot resume (stays restored)', () => {
|
|
const apply = vi.fn();
|
|
const { redo: _redo, ...noRedo } = SNAP;
|
|
useRegenSnapshots.getState().setSnapshot('call-1', noRedo);
|
|
useRegenSnapshots.getState().restore('call-1', apply); // undo
|
|
useRegenSnapshots.getState().restore('call-1', apply); // no redo → no-op
|
|
expect(apply).toHaveBeenCalledTimes(1);
|
|
expect(useRegenSnapshots.getState().snapshots['call-1'].restored).toBe(true);
|
|
});
|
|
|
|
it('restore is a no-op for an unknown toolCallId', () => {
|
|
const apply = vi.fn();
|
|
useRegenSnapshots.getState().restore('missing', apply);
|
|
expect(apply).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('actionsOnly snapshot restores actions only (never reverts slide content)', () => {
|
|
const apply = vi.fn();
|
|
useRegenSnapshots.getState().setSnapshot('call-1', { ...SNAP, actionsOnly: true });
|
|
useRegenSnapshots.getState().restore('call-1', apply);
|
|
expect(apply).toHaveBeenCalledWith('s1', { actions: SNAP.actions });
|
|
// crucially, the patch must NOT carry content (would clobber later edits)
|
|
expect(apply.mock.calls[0][1]).not.toHaveProperty('content');
|
|
});
|
|
|
|
it('clearAll drops every snapshot', () => {
|
|
useRegenSnapshots.getState().setSnapshot('call-1', SNAP);
|
|
useRegenSnapshots.getState().setSnapshot('call-2', SNAP);
|
|
useRegenSnapshots.getState().clearAll();
|
|
expect(useRegenSnapshots.getState().snapshots).toEqual({});
|
|
});
|
|
});
|