// @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 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: name === 'game_resource_status' ? 'read' : 'write', projectWriteLease: name === 'game_resource_save_output', permissions: [`hosted.game-resource.${operation.replaceAll('_', '-')}`], inputSchema: { type: 'object' }, }; } async function fixture() { const projectPath = await mkdtemp(path.join(tmpdir(), 'makelore-game-resource-')); roots.push(projectPath); 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 adapter = new GameResourcePluginAdapter({ client: client as unknown as GameResourceClient, 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 a fresh admission and keeps project reference bytes behind the hosted boundary', async () => { const { adapter, client, context, projectPath, resolve } = await fixture(); await writeFile(path.join(projectPath, 'reference.png'), new Uint8Array([4, 5, 6])); const result = await adapter.invoke(context, tool('game_resource_generate'), { kind: 'pixel', templateName: 'character', requirement: 'Blue-armored hero', confirmed: true, referencePaths: ['reference.png'], }); 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: 202, billing, data: { executionId: EXECUTION_ID, status: 'accepted', outputCount: 0 }, }); 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('requires explicit confirmation and never overwrites a project file', async () => { const { adapter, client, context, projectPath } = await fixture(); const saveTool = tool('game_resource_save_output', 'game-resource.library', 'save_output'); await expect(adapter.invoke(context, saveTool, { executionId: EXECUTION_ID, relativePath: 'assets/hero.png', confirmed: false, })).resolves.toMatchObject({ success: false, code: 'confirmation_required' }); expect(client.download).not.toHaveBeenCalled(); await expect(adapter.invoke(context, saveTool, { executionId: EXECUTION_ID, relativePath: 'assets/hero.png', confirmed: true, })).resolves.toMatchObject({ success: true, data: { savedPath: 'assets/hero.png', bytes: 3 }, }); await expect(readFile(path.join(projectPath, 'assets/hero.png'))).resolves.toEqual(Buffer.from([1, 2, 3])); await expect(adapter.invoke(context, saveTool, { executionId: EXECUTION_ID, relativePath: 'assets/hero.png', confirmed: true, })).resolves.toMatchObject({ success: false, code: 'game_resource_destination_exists' }); await expect(readFile(path.join(projectPath, 'assets/hero.png'))).resolves.toEqual(Buffer.from([1, 2, 3])); }); 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(); }); });