import { describe, expect, it, vi } from 'vitest'; import { GameAudioClient } from '../../electron/services/game-audio-client'; const execution = '00000000-0000-4000-8000-000000000601'; const release = '00000000-0000-4000-8000-000000000505'; const project = '00000000-0000-4000-8000-000000000602'; const receipt = () => ({ schema_version: 1, plugin_id: 'makelore.game-audio', execution_id: execution, release_id: release, project_id: project, logical_operation_id: 'audio-request', kind: 'sound', provider_status: 'succeeded', output_ready: true, outputs: [{ index: 0, name: 'sound-1' }], progress_percent: 100, poll_interval_seconds: 2, error_code: null, billing: { mode: 'platform_metered', status: 'settled', reserved_points: '0.16', actual_points: '0.04', usage_amount: 4, unit: 'half_second' }, }); const command = { release_id: release, release_admission_id: project, project_id: project, logical_operation_id: 'audio-request', pricing_version_id: project, kind: 'sound' as const, mode: 'single' as const, duration: 2, count: 1, loop: false, prompt: 'A coin chime', confirmed: true as const, }; function setup(fetchImpl: typeof fetch) { return new GameAudioClient({ fetchImpl, apiBaseUrl: 'https://square.test', getAccessToken: async () => 'session-token', getAccountBinding: () => ({ accountKey: 'owner', epoch: 1 }) }); } describe('Game Audio typed Main transport', () => { it('normalizes nullable unsettled amounts into the shared optional receipt field', async () => { const wire = { ...receipt(), provider_status: 'running', output_ready: false, outputs: [], billing: { ...receipt().billing, status: 'dispatched', actual_points: null } }; const job = await setup(vi.fn().mockResolvedValue(Response.json(wire))).get(execution, 'owner'); expect(job.billing).not.toHaveProperty('actual_points'); }); it('uses fixed routes and retries a 401 only once with exactly the same command', async () => { const fetchImpl = vi.fn() .mockResolvedValueOnce(new Response('', { status: 401 })) .mockResolvedValueOnce(Response.json(receipt())); const client = setup(fetchImpl); expect(await client.generate(command, 'owner')).toEqual(receipt()); expect(fetchImpl).toHaveBeenCalledTimes(2); expect(fetchImpl.mock.calls[0]?.[0]).toBe('https://square.test/api/plugins/v1/hosted/game-audio/generations'); expect(fetchImpl.mock.calls[0]?.[1]?.body).toBe(JSON.stringify(command)); expect(fetchImpl.mock.calls[1]?.[1]?.body).toBe(fetchImpl.mock.calls[0]?.[1]?.body); }); it('rejects contradictory output/billing and response identity', async () => { for (const value of [ { ...receipt(), output_ready: false }, { ...receipt(), execution_id: '../escape' }, { ...receipt(), project_id: execution }, { ...receipt(), billing: { ...receipt().billing, actual_points: 'bad' } }, { ...receipt(), outputs: [{ index: 1, name: 'sound-2' }] }, ]) { await expect(setup(vi.fn().mockResolvedValue(Response.json(value))) .generate(command, 'owner')).rejects.toMatchObject({ code: 'plugin_backend_invalid' }); } }); it('checks account binding before network and again after a response', async () => { const fetchImpl = vi.fn().mockResolvedValue(Response.json(receipt())); await expect(setup(fetchImpl).get(execution, 'other')).rejects.toMatchObject({ code: 'plugin_account_changed' }); expect(fetchImpl).not.toHaveBeenCalled(); let epoch = 1; const client = new GameAudioClient({ getAccessToken: async () => 'token', getAccountBinding: () => ({ accountKey: 'owner', epoch }), fetchImpl: async () => { epoch++; return Response.json(receipt()); }, }); await expect(client.get(execution, 'owner')).rejects.toMatchObject({ code: 'plugin_account_changed' }); }); it('bounds chunked JSON and downloads only supported audio MIME', async () => { let cancelled = false; const body = new ReadableStream({ pull(controller) { controller.enqueue(new Uint8Array(600_000)); }, cancel() { cancelled = true; }, }); await expect(setup(vi.fn().mockResolvedValue(new Response(body))) .get(execution, 'owner')).rejects.toMatchObject({ code: 'plugin_backend_invalid' }); expect(cancelled).toBe(true); await expect(setup(vi.fn().mockResolvedValue(new Response('html', { headers: { 'Content-Type': 'text/html' }, }))).download(execution, 0, 'owner')).rejects.toMatchObject({ code: 'game_audio_content_invalid' }); const audio = await setup(vi.fn().mockResolvedValue(new Response('ID3test', { headers: { 'Content-Type': 'audio/mpeg' }, }))).download(execution, 0, 'owner'); expect(audio.extension).toBe('mp3'); }); });