// @vitest-environment jsdom import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import type { GameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp'; import type { TauriInvoke } from '../src/app/types'; import { generateGameDistributionCover, publishLocalProjectGame, readGameCoverGenerationPrice, readGamePublishAvailability, suggestGameDistributionPublishMetadata, } from '../src/services/gameDistributionPublish'; const MANIFEST = { projectId: 'local-proj-1', name: '星轨防线', goal: '守住轨道城', } as unknown as GameCreationAppManifest; const PUBLISH_RESULT = { gameId: 'game_1', versionId: 'gamever_1', versionNumber: 1, status: 'pending_review', packageSha256: 'a'.repeat(64), packageSizeBytes: 1024, fileCount: 1, }; type InvokeCall = { command: string; args: Record | undefined; }; function installNativeInvoke( implementation: (command: string, args?: Record) => unknown, ) { const calls: InvokeCall[] = []; const invoke = vi.fn( async (command: string, args?: Record) => { calls.push({ command, args }); return implementation(command, args); }, ) as unknown as TauriInvoke; return { invoke, calls }; } beforeEach(() => { window.localStorage.clear(); }); afterEach(() => { vi.useRealTimers(); delete (window as unknown as { __TAURI__?: unknown }).__TAURI__; }); describe('Rust 发布 facade', () => { test('发布只通过一个 native command 传递本地路径、资料和幂等键', async () => { const { invoke, calls } = installNativeInvoke((command) => { if (command === 'publish_local_project_game') return PUBLISH_RESULT; throw new Error(`未预期的命令:${command}`); }); const result = await publishLocalProjectGame({ invoke, projectPath: '/tmp/project', packageRelativePath: 'exports/playtest-package-1.zip', manifest: MANIFEST, metadata: { coverAssetId: 'asset_cover', screenshots: ['asset_shot_1', 'asset_shot_2'], }, idempotencyKey: 'agc-publish-test', }); expect(result).toEqual(PUBLISH_RESULT); expect(calls).toHaveLength(1); expect(calls[0]?.command).toBe('publish_local_project_game'); expect(calls[0]?.args).toMatchObject({ projectPath: '/tmp/project', packageRelativePath: 'exports/playtest-package-1.zip', idempotencyKey: 'agc-publish-test', metadata: { localProjectId: 'local-proj-1', coverAssetId: 'asset_cover', screenshots: ['asset_shot_1', 'asset_shot_2'], }, }); expect(calls[0]?.args).not.toHaveProperty('accessToken'); expect(calls[0]?.args).not.toHaveProperty('apiBaseUrl'); expect(calls[0]?.args).not.toHaveProperty('packageBytes'); }); test('缺少本地项目标识时在发起 native command 前失败关闭', async () => { const invoke = vi.fn() as unknown as TauriInvoke; await expect( publishLocalProjectGame({ invoke, projectPath: '/tmp/project', packageRelativePath: 'exports/playtest-package-1.zip', manifest: { ...MANIFEST, projectId: ' ' } as GameCreationAppManifest, metadata: { coverAssetId: 'asset_cover' }, }), ).rejects.toThrow('发布需要本地项目标识'); expect(invoke).not.toHaveBeenCalled(); }); test('缺少封面时在创建 native 发布任务前失败关闭', async () => { const invoke = vi.fn() as unknown as TauriInvoke; await expect( publishLocalProjectGame({ invoke, projectPath: '/tmp/project', packageRelativePath: 'exports/playtest-package-1.zip', manifest: MANIFEST, metadata: { coverAssetId: ' ' }, }), ).rejects.toThrow('请先选择游戏封面'); expect(invoke).not.toHaveBeenCalled(); }); test('截图超过 6 张时在 native command 前失败关闭', async () => { const invoke = vi.fn() as unknown as TauriInvoke; await expect( publishLocalProjectGame({ invoke, projectPath: '/tmp/project', packageRelativePath: 'exports/playtest-package-1.zip', manifest: MANIFEST, metadata: { coverAssetId: 'asset_cover', screenshots: Array.from( { length: 7 }, (_, index) => `asset_${index}`, ), }, }), ).rejects.toThrow('游戏截图最多 6 张'); expect(invoke).not.toHaveBeenCalled(); }); test('发布灰度、资料建议和价格均通过 native command', async () => { const { invoke, calls } = installNativeInvoke((command) => { if (command === 'read_game_publish_availability') return true; if (command === 'suggest_game_distribution_publish_metadata') { return { summary: '驾驶炮台守住轨道城', category: '策略' }; } if (command === 'read_game_cover_generation_price') return 5; throw new Error(`未预期的命令:${command}`); }); await expect(readGamePublishAvailability(invoke)).resolves.toBe(true); await expect( suggestGameDistributionPublishMetadata( { name: '星轨防线', goal: '守住轨道城', context: '当前任务与状态:原型已完成;现有素材:image:assets/hero.png', }, invoke, ), ).resolves.toEqual({ summary: '驾驶炮台守住轨道城', category: '策略' }); await expect( readGameCoverGenerationPrice( { model: 'gpt-image-2', imageSize: '2K' }, invoke, ), ).resolves.toBe(5); expect(calls).toEqual([ { command: 'read_game_publish_availability', args: undefined }, { command: 'suggest_game_distribution_publish_metadata', args: { name: '星轨防线', goal: '守住轨道城', context: '当前任务与状态:原型已完成;现有素材:image:assets/hero.png', }, }, { command: 'read_game_cover_generation_price', args: { model: 'gpt-image-2', imageSize: '2K' }, }, ]); }); test('封面生成由 Rust 等待队列后一次性返回已登记素材', async () => { const { invoke, calls } = installNativeInvoke((command) => { if (command === 'generate_game_distribution_cover') { return { assetObjectId: 'asset_generated_cover', previewUrl: 'https://assets.example.com/generated-cover.png', taskId: 'task_cover_1', model: 'gpt-image-2', }; } throw new Error(`未预期的命令:${command}`); }); await expect( generateGameDistributionCover( { prompt: '为《星轨防线》生成游戏封面', model: 'gpt-image-2', aspectRatio: '16:9', imageSize: '2K', }, invoke, ), ).resolves.toEqual({ assetObjectId: 'asset_generated_cover', previewUrl: 'https://assets.example.com/generated-cover.png', taskId: 'task_cover_1', model: 'gpt-image-2', }); expect(calls).toEqual([ { command: 'generate_game_distribution_cover', args: { prompt: '为《星轨防线》生成游戏封面', model: 'gpt-image-2', aspectRatio: '16:9', imageSize: '2K', assetLabel: '游戏封面', }, }, ]); }); test('缺少 native 宿主时发布网络 facade 失败关闭', async () => { await expect(readGamePublishAvailability()).rejects.toThrow( '需要在 Tauri App 内执行发布操作', ); }); });