Files
makelore/tests/unit/game-audio-client.test.ts

86 lines
4.7 KiB
TypeScript

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<typeof fetch>().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<typeof fetch>()
.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<typeof fetch>().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<typeof fetch>().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<Uint8Array>({
pull(controller) { controller.enqueue(new Uint8Array(600_000)); },
cancel() { cancelled = true; },
});
await expect(setup(vi.fn<typeof fetch>().mockResolvedValue(new Response(body)))
.get(execution, 'owner')).rejects.toMatchObject({ code: 'plugin_backend_invalid' });
expect(cancelled).toBe(true);
await expect(setup(vi.fn<typeof fetch>().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<typeof fetch>().mockResolvedValue(new Response('ID3test', {
headers: { 'Content-Type': 'audio/mpeg' },
}))).download(execution, 0, 'owner');
expect(audio.extension).toBe('mp3');
});
});