Files
openmaic/OpenMAIC/tests/audio/audio-player-pool-first.test.ts
2026-08-16 14:58:47 +08:00

98 lines
2.9 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
audioGet: vi.fn(),
poolResolve: vi.fn(),
poolRelease: vi.fn(),
}));
vi.mock('@/lib/utils/database', () => ({
db: { audioFiles: { get: mocks.audioGet } },
}));
vi.mock('@/lib/media/asset-pool', () => ({
getAssetPool: () => ({ resolve: mocks.poolResolve, release: mocks.poolRelease }),
}));
import { AudioPlayer } from '@/lib/utils/audio-player';
/** Captures the blob each object URL was minted from. */
function stubObjectUrl(): { sources: Blob[] } {
const sources: Blob[] = [];
class URLStub extends URL {}
Object.assign(URLStub, {
createObjectURL: vi.fn((blob: Blob) => {
sources.push(blob);
return `blob:audio-${sources.length}`;
}),
revokeObjectURL: vi.fn(),
});
vi.stubGlobal('URL', URLStub);
return { sources };
}
function stubAudio() {
class AudioStub {
play = vi.fn(async () => {});
addEventListener = vi.fn();
pause = vi.fn();
volume = 1;
defaultPlaybackRate = 1;
playbackRate = 1;
src = '';
currentTime = 0;
}
vi.stubGlobal('Audio', AudioStub);
}
describe('AudioPlayer stored-byte resolution', () => {
beforeEach(() => {
vi.unstubAllGlobals();
vi.clearAllMocks();
stubAudio();
});
/**
* Speech regeneration replaces bytes under the same id, committing to the
* pool first. When the compatibility write then fails, the Dexie row is
* stale and playback must not serve the superseded narration.
*/
it('prefers pool bytes over a lagging audio row', async () => {
const { sources } = stubObjectUrl();
mocks.audioGet.mockResolvedValue({ id: 'ast_voice', blob: new Blob(['stale-narration']) });
mocks.poolResolve.mockResolvedValue('blob:pool-audio');
vi.stubGlobal(
'fetch',
vi.fn(async (input: string) => {
if (input === 'blob:pool-audio') return new Response(new Blob(['current-narration']));
throw new Error(`Unexpected fetch: ${input}`);
}),
);
const played = await new AudioPlayer().play('ast_voice');
expect(played).toBe(true);
expect(await sources[0].text()).toBe('current-narration');
expect(mocks.poolRelease).toHaveBeenCalled();
});
it('falls back to the stored row when the pool has no entry', async () => {
const { sources } = stubObjectUrl();
mocks.audioGet.mockResolvedValue({ id: 'tts_s1_legacy', blob: new Blob(['legacy-narration']) });
mocks.poolResolve.mockResolvedValue(null);
const played = await new AudioPlayer().play('tts_s1_legacy');
expect(played).toBe(true);
expect(await sources[0].text()).toBe('legacy-narration');
});
it('reports no audio when neither store has bytes', async () => {
stubObjectUrl();
mocks.audioGet.mockResolvedValue(undefined);
mocks.poolResolve.mockResolvedValue(null);
expect(await new AudioPlayer().play('ast_missing')).toBe(false);
});
});