// @vitest-environment node import { describe, expect, it, vi } from 'vitest'; import { GameResourceClient, GameResourceClientError, isTerminalGameResourceStatus, } from '../../electron/services/game-resource-client'; const EXECUTION_ID = '11111111-1111-4111-8111-111111111111'; const RELEASE_ID = '22222222-2222-4222-8222-222222222222'; const PROJECT_ID = '33333333-3333-4333-8333-333333333333'; function generation(overrides: Record = {}) { return { schema_version: 1, plugin_id: 'makelore.game-resource', execution_id: EXECUTION_ID, release_id: RELEASE_ID, project_id: PROJECT_ID, logical_operation_id: 'pi:run-a:resource-a', kind: 'pixel', template_name: 'character', status: 'accepted', output_count: 0, poll_interval_seconds: 3, billing: { mode: 'platform_metered', status: 'dispatched', reserved_points: '2.00', usage_amount: 1, unit: 'generation', }, ...overrides, }; } function jsonResponse(value: unknown, status = 200): Response { return new Response(JSON.stringify(value), { status, headers: { 'content-type': 'application/json' }, }); } describe('GameResourceClient', () => { it('refreshes one 401 and parses the provider-neutral metered contract', async () => { const fetchImpl = vi.fn() .mockResolvedValueOnce(new Response(null, { status: 401 })) .mockResolvedValueOnce(jsonResponse(generation({ provider_job_id: 'must-not-project' }), 202)); const getAccessToken = vi.fn(async (options?: { forceRefresh?: boolean }) => ( options?.forceRefresh ? 'fresh-token' : 'stale-token' )); const client = new GameResourceClient({ apiBaseUrl: 'https://works.example/', fetchImpl, getAccessToken: getAccessToken as never, }); const result = await client.generate({ releaseAdmissionId: 'admission-a', releaseId: RELEASE_ID, projectId: PROJECT_ID, logicalOperationId: 'pi:run-a:resource-a', kind: 'pixel', templateName: 'character', templateConfig: {}, requirement: 'A blue-armored hero', }); expect(result).toEqual({ executionId: EXECUTION_ID, releaseId: RELEASE_ID, projectId: PROJECT_ID, logicalOperationId: 'pi:run-a:resource-a', kind: 'pixel', templateName: 'character', status: 'accepted', outputCount: 0, pollIntervalSeconds: 3, billing: { mode: 'platform_metered', status: 'dispatched', reserved_points: '2.00', usage_amount: 1, unit: 'generation', }, }); expect(JSON.stringify(result)).not.toContain('provider_job_id'); expect(fetchImpl).toHaveBeenCalledTimes(2); expect(fetchImpl.mock.calls[0]?.[1]?.headers).toMatchObject({ Authorization: 'Bearer stale-token' }); expect(fetchImpl.mock.calls[1]?.[1]?.headers).toMatchObject({ Authorization: 'Bearer fresh-token' }); expect(getAccessToken).toHaveBeenNthCalledWith(2, { fetchImpl, forceRefresh: true }); }); it('fails closed on a malformed billing receipt', async () => { const client = new GameResourceClient({ fetchImpl: vi.fn().mockResolvedValue(jsonResponse(generation({ billing: { mode: 'platform_metered', status: 'settled', reserved_points: '2.00', unit: 'credit' }, }))), getAccessToken: vi.fn(async () => 'token') as never, }); await expect(client.get(EXECUTION_ID)).rejects.toMatchObject({ code: 'plugin_backend_invalid', status: 502, }); }); it('downloads bounded output bytes without accepting provider redirects', async () => { const fetchImpl = vi.fn().mockResolvedValue(new Response(new Uint8Array([1, 2, 3]), { status: 200, headers: { 'content-type': 'image/png', 'content-disposition': "attachment; filename*=UTF-8''hero.png", }, })); const client = new GameResourceClient({ apiBaseUrl: 'https://works.example', fetchImpl, getAccessToken: vi.fn(async () => 'token') as never, }); await expect(client.download(EXECUTION_ID, 1)).resolves.toMatchObject({ fileName: 'hero.png', contentType: 'image/png', bytes: new Uint8Array([1, 2, 3]), }); expect(fetchImpl.mock.calls[0]?.[0]).toBe( `https://works.example/api/plugins/v1/hosted/game-resource/generations/${EXECUTION_ID}/content?output_index=1`, ); expect(fetchImpl.mock.calls[0]?.[1]).toMatchObject({ redirect: 'manual' }); expect(isTerminalGameResourceStatus('succeeded')).toBe(true); expect(isTerminalGameResourceStatus('running')).toBe(false); }); });