Files
openmaic/OpenMAIC/tests/playback/discussion-consumption.test.ts
2026-08-16 14:58:47 +08:00

119 lines
3.4 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import { PlaybackEngine } from '@/lib/playback/engine';
import type { Action, DiscussionAction } from '@/lib/types/action';
import type { Scene } from '@/lib/types/stage';
import type { ActionEngine } from '@/lib/action/engine';
import type { AudioPlayer } from '@/lib/utils/audio-player';
import type { PlaybackSnapshot } from '@/lib/playback/types';
function discussion(id: string, agentId?: string): Action {
return {
id,
type: 'discussion',
topic: `topic-${id}`,
prompt: `prompt-${id}`,
agentId,
} as unknown as DiscussionAction;
}
function scene(actions: Action[]): Scene {
return {
id: 'scene-1',
stageId: 'stage-1',
type: 'slide',
title: 'Scene 1',
order: 1,
content: { type: 'slide', canvas: {} },
actions,
} as unknown as Scene;
}
function createActionEngine(): ActionEngine {
return {
execute: vi.fn(async () => {}),
clearEffects: vi.fn(),
resetPlaybackVisualState: vi.fn(),
} as unknown as ActionEngine;
}
function createAudioPlayer(): AudioPlayer {
return {
play: vi.fn(async () => false),
onEnded: vi.fn(),
stop: vi.fn(),
pause: vi.fn(),
resume: vi.fn(),
isPlaying: vi.fn(() => false),
hasActiveAudio: vi.fn(() => false),
} as unknown as AudioPlayer;
}
/**
* Every discussion-consumption path must publish a progress snapshot carrying
* the consumed id: `onProgress` otherwise only fires BEFORE the discussion
* action executes, and a discussion is the scene's last action, so persistence
* would never observe the fact (the P0 found in final review).
*/
describe('discussion consumption emits a progress snapshot', () => {
afterEach(() => {
vi.useRealTimers();
});
function snapshotsWith(onProgress: ReturnType<typeof vi.fn>, id: string): PlaybackSnapshot[] {
return (onProgress.mock.calls as [PlaybackSnapshot][])
.map(([snapshot]) => snapshot)
.filter((snapshot) => snapshot.consumedDiscussions.includes(id));
}
it('publishes the fact when an unselected agent auto-skips the discussion', async () => {
const onProgress = vi.fn();
const engine = new PlaybackEngine(
[scene([discussion('disc-auto', 'agent-x')])],
createActionEngine(),
createAudioPlayer(),
{ onProgress, isAgentSelected: () => false },
);
engine.start();
await Promise.resolve();
await Promise.resolve();
expect(snapshotsWith(onProgress, 'disc-auto').length).toBeGreaterThan(0);
});
it('publishes the fact when the user skips the proactive card', async () => {
vi.useFakeTimers();
const onProgress = vi.fn();
const engine = new PlaybackEngine(
[scene([discussion('disc-skip')])],
createActionEngine(),
createAudioPlayer(),
{ onProgress },
);
engine.start();
await vi.advanceTimersByTimeAsync(4000);
engine.skipDiscussion();
expect(snapshotsWith(onProgress, 'disc-skip').length).toBeGreaterThan(0);
});
it('publishes the fact when the user joins the discussion', async () => {
vi.useFakeTimers();
const onProgress = vi.fn();
const engine = new PlaybackEngine(
[scene([discussion('disc-join')])],
createActionEngine(),
createAudioPlayer(),
{ onProgress },
);
engine.start();
await vi.advanceTimersByTimeAsync(4000);
engine.confirmDiscussion();
expect(snapshotsWith(onProgress, 'disc-join').length).toBeGreaterThan(0);
});
});