Files
openmaic/OpenMAIC/tests/lib/agent/client/regen-snapshots.test.ts
2026-08-16 14:58:47 +08:00

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({});
});
});