// @vitest-environment node import { 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 type { CodingPluginToolDefinition } from '../../shared/coding-plugins'; import { GameResourcePluginAdapter, } from '../../electron/coding-plugins/adapters/game-resource'; 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'; import type { MarketplacePackageClientPort, PluginPackageStore } from '../../electron/coding-plugins/package-store'; import type { TrustedCodingCapabilityContext } from '../../electron/coding-plugins/registry'; 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 billing = { mode: 'platform_metered' as const, status: 'dispatched' as const, reserved_points: '2.00', usage_amount: 1, unit: 'generation', }; function generation(status: GameResourceGeneration['status'] = 'accepted'): 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' ? 1 : 0, pollIntervalSeconds: 3, billing, }; } function tool( name: string, capabilityId = 'game-resource.generate', operation = name.replace('game_resource_', ''), ): CodingPluginToolDefinition { return { name, label: name, description: name, capabilityId, operation, roles: ['parent'], mutation: operation === 'templates' ? 'read' : 'write', projectWriteLease: false, permissions: [`hosted.game-resource.${operation.replaceAll('_', '-')}`], inputSchema: { type: 'object' }, }; } async function fixture() { const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-game-resource-')); const userDataDir = await mkdtemp(path.join(tmpdir(), 'makelore-game-resource-receipts-')); roots.push(projectPath, userDataDir); const resolve = vi.fn(async () => ({ resolveRequestId: 'pi:run-a:resource-a', resolveRequestDigest: 'a'.repeat(64), items: [{ pluginId: 'makelore.game-resource', action: 'keep' as const, releaseId: RELEASE_ID, version: '1.0.0', sha256: 'a'.repeat(64), sizeBytes: 1, releaseAdmissionId: 'admission-a', expiresAt: '2100-01-01T00:00:00Z', channel: 'stable' as const, reason: null, }], catalogGeneration: 1, etag: '"plugins-1"', stale: false, })); const client = { templates: vi.fn(async () => ['character']), generate: vi.fn(async () => generation()), get: vi.fn(async () => generation('succeeded')), cancel: vi.fn(async () => generation('cancelled')), download: vi.fn(async () => ({ bytes: new Uint8Array([1, 2, 3]), fileName: 'hero.png', contentType: 'image/png', })), }; const delivery = new GameResourceDeliveryCoordinator({ client: client as unknown as GameResourceClient, receipts: new GameResourceDeliveryReceiptStore(path.join(userDataDir, 'receipts.json')), leases: new PiProjectWriteLeaseCoordinator(), sleep: async () => undefined, }); const adapter = new GameResourcePluginAdapter({ client: client as unknown as GameResourceClient, delivery, marketplace: { resolve } as unknown as MarketplacePackageClientPort, packageStore: { getInstalled: vi.fn(async () => ({ pluginId: 'makelore.game-resource', releaseId: RELEASE_ID, version: '1.0.0', sha256: 'a'.repeat(64), channel: 'stable', })), getInstalledRelease: vi.fn(async () => ({ pluginId: 'makelore.game-resource', releaseId: RELEASE_ID, version: '1.0.0', sha256: 'a'.repeat(64), sizeBytes: 1, channel: 'stable', })), } as unknown as Pick, makeloreVersion: '2.0.0', }); const context: TrustedCodingCapabilityContext = { conversationId: 'conversation-a', runId: 'run-a', resourceId: 'resource-a', requestId: 'pi:run-a:resource-a', localProjectId: 'local-a', projectPath, durableProjectId: PROJECT_ID, workerRole: 'parent', effectiveSkillIds: ['game-resource'], pluginReleaseId: RELEASE_ID, }; return { adapter, client, context, projectPath, resolve }; } afterEach(async () => { await Promise.all(roots.splice(0).map(async (root) => await rm(root, { recursive: true, force: true }))); }); describe('GameResourcePluginAdapter', () => { it('resolves admission, hides polling, and automatically saves generated output in the project', async () => { const { adapter, client, context, projectPath, resolve } = await fixture(); await writeFile(path.join(projectPath, 'reference.png'), new Uint8Array([4, 5, 6])); const onProgress = vi.fn(); const result = await adapter.invoke(context, tool('game_resource_generate'), { kind: 'pixel', templateName: 'character', requirement: 'Blue-armored hero', confirmed: true, referencePaths: ['reference.png'], }, onProgress); expect(resolve).toHaveBeenCalledWith(expect.objectContaining({ resolveRequestId: 'pi:run-a:resource-a', channel: 'stable', installed: [{ pluginId: 'makelore.game-resource', releaseId: RELEASE_ID, sha256: 'a'.repeat(64) }], })); expect(client.generate).toHaveBeenCalledWith(expect.objectContaining({ releaseAdmissionId: 'admission-a', releaseId: RELEASE_ID, projectId: PROJECT_ID, logicalOperationId: 'pi:run-a:resource-a', referenceFiles: [{ name: 'reference.png', mimeType: 'image/png', dataBase64: 'BAUG' }], })); expect(result).toMatchObject({ success: true, status: 200, billing, data: { executionId: EXECUTION_ID, providerStatus: 'succeeded', deliveryStatus: 'saved', files: [{ path: `assets/generated/game-resource/${EXECUTION_ID}/output-1.png`, bytes: 3, }], }, }); expect(client.get).toHaveBeenCalledTimes(1); expect(client.download).toHaveBeenCalledWith(EXECUTION_ID, 0); await expect(readFile(path.join( projectPath, 'assets', 'generated', 'game-resource', EXECUTION_ID, 'output-1.png', ))).resolves.toEqual(Buffer.from([1, 2, 3])); expect(onProgress.mock.calls.map(([progress]) => progress.data.phase)).toEqual([ 'submitted', 'generating', 'saving', 'saved', ]); expect(JSON.stringify(result)).not.toContain(projectPath); }); it('does not resolve admission or reserve usage before explicit generation confirmation', async () => { const { adapter, client, context, resolve } = await fixture(); await expect(adapter.invoke(context, tool('game_resource_generate'), { kind: 'pixel', templateName: 'character', requirement: 'Blue-armored hero', confirmed: false, })).resolves.toMatchObject({ success: false, code: 'confirmation_required' }); expect(resolve).not.toHaveBeenCalled(); expect(client.generate).not.toHaveBeenCalled(); }); it('lists server-owned templates through the same release admission', async () => { const { adapter, context, client } = await fixture(); await expect(adapter.invoke(context, tool('game_resource_templates'), { kind: 'pixel' })) .resolves.toMatchObject({ success: true, data: { kind: 'pixel', templates: ['character'] } }); expect(client.templates).toHaveBeenCalledWith({ releaseId: RELEASE_ID, releaseAdmissionId: 'admission-a', kind: 'pixel', }); }); it('fails closed when a worker has no frozen release instead of using the current package', async () => { const { adapter, context, client, resolve } = await fixture(); await expect(adapter.invoke( { ...context, pluginReleaseId: undefined }, tool('game_resource_templates'), { kind: 'pixel' }, )).resolves.toMatchObject({ success: false, status: 409, code: 'plugin_release_unavailable', retryable: false, }); expect(resolve).not.toHaveBeenCalled(); expect(client.templates).not.toHaveBeenCalled(); }); });