86 lines
4.7 KiB
TypeScript
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');
|
|
});
|
|
});
|