// @vitest-environment node import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { PiProjectWriteLeaseCoordinator } from '../../electron/coding-runtime/pi/write-lease'; import { GameResourceDeliveryCoordinator, GameResourceDeliveryReceiptStore, } from '../../electron/services/game-resource-delivery'; import type { GameResourceClient, GameResourceGeneration, } 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'; const roots: string[] = []; const dispatchedBilling = { mode: 'platform_metered' as const, status: 'dispatched' as const, reserved_points: '2.00', usage_amount: 1, unit: 'generation', }; const settledBilling = { mode: 'platform_metered' as const, status: 'settled' as const, reserved_points: '2.00', actual_points: '2.00', usage_amount: 1, unit: 'generation', }; function generation( status: GameResourceGeneration['status'], overrides: Partial = {}, ): GameResourceGeneration { return { executionId: EXECUTION_ID, releaseId: RELEASE_ID, projectId: PROJECT_ID, logicalOperationId: 'pi:run-a:resource-a', kind: 'pixel', templateName: 'character', status, outputCount: status === 'succeeded' ? 2 : 0, pollIntervalSeconds: 3, billing: status === 'succeeded' ? settledBilling : dispatchedBilling, ...overrides, }; } async function fixture(overrides: { generate?: () => Promise; get?: () => Promise; download?: (executionId: string, outputIndex?: number) => Promise<{ bytes: Uint8Array; fileName: string | null; contentType: string; }>; writeOutput?: (filePath: string, bytes: Uint8Array) => Promise; sleep?: (milliseconds: number, signal?: AbortSignal) => Promise; acquireBackgroundLease?: () => () => void; } = {}) { const root = await mkdtemp(path.join(tmpdir(), 'makelore-game-delivery-')); const userDataDir = await mkdtemp(path.join(tmpdir(), 'makelore-game-receipts-')); roots.push(root, userDataDir); const generate = vi.fn(overrides.generate ?? (async () => generation('accepted'))); const get = vi.fn(overrides.get ?? (async () => generation('succeeded'))); const download = vi.fn(overrides.download ?? (async (_executionId, index = 0) => ({ bytes: new Uint8Array([index + 1, index + 2]), fileName: index === 0 ? 'hero.png' : 'portrait.webp', contentType: index === 0 ? 'image/png' : 'image/webp', }))); const client = { generate, get, download } as unknown as Pick< GameResourceClient, 'generate' | 'get' | 'download' >; const leases = new PiProjectWriteLeaseCoordinator(); const touched = vi.fn(async () => undefined); const progress = vi.fn(); const receiptStore = new GameResourceDeliveryReceiptStore( path.join(userDataDir, 'coding-runtime', 'game-resource', 'receipts.json'), ); const coordinator = new GameResourceDeliveryCoordinator({ client, receipts: receiptStore, leases, sleep: overrides.sleep ?? (async () => undefined), recordTouchedPaths: touched, ...(overrides.writeOutput ? { writeOutput: overrides.writeOutput } : {}), ...(overrides.acquireBackgroundLease ? { acquireBackgroundLease: overrides.acquireBackgroundLease } : {}), }); const context = { conversationId: 'conversation-a', runId: 'run-a', localProjectId: 'local-project-a', durableProjectId: PROJECT_ID, projectPath: root, logicalOperationId: 'pi:run-a:resource-a', }; const request = { releaseAdmissionId: 'admission-a', releaseId: RELEASE_ID, projectId: PROJECT_ID, logicalOperationId: context.logicalOperationId, kind: 'pixel' as const, templateName: 'character', templateConfig: {}, requirement: 'A blue-armored hero', }; return { coordinator, receiptStore, context, request, root, userDataDir, client, generate, get, download, leases, touched, progress, }; } afterEach(async () => { await Promise.all(roots.splice(0).map(async (root) => await rm(root, { recursive: true, force: true }))); }); describe('GameResourceDeliveryCoordinator', () => { it('submits once, hides polling, saves every output under the frozen project, and reports progress', async () => { let activeProjectPath: string | undefined; const states = [generation('running'), generation('succeeded')]; let fixtureValue: Awaited>; fixtureValue = await fixture({ get: async () => { expect(fixtureValue.leases.activeCount).toBe(0); activeProjectPath = path.join(fixtureValue.root, '..', 'another-project'); return states.shift() as GameResourceGeneration; }, download: async (_executionId, index = 0) => { expect(fixtureValue.leases.activeCount).toBe(0); expect(fixtureValue.progress.mock.calls.at(-1)?.[0].phase).toBe('saving'); return { bytes: new Uint8Array([index + 1, index + 2]), fileName: index === 0 ? 'hero.png' : 'portrait.webp', contentType: index === 0 ? 'image/png' : 'image/webp', }; }, }); const result = await fixtureValue.coordinator.generateAndMaterialize({ context: fixtureValue.context, request: fixtureValue.request, onProgress: fixtureValue.progress, }); expect(activeProjectPath).not.toBe(fixtureValue.root); expect(fixtureValue.generate).toHaveBeenCalledTimes(1); expect(fixtureValue.get).toHaveBeenCalledTimes(2); expect(fixtureValue.download.mock.calls.map((call) => call[1])).toEqual([0, 1]); expect(result).toMatchObject({ executionId: EXECUTION_ID, providerStatus: 'succeeded', deliveryStatus: 'saved', files: [ { path: `assets/generated/game-resource/${EXECUTION_ID}/output-1.png`, bytes: 2 }, { path: `assets/generated/game-resource/${EXECUTION_ID}/output-2.webp`, bytes: 2 }, ], billing: settledBilling, }); await expect(readFile(path.join( fixtureValue.root, 'assets', 'generated', 'game-resource', EXECUTION_ID, 'output-1.png', ))).resolves.toEqual(Buffer.from([1, 2])); await expect(readFile(path.join( fixtureValue.root, 'assets', 'generated', 'game-resource', EXECUTION_ID, 'output-2.webp', ))).resolves.toEqual(Buffer.from([2, 3])); expect(fixtureValue.leases.activeCount).toBe(0); expect(fixtureValue.touched).toHaveBeenCalledWith( 'conversation-a', 'run-a', result.files.map(({ path: filePath }) => filePath), ); expect(fixtureValue.progress.mock.calls.map(([item]) => item.phase)).toEqual([ 'submitted', 'generating', 'generating', 'saving', 'saved', ]); }); it('deduplicates concurrent replay and returns persisted saved paths without another submit or download', async () => { let releaseGenerate: ((value: GameResourceGeneration) => void) | undefined; const generated = new Promise((resolve) => { releaseGenerate = resolve; }); const value = await fixture({ generate: async () => await generated }); const first = value.coordinator.generateAndMaterialize({ context: value.context, request: value.request }); const second = value.coordinator.generateAndMaterialize({ context: value.context, request: value.request }); releaseGenerate?.(generation('succeeded')); const [left, right] = await Promise.all([first, second]); expect(left).toEqual(right); expect(value.generate).toHaveBeenCalledTimes(1); expect(value.download).toHaveBeenCalledTimes(2); const replay = await value.coordinator.generateAndMaterialize({ context: value.context, request: value.request, }); expect(replay).toEqual(left); expect(value.generate).toHaveBeenCalledTimes(1); expect(value.download).toHaveBeenCalledTimes(2); }); it('keeps a background lifecycle lease through polling and delivery', async () => { let held = false; const release = vi.fn(() => { held = false; }); const value = await fixture({ acquireBackgroundLease: () => { held = true; return release; }, generate: async () => { expect(held).toBe(true); return generation('accepted'); }, get: async () => { expect(held).toBe(true); return generation('succeeded'); }, download: async (_executionId, index = 0) => { expect(held).toBe(true); return { bytes: new Uint8Array([index + 1]), fileName: 'hero.png', contentType: 'image/png', }; }, }); await expect(value.coordinator.generateAndMaterialize({ context: value.context, request: value.request, })).resolves.toMatchObject({ deliveryStatus: 'saved' }); expect(held).toBe(false); expect(release).toHaveBeenCalledOnce(); }); it('resumes an accepted persisted execution after the original waiter stops', async () => { const value = await fixture({ sleep: async () => { throw new Error('waiter stopped'); }, }); await expect(value.coordinator.generateAndMaterialize({ context: value.context, request: value.request, })).rejects.toThrow('waiter stopped'); expect(value.generate).toHaveBeenCalledTimes(1); const restarted = new GameResourceDeliveryCoordinator({ client: value.client, receipts: new GameResourceDeliveryReceiptStore( path.join(value.userDataDir, 'coding-runtime', 'game-resource', 'receipts.json'), ), leases: value.leases, sleep: async () => undefined, }); await expect(restarted.resumePending()).resolves.toEqual([expect.objectContaining({ executionId: EXECUTION_ID, providerStatus: 'succeeded', deliveryStatus: 'saved', })]); expect(value.generate).toHaveBeenCalledTimes(1); }); it('persists provider success and resumes only local delivery after a write failure', async () => { let failWrite = true; const writeOutput = vi.fn(async (target: string, bytes: Uint8Array) => { if (failWrite) throw Object.assign(new Error('disk unavailable'), { code: 'ENOSPC' }); await import('node:fs/promises').then(async ({ mkdir, writeFile }) => { await mkdir(path.dirname(target), { recursive: true }); await writeFile(target, bytes, { flag: 'wx' }); }); }); const value = await fixture({ generate: async () => generation('succeeded'), writeOutput, }); await expect(value.coordinator.generateAndMaterialize({ context: value.context, request: value.request, })).resolves.toMatchObject({ providerStatus: 'succeeded', deliveryStatus: 'delivery_failed', errorCode: 'game_resource_delivery_failed', files: [], }); expect(value.generate).toHaveBeenCalledTimes(1); failWrite = false; const restarted = new GameResourceDeliveryCoordinator({ client: value.client, receipts: new GameResourceDeliveryReceiptStore( path.join(value.userDataDir, 'coding-runtime', 'game-resource', 'receipts.json'), ), leases: value.leases, sleep: async () => undefined, writeOutput, recordTouchedPaths: value.touched, }); const resumed = await restarted.resumePending(); expect(resumed).toEqual([expect.objectContaining({ executionId: EXECUTION_ID, providerStatus: 'succeeded', deliveryStatus: 'saved', })]); expect(value.generate).toHaveBeenCalledTimes(1); expect(value.get).not.toHaveBeenCalled(); expect(value.download).toHaveBeenCalledTimes(8); }); it('never overwrites an existing project file with different bytes', async () => { const value = await fixture({ generate: async () => generation('succeeded') }); const relativePath = `assets/generated/game-resource/${EXECUTION_ID}/output-1.png`; const target = path.join(value.root, ...relativePath.split('/')); await mkdir(path.dirname(target), { recursive: true }); await writeFile(target, Buffer.from([99, 98])); await expect(value.coordinator.generateAndMaterialize({ context: value.context, request: value.request, })).resolves.toMatchObject({ providerStatus: 'succeeded', deliveryStatus: 'delivery_failed', errorCode: 'game_resource_delivery_failed', }); await expect(readFile(target)).resolves.toEqual(Buffer.from([99, 98])); expect(value.generate).toHaveBeenCalledTimes(1); }); it('persists provider success and resumes only local delivery after a download failure', async () => { let failDownload = true; const value = await fixture({ generate: async () => generation('succeeded'), download: async (_executionId, index = 0) => { if (failDownload) throw new Error('download unavailable'); return { bytes: new Uint8Array([index + 1, index + 2]), fileName: index === 0 ? 'hero.png' : 'portrait.webp', contentType: index === 0 ? 'image/png' : 'image/webp', }; }, }); await expect(value.coordinator.generateAndMaterialize({ context: value.context, request: value.request, })).resolves.toMatchObject({ providerStatus: 'succeeded', deliveryStatus: 'delivery_failed', errorCode: 'game_resource_delivery_failed', files: [], }); expect(value.generate).toHaveBeenCalledTimes(1); failDownload = false; const restarted = new GameResourceDeliveryCoordinator({ client: value.client, receipts: new GameResourceDeliveryReceiptStore( path.join(value.userDataDir, 'coding-runtime', 'game-resource', 'receipts.json'), ), leases: value.leases, sleep: async () => undefined, recordTouchedPaths: value.touched, }); await expect(restarted.resumePending()).resolves.toEqual([expect.objectContaining({ executionId: EXECUTION_ID, providerStatus: 'succeeded', deliveryStatus: 'saved', })]); expect(value.generate).toHaveBeenCalledTimes(1); expect(value.get).not.toHaveBeenCalled(); }); it('retries a transient local delivery failure without submitting another generation', async () => { let downloadAttempts = 0; const sleep = vi.fn(async () => undefined); const value = await fixture({ generate: async () => generation('succeeded'), sleep, download: async (_executionId, index = 0) => { downloadAttempts += 1; if (downloadAttempts === 1) throw new Error('temporary download failure'); return { bytes: new Uint8Array([index + 1, index + 2]), fileName: index === 0 ? 'hero.png' : 'portrait.webp', contentType: index === 0 ? 'image/png' : 'image/webp', }; }, }); await expect(value.coordinator.generateAndMaterialize({ context: value.context, request: value.request, })).resolves.toMatchObject({ providerStatus: 'succeeded', deliveryStatus: 'saved', }); expect(value.generate).toHaveBeenCalledTimes(1); expect(value.get).not.toHaveBeenCalled(); expect(sleep).toHaveBeenCalledOnce(); }); it('continues resuming later receipts when an earlier reconciliation is unavailable', async () => { let generationIndex = 0; let sleepCalls = 0; const value = await fixture({ generate: async () => { const first = generationIndex++ === 0; return generation(first ? 'accepted' : 'succeeded', { executionId: first ? '11111111-1111-4111-8111-111111111111' : '44444444-4444-4444-8444-444444444444', }); }, download: async () => { throw new Error('delivery unavailable'); }, sleep: async () => { if (sleepCalls++ === 0) throw new Error('waiter stopped'); }, }); const secondContext = { ...value.context, logicalOperationId: 'pi:run-a:resource-b', }; const secondRequest = { ...value.request, logicalOperationId: secondContext.logicalOperationId, }; await expect(value.coordinator.generateAndMaterialize({ context: value.context, request: value.request, })).rejects.toThrow('waiter stopped'); await value.coordinator.generateAndMaterialize({ context: secondContext, request: secondRequest, }); const resumedClient = { generate: vi.fn(async () => { throw new Error('must not regenerate'); }), get: vi.fn(async () => { throw new Error('first reconciliation unavailable'); }), download: vi.fn(async (executionId: string, index = 0) => { if (executionId === EXECUTION_ID) throw new Error('first delivery remains unavailable'); return { bytes: new Uint8Array([index + 1]), fileName: 'result.png', contentType: 'image/png', }; }), } as unknown as Pick; const restarted = new GameResourceDeliveryCoordinator({ client: resumedClient, receipts: new GameResourceDeliveryReceiptStore( path.join(value.userDataDir, 'coding-runtime', 'game-resource', 'receipts.json'), ), leases: value.leases, sleep: async () => undefined, }); const results = await restarted.resumePending(); expect(results).toEqual([expect.objectContaining({ executionId: '44444444-4444-4444-8444-444444444444', deliveryStatus: 'saved', })]); expect(resumedClient.generate).not.toHaveBeenCalled(); expect(resumedClient.get).toHaveBeenCalledOnce(); }); it('does not hold the write lease or create files for a pending-review generation', async () => { const value = await fixture({ generate: async () => generation('pending_review', { outputCount: 0 }), }); await expect(value.coordinator.generateAndMaterialize({ context: value.context, request: value.request, })).resolves.toMatchObject({ providerStatus: 'pending_review', deliveryStatus: 'not_started', files: [], }); expect(value.download).not.toHaveBeenCalled(); expect(value.leases.activeCount).toBe(0); expect(value.touched).not.toHaveBeenCalled(); }); });