import type { ProjectResourceCanvasLayout, ProjectResourceCanvasPosition, } from '../../../../packages/shared/src/contracts/gameCreationApp'; import { consumeInitialGameChatMessage, latestGameChatPlayableRevision, } from '../../src/App'; import type { AgentRuntimeEventRecord, AgentRuntimeState, } from '../../src/app/types'; import { MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE } from '../../src/features/agent-runtime/model'; import { buildGameChatProgressEvidence, collectGameChatResultImages, collectGameChatRuntimeEvents, formatGameChatStageRecord, gameChatFinalReplyMessages, gameChatRuntimeEventMessages, isGameChatStageRecordMessage, mergeGameChatFinalReplyMessagesIntoHistory, SupervisorChatOnlyView, } from '../../src/features/project-workspace/SupervisorChatOnlyView'; import { RESOURCE_CANVAS_CARD_WIDTH, RESOURCE_CANVAS_COLUMN_GAP, } from '../../src/view/project-development/resourceCanvasLayoutModel'; import { act, agentRuntimeUserInputRequest, App, cleanup, createGameCreationAppManifest, createGameCreationAppSeedTasks, createProjectSupervisorRuntimeHarness, emptyProjectPolicy, expect, fireEvent, GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, GameChatReleaseApp, type GameCreationAgentRunTrace, it, mockRoleAgentReply, ProjectDevelopmentView, projectSupervisorResponseStream, React, readFileSync, render, renderAppAt, renderLauncherProjectsAt, resolve, screen, submitChat, vi, waitFor, within, } from './harness'; function resourceGraphForInputs(args?: Record) { const resources = (args?.resources as | Array<{ resourceId: string; producerTaskId: string | null }> | undefined) ?? []; return { resourceIds: resources.map(({ resourceId }) => resourceId), referenceEdges: [], taskFlows: [], connectionIndex: resources.map(({ resourceId }) => ({ resourceId, upstreamReferenceResourceIds: [], downstreamReferenceResourceIds: [], referenceEdgeIds: [], taskFlowIds: [], })), producerAssignments: resources.flatMap((resource) => resource.producerTaskId ? [ { resourceId: resource.resourceId, taskId: resource.producerTaskId, }, ] : [], ), dependencyDepths: resources.map((resource) => ({ resourceId: resource.resourceId, dependencyDepth: 0, })), unresolvedReferenceResourceIds: [], cyclicResourceIds: [], cyclicTaskIds: [], producerMappingTruncated: false, }; } function gameChatRuntimeEvent({ agentId = 'project-supervisor', taskId = agentId, sessionId = `${agentId}-session`, runId = 'game-chat-supervisor-run', eventType = 'agent.progress', status = 'running', phase = 'execution', summary, detail = null, updatedAt, eventId = `${agentId}-${runId}-${updatedAt}-${eventType}`, publicText = summary, }: { agentId?: string; taskId?: string; sessionId?: string; runId?: string; eventType?: string; status?: string; phase?: string; summary: string; detail?: string | null; eventId?: string; publicText?: string | null; updatedAt: number; }): AgentRuntimeEventRecord { return { schemaVersion: 'game-creator-runtime-event.v1', agentId, taskId, sessionId, runId, source: agentId === 'project-supervisor' ? 'project-supervisor-game-chat' : 'agent-delegate', eventId, eventType, status, phase, summary, detail, publicText, updatedAt, }; } function gameChatRuntimeState( overrides: Partial = {}, ): AgentRuntimeState { return { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'project-supervisor', taskId: 'project-supervisor', sessionId: 'game-chat-supervisor-session', runId: 'game-chat-supervisor-run', source: 'project-supervisor-game-chat', status: 'running', phase: 'execution', currentTask: '生成首版游戏', currentGoal: '完成可运行原型', currentAction: '执行当前计划', waitingOn: '', nextStep: '继续执行', plan: [], observations: [], allowedTools: [], pendingToolAction: null, lastResponse: null, error: null, updatedAt: 1, ...overrides, }; } const GAME_CHAT_STAGE_TASK_IDS = [ 'design-director', 'art-director', 'art-asset-plan', 'code-director', 'code-prototype', 'preview-readiness', 'preview-playtest', ] as const; function isGameChatStageTask(taskId: string) { return (GAME_CHAT_STAGE_TASK_IDS as readonly string[]).includes(taskId); } function gameChatPreviewPlaytestRuntime({ parentRunId, revision, updatedAt, }: { parentRunId: string; revision: number; updatedAt: number; }): AgentRuntimeState { const runId = `preview-playtest-${parentRunId}-${revision}`; const sessionId = `preview-playtest-session-${revision}`; return gameChatRuntimeState({ agentId: 'preview-playtest', taskId: 'preview-playtest', sessionId, runId, source: 'agent-ready-task-scheduler', parentAgentId: 'project-supervisor', parentRunId, status: 'completed', phase: 'completed', currentTask: '执行浏览器试玩验证', currentGoal: '确认当前 revision 可以试玩', currentAction: '浏览器试玩验证已通过', recentEvents: [ gameChatRuntimeEvent({ agentId: 'preview-playtest', taskId: 'preview-playtest', sessionId, runId, eventType: 'observation', status: 'completed', phase: 'tool-observation', summary: 'preview.validate:ok · 浏览器验证已通过', detail: JSON.stringify({ diagnosticsCount: 0, passed: true, playtestPassed: true, revision, }), updatedAt, }), ], updatedAt, }); } function renderGameChatStatus({ runtime, runtimeByAgentId = {}, manifest = null, viewOverrides = {}, }: { runtime: AgentRuntimeState; runtimeByAgentId?: Record; manifest?: ReturnType | null; viewOverrides?: Partial>; }) { return render( React.createElement(SupervisorChatOnlyView, { chatAgentBusy: false, chatInput: '', messagesRef: React.createRef(), onCancelConfirmation: vi.fn(), onChatInputChange: vi.fn(), onCloseRuntimeConfig: vi.fn(), onConfirmConfirmation: vi.fn(), onOpenRuntimeConfig: vi.fn(), onScroll: vi.fn(), onShowEarlierMessages: vi.fn(), onSubmit: vi.fn(), onToolAction: vi.fn(), onUserInput: vi.fn(), pendingConfirmation: null, projectPath: '/tmp/game-chat-status', runtime, runtimeConfigOpen: false, runtimeError: '', transientReply: '', hasConversationControls: false, hiddenConversationCount: 0, needsUserInput: false, visibleMessages: [], workspaceStatus: '已打开', gameChatMode: true, runtimeByAgentId, manifest, projectReady: true, ...viewOverrides, }), ); } function createDeferred() { let resolve!: (value: T | PromiseLike) => void; let reject!: (reason?: unknown) => void; const promise = new Promise((nextResolve, nextReject) => { resolve = nextResolve; reject = nextReject; }); return { promise, reject, resolve }; } type GameChatPreviewFixture = { port: number; root: string; url: string; }; async function renderGameChatAutoPreviewDriver({ projectPath, port, readPolicy, startPreview, }: { projectPath: string; port: number; readPolicy?: (callIndex: number) => Promise>; startPreview?: ( args: Record, callIndex: number, ) => Promise; }) { let backgroundRuntimes: Array> = []; const harness = createProjectSupervisorRuntimeHarness({ projectPath, runtimeMapLoader: async () => backgroundRuntimes, }); const manifest = createGameCreationAppManifest( projectPath.split(/[\\/]/u).filter(Boolean).at(-1) ?? 'game-chat-race', 'game-chat-race', ); manifest.tasks = manifest.tasks.map((task) => task.id === 'code-prototype' ? { ...task, status: 'completed' as const } : task, ); let parentRunId = ''; let currentPreview: GameChatPreviewFixture | null = null; let policyCallCount = 0; let startCallCount = 0; const stoppedPreviews: GameChatPreviewFixture[] = []; const invoke = vi.fn( async (command: string, args: Record = {}) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: manifest.name, recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'get_local_game_preview_status') { return currentPreview ? { status: 'running', ...currentPreview } : { status: 'stopped', url: null, port: null, root: null }; } if (command === 'get_local_game_manifest') { return manifest; } if (command === 'read_project_permission_policy') { policyCallCount += 1; return readPolicy ? readPolicy(policyCallCount) : { path: '.agent/policy.json', policy: { deniedCommands: [], confirmCommands: [] }, }; } if (command === 'start_local_game_preview') { startCallCount += 1; const started = startPreview ? await startPreview(args, startCallCount) : { url: `http://127.0.0.1:${port}`, port, root: `${projectPath}/game`, }; currentPreview = started; return started; } if (command === 'stop_local_game_preview_if_matches') { const expected = args.expectedPreview as GameChatPreviewFixture; const matches = currentPreview?.url === expected?.url && currentPreview.port === expected.port && currentPreview.root === expected.root; if (matches) { stoppedPreviews.push(expected); currentPreview = null; } return matches; } if (command === 'start_game_creator_supervisor_runtime_task') { parentRunId = String(args.runId ?? ''); } return harness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: harness.listen }, }; render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, gameChatOnly: true, }), ); const surface = await screen.findByLabelText('游戏创作聊天'); const composer = within(surface).getByLabelText('项目总控对话内容'); const send = within(surface).getByRole('button', { name: '发送' }); await waitFor(() => { expect((composer as HTMLTextAreaElement).disabled).toBe(false); expect((send as HTMLButtonElement).disabled).toBe(false); }); const submit = async (prompt: string) => { const previousSubmissionCount = invoke.mock.calls.filter( ([command]) => command === 'start_game_creator_supervisor_runtime_task' || command === 'steer_game_creator_agent_runtime_task', ).length; fireEvent.change(composer, { target: { value: prompt } }); fireEvent.click(send); await waitFor(() => { expect( invoke.mock.calls.filter( ([command]) => command === 'start_game_creator_supervisor_runtime_task' || command === 'steer_game_creator_agent_runtime_task', ), ).toHaveLength(previousSubmissionCount + 1); }); await waitFor(() => { expect((send as HTMLButtonElement).disabled).toBe(false); }); }; const emitValidation = async (revision: number, validatedAt: number) => { harness.setProjectRevision(revision); const runtime = gameChatPreviewPlaytestRuntime({ parentRunId, revision, updatedAt: validatedAt, }); backgroundRuntimes = [runtime]; await act(async () => { harness.emitAgentRuntime(runtime); await Promise.resolve(); }); }; const readAuthorization = () => { const raw = window.localStorage.getItem( 'genarrative.game-chat.auto-preview-authorization.v2', ); return raw ? (JSON.parse(raw) as Record) : null; }; return { emitValidation, get currentPreview() { return currentPreview; }, get parentRunId() { return parentRunId; }, harness, invoke, readAuthorization, stoppedPreviews, submit, surface, }; } async function assertNewGameChatAuthorizationSupersedesDeferredAttempt( deferredStage: 'policy' | 'start', ) { const projectPath = `/tmp/game-chat-deferred-${deferredStage}`; const deferredPolicy = createDeferred>(); const deferredStart = createDeferred(); const policyResult = { path: '.agent/policy.json', policy: { deniedCommands: [], confirmCommands: [] }, }; const staleStartedPreview = { url: 'http://127.0.0.1:4391', port: 4391, root: `${projectPath}/game`, }; const driver = await renderGameChatAutoPreviewDriver({ projectPath, port: 4391, readPolicy: async (callIndex) => deferredStage === 'policy' && callIndex === 1 ? deferredPolicy.promise : policyResult, startPreview: async (args, callIndex) => { expect(args).toEqual({ projectPath, expectedRevision: deferredStage === 'policy' ? 2 : callIndex, }); if (deferredStage === 'start' && callIndex === 1) { return deferredStart.promise; } return { url: 'http://127.0.0.1:4391', port: 4391, root: `${projectPath}/game`, }; }, }); await driver.submit('生成首个可玩版本'); await driver.emitValidation(1, 1000); await waitFor(() => { const reachedDeferredStage = driver.invoke.mock.calls.some( ([command]) => command === (deferredStage === 'policy' ? 'read_project_permission_policy' : 'start_local_game_preview'), ); expect(reachedDeferredStage).toBe(true); }, { timeout: 2_500 }); const firstAuthorization = driver.readAuthorization(); expect(firstAuthorization?.authorizationId).toEqual(expect.any(String)); await driver.submit('同一 Run 内替换自动预览授权'); await waitFor(() => { const nextAuthorization = driver.readAuthorization(); expect(nextAuthorization?.authorizationId).toEqual(expect.any(String)); expect(nextAuthorization?.authorizationId).not.toBe( firstAuthorization?.authorizationId, ); expect(nextAuthorization).toMatchObject({ afterRevision: 1, afterValidatedAt: 1000, projectPath, runId: driver.parentRunId, }); }); const replacementAuthorizationId = driver.readAuthorization()?.authorizationId; await act(async () => { if (deferredStage === 'policy') { deferredPolicy.resolve(policyResult); await deferredPolicy.promise; } else { deferredStart.resolve(staleStartedPreview); await deferredStart.promise; } await Promise.resolve(); await Promise.resolve(); }); if (deferredStage === 'start') { await waitFor(() => { expect(driver.stoppedPreviews).toEqual([staleStartedPreview]); }); expect(driver.invoke).toHaveBeenCalledWith( 'stop_local_game_preview_if_matches', { projectPath, expectedPreview: staleStartedPreview }, ); } else { expect( driver.invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(0); } expect(driver.readAuthorization()?.authorizationId).toBe( replacementAuthorizationId, ); expect(screen.queryByLabelText('游戏运行')).toBeNull(); await driver.emitValidation(2, 2000); expect( await screen.findByLabelText('游戏运行', {}, { timeout: 3000 }), ).not.toBeNull(); const expectedStartCount = deferredStage === 'start' ? 2 : 1; await waitFor(() => { expect( driver.invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(expectedStartCount); }); expect( driver.invoke.mock.calls .filter(([command]) => command === 'start_local_game_preview') .at(-1), ).toEqual(['start_local_game_preview', { projectPath, expectedRevision: 2 }]); expect(driver.readAuthorization()).toBeNull(); } export function registerProjectWorkbenchFoundationTests() { it('renders the first project workbench slice with honest disabled run and local approval UI', () => { const manifest = createGameCreationAppManifest( 'workbench-draft', '陶泥儿工作台测试', ); expect( manifest.tasks.find((task) => task.id === 'design-foundation'), ).toMatchObject({ title: '确定玩法规格与界面原型', artifacts: [ 'memory/project.md', 'game/game_design.md', 'assets/ui-prototype.png', ], acceptanceCriteria: [ '核心循环、胜负条件和第一版关卡目标明确,且已基于规范图生成可读的 16:9 横屏界面原型图', ], }); expect( manifest.tasks.find((task) => task.id === 'art-asset-plan'), ).toMatchObject({ title: '生成首版美术素材', artifacts: ['assets/manifest.art.json', 'assets/art-spritesheet.png'], acceptanceCriteria: [ '角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记', ], }); render( React.createElement(ProjectDevelopmentView, { projectName: '陶泥儿工作台测试', projectPath: '/tmp/workbench-draft', manifest, attachments: [ { fileName: 'broken-reference.png', mediaType: 'image/png', status: 'failed', error: '图片解码失败', }, ], recentRunStatus: null, recentRunStopReason: null, supervisor: React.createElement( 'div', { 'aria-label': '测试项目总控' }, '项目总控对话内容', ), onHomeOpen: vi.fn(), onProjectsOpen: vi.fn(), }), ); expect(screen.getByLabelText('项目开发工作台')).not.toBeNull(); expect(screen.getByLabelText('项目主视窗')).not.toBeNull(); expect(screen.getByLabelText('陶泥儿 Agent 对话')).not.toBeNull(); expect(screen.getByLabelText('测试项目总控')).not.toBeNull(); expect(screen.getByLabelText('子 Agent 状态栏')).not.toBeNull(); expect(screen.getByRole('article', { name: /策划 Agent/ })).not.toBeNull(); expect(screen.getByRole('article', { name: /美术 Agent/ })).not.toBeNull(); expect(screen.getByRole('article', { name: /程序 Agent/ })).not.toBeNull(); const runTab = screen.getByRole('tab', { name: '运行', }) as HTMLButtonElement; expect(runTab.disabled).toBe(false); expect(runTab.getAttribute('data-unavailable')).toBe('true'); fireEvent.click(runTab); expect(runTab.getAttribute('aria-selected')).toBe('false'); expect( screen.getByText('首个可运行原型尚未完成,运行视图暂不可用'), ).not.toBeNull(); expect(screen.getByLabelText('附件导入失败')).not.toBeNull(); expect(screen.getByText('broken-reference.png')).not.toBeNull(); expect(screen.getByText('图片解码失败')).not.toBeNull(); fireEvent.click( screen.getByRole('button', { name: '审批配置,当前严格审批', }), ); expect( screen.getByRole('dialog', { name: '陶泥儿的操作权限' }), ).not.toBeNull(); const riskApproval = screen.getByRole('radio', { name: /风险审批/ }); fireEvent.click(riskApproval); expect(riskApproval.getAttribute('aria-checked')).toBe('false'); expect(riskApproval.getAttribute('data-unavailable')).toBe('true'); expect(screen.getAllByText('Rank 规则待定,当前暂不可用')).toHaveLength(2); fireEvent.click(screen.getByRole('button', { name: '完成' })); expect( screen.getByRole('button', { name: '审批配置,当前严格审批', }), ).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '更多小组' })); expect(screen.getByRole('article', { name: /数值 Agent/ })).not.toBeNull(); expect(screen.getByRole('article', { name: /音频 Agent/ })).not.toBeNull(); expect(screen.getByRole('article', { name: /发布 Agent/ })).not.toBeNull(); }); it('renders immutable manifest versions, their parent graph, and bound asset highlights', async () => { const manifest = createGameCreationAppManifest( 'workbench-versions', '版本工作台测试', ); manifest.assets = [ { id: 'asset-player', kind: 'character', mediaType: 'image/png', localPath: 'assets/player.png', source: { kind: 'generated' }, }, ]; manifest.versions = [ { versionId: 'version-root', parentVersionId: null, projectRevision: 3, resourceBindings: [{ slotId: 'player', resourceId: 'asset-player' }], createdReason: 'initial', createdAt: 100, }, { versionId: 'version-child', parentVersionId: 'version-root', projectRevision: 4, resourceBindings: [ { slotId: 'player', resourceId: 'asset-player' }, { slotId: 'historical', resourceId: 'asset-removed' }, ], createdReason: 'agent-revision', createdAt: 200, }, ]; render( React.createElement(ProjectDevelopmentView, { projectName: manifest.name, projectPath: '/tmp/workbench-versions', manifest, attachments: [], recentRunStatus: null, recentRunStopReason: null, supervisor: React.createElement('div', null, '项目总控'), onHomeOpen: vi.fn(), onProjectsOpen: vi.fn(), }), ); fireEvent.click(screen.getByRole('button', { name: '按类型' })); const resourceCards = screen.getAllByTitle('打开资源详情'); const rootVersionCard = resourceCards.find((card) => card.textContent?.includes('版本 1'), ); const childVersionCard = resourceCards.find((card) => card.textContent?.includes('版本 2'), ); expect(rootVersionCard?.textContent).toContain('1 个直接子版本'); expect(childVersionCard?.textContent).toContain('父版本 version-root'); fireEvent.click(childVersionCard!); const versionFocus = screen.getByRole('region', { name: '版本 2' }); expect(within(versionFocus).getByText('version-child')).not.toBeNull(); expect(within(versionFocus).getByText('version-root')).not.toBeNull(); expect(within(versionFocus).getByText('Agent 修订')).not.toBeNull(); expect( within(versionFocus).getByText( 'player → asset-player;historical → asset-removed', ), ).not.toBeNull(); expect(screen.queryByText('asset:asset-removed')).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '收起资源' })); const playerCard = screen .getAllByTitle('打开资源详情') .find((card) => card.textContent?.includes('player.png')); expect(playerCard?.classList.contains('is-relation-version-binding')).toBe( true, ); fireEvent.click(screen.getByRole('button', { name: '按依赖' })); await waitFor(() => { const dependencyPlayerCard = screen .getAllByTitle('打开资源详情') .find((card) => card.textContent?.includes('player.png')); expect( dependencyPlayerCard?.classList.contains('is-relation-version-binding'), ).toBe(true); }); }); it('opens text receipts in the central focus state and restores the resource list context', () => { const manifest = createGameCreationAppManifest( 'workbench-resource-details', '不应显示的项目标题', ); render( React.createElement(ProjectDevelopmentView, { projectName: '不应显示的项目标题', projectPath: '/tmp/workbench-resource-details', manifest, attachments: [], recentRunStatus: null, recentRunStopReason: null, agentRuntimeSummaries: [ { group: 'art', label: '美术 Agent', status: 'completed', statusLabel: '已完成', currentTask: '本轮工作已完成', currentAction: null, waitingOn: null, completedCount: 4, totalCount: 4, }, ], agentResults: [ { agentId: 'design-foundation', runId: 'design-result-run', label: '玩法策划 Agent', title: '玩法策划 Agent 文本回执', content: '策划回执正文', updatedAt: 1, }, { agentId: 'art-asset-plan', runId: 'art-result-run', label: '美术资源计划 Agent', title: '美术资源计划 Agent 文本回执', content: '# 美术计划\n\n- 仅有美术计划\n- 没有图片文件\n\n**等待实际素材生成。**', updatedAt: 2, }, ], supervisor: React.createElement('div', null, '项目总控'), onHomeOpen: vi.fn(), onProjectsOpen: vi.fn(), }), ); expect(screen.queryByText('不应显示的项目标题')).toBeNull(); expect(screen.queryByRole('button', { name: '回首页' })).toBeNull(); expect(screen.queryByRole('button', { name: '项目组' })).toBeNull(); expect(screen.getByText('仅完成计划')).not.toBeNull(); expect( screen.getByText('美术资源计划已完成,尚未生成或登记图片'), ).not.toBeNull(); fireEvent.click(screen.getByRole('button', { name: '按类型' })); const searchInput = screen.getByLabelText( '搜索项目资源', ) as HTMLInputElement; fireEvent.change(searchInput, { target: { value: '美术资源计划' } }); const resourceCanvas = screen.getByLabelText( '资源类型视图', ) as HTMLDivElement; resourceCanvas.scrollLeft = 48; resourceCanvas.scrollTop = 36; const resourceCards = screen.getAllByTitle('打开资源详情'); const artReceiptCard = resourceCards.find((card) => card.textContent?.includes('美术资源计划 Agent 文本回执'), ); expect(artReceiptCard).not.toBeUndefined(); const originalStyle = artReceiptCard?.getAttribute('style'); fireEvent.pointerDown(artReceiptCard!, { pointerId: 7, button: 0, clientX: 0, clientY: 0, }); fireEvent.pointerMove(artReceiptCard!, { pointerId: 7, clientX: 240, clientY: 32, }); fireEvent.pointerUp(artReceiptCard!, { pointerId: 7, clientX: 240, clientY: 32, }); fireEvent.pointerCancel(artReceiptCard!, { pointerId: 7 }); expect(artReceiptCard?.getAttribute('style')).toBe(originalStyle); expect(artReceiptCard?.classList.contains('is-dragging')).toBe(false); const styles = readFileSync( resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); expect(styles).toMatch( /\.game-resource-card\s*\{[^}]*cursor:\s*pointer[^}]*touch-action:\s*manipulation/s, ); expect(styles).not.toMatch(/\.game-resource-card\.is-dragging/); fireEvent.click(artReceiptCard!); const workbenchStage = screen.getByLabelText('项目主视窗'); expect(workbenchStage.getAttribute('data-resource-view-state')).toBe( 'resources.focused.document', ); expect(screen.queryByLabelText('搜索项目资源')).toBeNull(); expect(screen.queryByRole('button', { name: '按类型' })).toBeNull(); expect(screen.getByLabelText('陶泥儿 Agent 对话')).not.toBeNull(); expect(screen.getByLabelText('子 Agent 状态栏')).not.toBeNull(); const receiptFocus = screen.getByRole('region', { name: '美术资源计划 Agent 文本回执', }); expect( within(receiptFocus).getByRole('heading', { name: '美术计划' }), ).not.toBeNull(); expect(within(receiptFocus).getByText('仅有美术计划')).not.toBeNull(); expect(within(receiptFocus).getByText('没有图片文件')).not.toBeNull(); expect(within(receiptFocus).getByText('等待实际素材生成。')).not.toBeNull(); expect( receiptFocus.querySelector('.game-resource-focus-body'), ).not.toBeNull(); expect(receiptFocus.querySelector('ul')).not.toBeNull(); expect(receiptFocus.querySelector('strong')).not.toBeNull(); expect(receiptFocus.closest('.game-workbench-stage')).toBe(workbenchStage); expect(receiptFocus.closest('.game-resource-focus-layer')).toBeNull(); expect(styles).not.toMatch( /\.game-resource-focus-titlebar\s*\{[^}]*cursor:/s, ); fireEvent.click( within(receiptFocus).getByRole('button', { name: '收起资源' }), ); expect(workbenchStage.getAttribute('data-resource-view-state')).toBe( 'resources.list', ); expect( (screen.getByLabelText('搜索项目资源') as HTMLInputElement).value, ).toBe('美术资源计划'); expect( screen .getByRole('button', { name: '按类型' }) .getAttribute('aria-pressed'), ).toBe('true'); const restoredCanvas = screen.getByLabelText( '资源类型视图', ) as HTMLDivElement; expect(restoredCanvas.scrollLeft).toBe(48); expect(restoredCanvas.scrollTop).toBe(36); const restoredCard = screen .getAllByTitle('打开资源详情') .find((card) => card.textContent?.includes('美术资源计划 Agent 文本回执'), ); expect(restoredCard?.getAttribute('aria-pressed')).toBe('true'); expect(document.activeElement).toBe(restoredCard); fireEvent.click(restoredCard!); expect( screen.getByRole('region', { name: '美术资源计划 Agent 文本回执', }), ).not.toBeNull(); fireEvent.keyDown(window, { key: 'Escape' }); const escapeRestoredCard = screen .getAllByTitle('打开资源详情') .find((card) => card.textContent?.includes('美术资源计划 Agent 文本回执'), ); expect(document.activeElement).toBe(escapeRestoredCard); }); it('loads registered documents, art media, video, and audio with safe failure states inside central focus', async () => { const manifest = createGameCreationAppManifest( 'workbench-resource-media', '资源媒体测试', ); manifest.assets.push( { id: 'design-document', kind: 'design-document', mediaType: 'text/markdown', localPath: 'game/design.md', source: { kind: 'generated' }, }, { id: 'art-svg', kind: 'icon', mediaType: 'image/svg+xml', localPath: 'assets/icon.svg', source: { kind: 'generated' }, }, { id: 'art-video', kind: 'animation', mediaType: 'video/mp4', localPath: 'assets/intro.mp4', source: { kind: 'generated' }, }, { id: 'audio-bgm', kind: 'bgm', mediaType: 'audio/mpeg', localPath: 'assets/bgm.mp3', source: { kind: 'generated' }, }, { id: 'blocked-document', kind: 'design-document', mediaType: 'text/markdown', localPath: 'game/blocked.md', source: { kind: 'generated' }, }, ); let layoutRevision = 0; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_local_project_resource_graph') { return resourceGraphForInputs(args); } if (command === 'read_local_project_resource_canvas_layout') { return { schemaVersion: 'game-creator-resource-layout.v1', projectId: manifest.projectId, mode: args?.mode, revision: layoutRevision, positions: [], updatedAt: 0, }; } if (command === 'update_local_project_resource_canvas_layout') { layoutRevision += 1; return { status: 'updated', layout: { schemaVersion: 'game-creator-resource-layout.v1', projectId: manifest.projectId, mode: args?.mode, revision: layoutRevision, positions: args?.positions, updatedAt: layoutRevision, }, }; } if (command === 'read_local_project_text_preview') { if (args?.relativePath === 'game/blocked.md') { throw new Error('项目权限策略要求用户确认:file.read'); } expect(args).toMatchObject({ projectPath: '/tmp/workbench-resource-media', relativePath: 'game/design.md', }); return { path: 'game/design.md', mediaType: 'text/markdown', byteLen: 64, content: '# 本地设计文档\n\n[外部链接](https://example.com)\n\n![远程图片](https://example.com/image.png)\n\n', }; } if (command === 'read_local_project_media_preview') { if (args?.category === 'art') { if (args?.relativePath === 'assets/intro.mp4') { return { path: 'assets/intro.mp4', mediaType: 'video/mp4', byteLen: 128, dataUrl: 'data:video/mp4;base64,AAAAIGZ0eXA=', }; } return { path: 'assets/icon.svg', mediaType: 'image/svg+xml', byteLen: 48, dataUrl: 'data:image/svg+xml;base64,PHN2Zy8+', }; } return { path: 'assets/bgm.mp3', mediaType: 'audio/mpeg', byteLen: 1024, dataUrl: 'data:audio/mpeg;base64,SUQz', }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; render( React.createElement(ProjectDevelopmentView, { projectName: '资源媒体测试', projectPath: '/tmp/workbench-resource-media', manifest, attachments: [], recentRunStatus: null, recentRunStopReason: null, supervisor: React.createElement('div', null, '项目总控'), onHomeOpen: vi.fn(), onProjectsOpen: vi.fn(), }), ); fireEvent.click(await screen.findByRole('button', { name: /design\.md/ })); const documentFocus = await screen.findByRole('region', { name: 'design.md', }); expect( within(documentFocus).getByRole('heading', { name: '本地设计文档' }), ).not.toBeNull(); expect(documentFocus.querySelector('script')).toBeNull(); expect(documentFocus.querySelector('a')).toBeNull(); expect(within(documentFocus).getByText('图片:远程图片')).not.toBeNull(); fireEvent.click( within(documentFocus).getByRole('button', { name: '收起资源' }), ); fireEvent.click(screen.getByRole('button', { name: /icon\.svg/ })); const artPreview = await screen.findByLabelText('icon.svg 美术媒体预览'); await waitFor(() => { expect(artPreview.querySelector('img')?.getAttribute('src')).toBe( 'data:image/svg+xml;base64,PHN2Zy8+', ); }); fireEvent.click( within(screen.getByRole('region', { name: 'icon.svg' })).getByRole( 'button', { name: '收起资源' }, ), ); fireEvent.click(screen.getByRole('button', { name: /intro\.mp4/ })); const video = (await screen.findByLabelText( 'intro.mp4 视频预览', )) as HTMLVideoElement; expect(video.controls).toBe(true); expect(video.preload).toBe('metadata'); expect(video.getAttribute('src')).toBe( 'data:video/mp4;base64,AAAAIGZ0eXA=', ); fireEvent.click( within(screen.getByRole('region', { name: 'intro.mp4' })).getByRole( 'button', { name: '收起资源' }, ), ); fireEvent.click(screen.getByRole('button', { name: /bgm\.mp3/ })); const audio = (await screen.findByLabelText( 'bgm.mp3 音频播放器', )) as HTMLAudioElement; expect(audio.controls).toBe(true); expect(audio.getAttribute('src')).toBe('data:audio/mpeg;base64,SUQz'); expect(screen.getByText('载入后显示')).not.toBeNull(); expect(invoke).toHaveBeenCalledWith( 'read_local_project_media_preview', expect.objectContaining({ category: 'audio' }), ); fireEvent.click( within(screen.getByRole('region', { name: 'bgm.mp3' })).getByRole( 'button', { name: '收起资源' }, ), ); fireEvent.click(screen.getByRole('button', { name: /blocked\.md/ })); expect((await screen.findByRole('alert')).textContent).toBe( '当前项目策略要求先确认读取资源', ); expect(screen.getByLabelText('陶泥儿 Agent 对话')).not.toBeNull(); expect(screen.getByLabelText('子 Agent 状态栏')).not.toBeNull(); }); it('preserves internal media focus across same-resource manifest updates and falls back when the resource is deleted', async () => { const manifest = createGameCreationAppManifest( 'workbench-resource-focus-updates', '资源焦点更新测试', ); manifest.assets = [ { id: 'focus-audio', kind: 'background-music', mediaType: 'audio/mpeg', localPath: 'assets/focus.mp3', source: { kind: 'generated' }, }, ]; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_local_project_resource_graph') { return resourceGraphForInputs(args); } if (command === 'read_local_project_resource_canvas_layout') { return { schemaVersion: 'game-creator-resource-layout.v1', projectId: manifest.projectId, mode: args?.mode, revision: 0, positions: [], updatedAt: 0, }; } if (command === 'update_local_project_resource_canvas_layout') { return { status: 'updated', layout: { schemaVersion: 'game-creator-resource-layout.v1', projectId: manifest.projectId, mode: args?.mode, revision: 1, positions: args?.positions, updatedAt: 1, }, }; } if (command === 'read_local_project_media_preview') { return { path: 'assets/focus.mp3', mediaType: 'audio/mpeg', byteLen: 1024, dataUrl: 'data:audio/mpeg;base64,SUQz', }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; const viewProps = { projectName: manifest.name, projectPath: '/tmp/workbench-resource-focus-updates', attachments: [], recentRunStatus: null, recentRunStopReason: null, supervisor: React.createElement('div', null, '项目总控'), onHomeOpen: vi.fn(), onProjectsOpen: vi.fn(), }; const rendered = render( React.createElement(ProjectDevelopmentView, { ...viewProps, manifest, }), ); fireEvent.click( await screen.findByRole('button', { name: /focus\.mp3/ }), ); const audio = (await screen.findByLabelText( 'focus.mp3 音频播放器', )) as HTMLAudioElement; audio.focus(); expect(document.activeElement).toBe(audio); const updatedManifest = { ...manifest, tasks: manifest.tasks.map((task) => task.id === 'audio-director' ? { ...task, status: 'completed' as const } : task, ), assets: manifest.assets.map((asset) => ({ ...asset, source: { ...asset.source, taskId: 'audio-director' }, })), }; rendered.rerender( React.createElement(ProjectDevelopmentView, { ...viewProps, manifest: updatedManifest, }), ); expect(document.activeElement).toBe(audio); expect( screen.getByRole('region', { name: 'focus.mp3' }), ).not.toBe(document.activeElement); rendered.rerender( React.createElement(ProjectDevelopmentView, { ...viewProps, manifest: { ...updatedManifest, assets: [] }, }), ); await waitFor(() => { expect(screen.queryByRole('region', { name: 'focus.mp3' })).toBeNull(); expect(document.activeElement).toBe( screen.getByLabelText('搜索项目资源'), ); }); expect( screen .queryAllByTitle('打开资源详情') .some((card) => card.getAttribute('aria-pressed') === 'true'), ).toBe(false); }); it('renders, filters, and destroys persistent resource dependency lines without cross-section task flows', async () => { const manifest = createGameCreationAppManifest( 'workbench-resource-graph', '资源依赖图测试', ); manifest.assets.push( { id: 'dependency-spec', kind: 'design-spec', mediaType: 'application/json', localPath: 'assets/spec-source.json', source: { kind: 'canvas', taskId: 'task-1', resourceId: 'canvas-spec-source', }, }, { id: 'dependency-ui', kind: 'ui-prototype', mediaType: 'application/json', localPath: 'assets/ui-dependency.json', source: { kind: 'canvas', taskId: 'task-2', resourceId: 'canvas-ui-target', referenceResourceIds: ['canvas-spec-source'], }, }, { id: 'unrelated-cycle', kind: 'metadata', mediaType: 'application/json', localPath: 'assets/unrelated-cycle.json', source: { kind: 'canvas', resourceId: 'canvas-unrelated', referenceResourceIds: ['canvas-unrelated'], }, }, ); const referenceId = 'asset-reference:["asset:dependency-spec","asset:dependency-ui"]'; const selfReferenceId = 'asset-reference:["asset:unrelated-cycle","asset:unrelated-cycle"]'; const flowId = 'task-flow:["art-director","design-foundation"]'; let layoutRevision = 0; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_local_project_resource_graph') { expect(args?.resources).toEqual( expect.arrayContaining([ expect.objectContaining({ resourceId: 'asset:dependency-spec', manifestAssetId: 'dependency-spec', producerTaskId: null, }), ]), ); return { resourceIds: [ 'asset:dependency-spec', 'asset:dependency-ui', 'asset:unrelated-cycle', ], referenceEdges: [ { id: referenceId, kind: 'asset-reference', sourceResourceId: 'asset:dependency-spec', targetResourceId: 'asset:dependency-ui', cyclic: false, }, { id: selfReferenceId, kind: 'asset-reference', sourceResourceId: 'asset:unrelated-cycle', targetResourceId: 'asset:unrelated-cycle', cyclic: true, }, ], taskFlows: [ { id: flowId, kind: 'task-flow', sourceTaskId: 'art-director', targetTaskId: 'design-foundation', sourceResourceIds: ['asset:dependency-spec'], targetResourceIds: ['asset:dependency-ui'], cyclic: false, }, ], connectionIndex: [ { resourceId: 'asset:dependency-spec', upstreamReferenceResourceIds: [], downstreamReferenceResourceIds: ['asset:dependency-ui'], referenceEdgeIds: [referenceId], taskFlowIds: [flowId], }, { resourceId: 'asset:dependency-ui', upstreamReferenceResourceIds: ['asset:dependency-spec'], downstreamReferenceResourceIds: [], referenceEdgeIds: [referenceId], taskFlowIds: [flowId], }, { resourceId: 'asset:unrelated-cycle', upstreamReferenceResourceIds: ['asset:unrelated-cycle'], downstreamReferenceResourceIds: ['asset:unrelated-cycle'], referenceEdgeIds: [selfReferenceId], taskFlowIds: [], }, ], producerAssignments: [ { resourceId: 'asset:dependency-spec', taskId: 'art-director', }, { resourceId: 'asset:dependency-ui', taskId: 'design-foundation', }, ], dependencyDepths: [ { resourceId: 'asset:dependency-spec', dependencyDepth: 0, }, { resourceId: 'asset:dependency-ui', dependencyDepth: 1, }, { resourceId: 'asset:unrelated-cycle', dependencyDepth: 0, }, ], unresolvedReferenceResourceIds: [], cyclicResourceIds: ['asset:unrelated-cycle'], cyclicTaskIds: [], producerMappingTruncated: false, }; } if (command === 'read_local_project_resource_canvas_layout') { return { schemaVersion: 'game-creator-resource-layout.v1', projectId: 'workbench-resource-graph', mode: args?.mode, revision: layoutRevision, positions: [], updatedAt: 0, }; } if (command === 'update_local_project_resource_canvas_layout') { layoutRevision += 1; return { status: 'updated', layout: { schemaVersion: 'game-creator-resource-layout.v1', projectId: 'workbench-resource-graph', mode: args?.mode, revision: layoutRevision, positions: args?.positions, updatedAt: layoutRevision, }, }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; const view = render( React.createElement(ProjectDevelopmentView, { projectName: '资源依赖图测试', projectPath: '/tmp/workbench-resource-graph', manifest, attachments: [], recentRunStatus: null, recentRunStopReason: null, supervisor: React.createElement('div', null, '项目总控'), onHomeOpen: vi.fn(), onProjectsOpen: vi.fn(), }), ); let overlay = await screen.findByTestId('resource-dependency-overlay'); await waitFor(() => { expect( overlay.querySelectorAll('[data-edge-kind="asset-reference"]'), ).toHaveLength(2); expect( overlay.querySelectorAll('[data-edge-kind="task-flow"]'), ).toHaveLength(0); }); const dependencyCanvas = screen.getByLabelText('资源依赖视图'); const descriptionId = dependencyCanvas.getAttribute('aria-describedby'); expect(descriptionId).not.toBeNull(); const relationshipDescription = document.getElementById(descriptionId!); expect(relationshipDescription?.textContent).toContain( 'ui-dependency.json(待视觉验收) 引用 spec-source.json', ); expect(overlay.getAttribute('aria-hidden')).toBe('true'); fireEvent.click(screen.getByRole('button', { name: '按类型' })); expect(screen.queryByTestId('resource-dependency-overlay')).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '按依赖' })); overlay = await screen.findByTestId('resource-dependency-overlay'); const search = screen.getByLabelText('搜索项目资源'); fireEvent.change(search, { target: { value: 'ui-dependency' } }); await waitFor(() => { expect(overlay.querySelector('[data-edge-kind]')).toBeNull(); }); fireEvent.change(search, { target: { value: '' } }); let sourceCard = screen.getByRole('button', { name: /spec-source\.json/, }); const targetCard = screen.getByRole('button', { name: /ui-dependency\.json/, }); const referenceSelector = '[data-edge-kind="asset-reference"]' + '[data-source-resource-id="asset:dependency-spec"]' + '[data-target-resource-id="asset:dependency-ui"]'; const firstPath = await waitFor(() => { const path = overlay.querySelector(referenceSelector); expect(path).not.toBeNull(); return path?.getAttribute('d'); }); const sourceStyle = sourceCard.getAttribute('style'); const layoutUpdatesBeforePointer = invoke.mock.calls.filter( ([command]) => command === 'update_local_project_resource_canvas_layout', ).length; fireEvent.pointerDown(sourceCard, { pointerId: 27, button: 0, clientX: 0, clientY: 0, }); fireEvent.pointerMove(sourceCard, { pointerId: 27, clientX: 72, clientY: 28, }); fireEvent.pointerUp(sourceCard, { pointerId: 27, clientX: 72, clientY: 28, }); fireEvent.pointerCancel(sourceCard, { pointerId: 27 }); expect(sourceCard.getAttribute('style')).toBe(sourceStyle); expect(sourceCard.classList.contains('is-dragging')).toBe(false); expect(overlay.querySelector(referenceSelector)?.getAttribute('d')).toBe( firstPath, ); expect( invoke.mock.calls.filter( ([command]) => command === 'update_local_project_resource_canvas_layout', ), ).toHaveLength(layoutUpdatesBeforePointer); fireEvent.click(targetCard); const targetFocus = screen.getByRole('region', { name: /ui-dependency\.json/u, }); fireEvent.click( within(targetFocus).getByRole('button', { name: '收起资源' }), ); overlay = await screen.findByTestId('resource-dependency-overlay'); sourceCard = screen.getByRole('button', { name: /spec-source\.json/ }); await waitFor(() => expect(overlay.querySelector(referenceSelector)).not.toBeNull(), ); expect(sourceCard.classList.contains('is-relation-upstream')).toBe(false); expect( overlay .querySelector(referenceSelector) ?.classList.contains('is-highlighted'), ).toBe(false); expect( overlay .querySelector('[data-source-resource-id="asset:unrelated-cycle"]') ?.classList.contains('is-dimmed'), ).toBe(false); const previousOverlay = overlay; const nextManifest = createGameCreationAppManifest( 'workbench-resource-graph-next', '新资源依赖图测试', ); view.rerender( React.createElement(ProjectDevelopmentView, { projectName: '新资源依赖图测试', projectPath: '/tmp/workbench-resource-graph-next', manifest: nextManifest, attachments: [], recentRunStatus: null, recentRunStopReason: null, supervisor: React.createElement('div', null, '项目总控'), onHomeOpen: vi.fn(), onProjectsOpen: vi.fn(), }), ); const nextOverlay = await screen.findByTestId( 'resource-dependency-overlay', ); expect(nextOverlay).not.toBe(previousOverlay); expect(previousOverlay.isConnected).toBe(false); expect(nextOverlay.querySelector('[data-edge-kind]')).toBeNull(); }); it('waits for the scoped resource graph before initializing dependency layout', async () => { const projectId = 'workbench-delayed-resource-graph'; const projectPath = '/tmp/workbench-delayed-resource-graph'; const manifest = createGameCreationAppManifest(projectId, '延迟依赖图测试'); const agentResults = [ { agentId: 'design-foundation', runId: 'delayed-graph-run', label: '玩法策划 Agent', title: '延迟依赖图回执', content: '图就绪后再初始化布局', updatedAt: 1, }, ]; let resolveGraph: (() => void) | null = null; let layoutReads = 0; let layoutRevision = 0; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_local_project_resource_graph') { return await new Promise((resolve) => { resolveGraph = () => resolve(resourceGraphForInputs(args)); }); } if (command === 'read_local_project_resource_canvas_layout') { layoutReads += 1; return { schemaVersion: 'game-creator-resource-layout.v1', projectId, mode: 'dependency', revision: layoutRevision, positions: [], updatedAt: 0, }; } if (command === 'update_local_project_resource_canvas_layout') { layoutRevision += 1; return { status: 'updated', layout: { schemaVersion: 'game-creator-resource-layout.v1', projectId, mode: 'dependency', revision: layoutRevision, positions: args?.positions, updatedAt: layoutRevision, }, }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; render( React.createElement(ProjectDevelopmentView, { projectName: '延迟依赖图测试', projectPath, manifest, attachments: [], agentResults, supervisor: React.createElement('div', null, '项目总控'), }), ); await waitFor(() => expect(resolveGraph).not.toBeNull()); expect(layoutReads).toBe(0); expect(screen.queryByText('延迟依赖图回执')).toBeNull(); await act(async () => { resolveGraph?.(); await Promise.resolve(); }); expect(await screen.findByText('延迟依赖图回执')).not.toBeNull(); expect(layoutReads).toBe(1); }); it('keeps trusted truncated-graph depths through the workbench without persisting a flat automatic layout', async () => { const projectId = 'workbench-truncated-resource-graph'; const projectPath = '/tmp/workbench-truncated-resource-graph'; const manifest = createGameCreationAppManifest( projectId, '截断依赖图布局保护测试', ); manifest.assets.push( { id: 'truncated-depth-0', kind: 'design-spec', mediaType: 'application/json', localPath: 'assets/truncated-depth-0.json', source: { kind: 'canvas', resourceId: 'external-truncated-depth-0', }, }, { id: 'truncated-depth-1', kind: 'metadata', mediaType: 'application/json', localPath: 'assets/truncated-depth-1.json', source: { kind: 'canvas', resourceId: 'external-truncated-depth-1', referenceResourceIds: ['external-truncated-depth-0'], }, }, { id: 'truncated-depth-2', kind: 'metadata', mediaType: 'application/json', localPath: 'assets/truncated-depth-2.json', source: { kind: 'canvas', resourceId: 'external-truncated-depth-2', referenceResourceIds: ['external-truncated-depth-1'], }, }, ); const slotWidth = RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_COLUMN_GAP; const existingPositions: ProjectResourceCanvasPosition[] = [ { resourceId: 'asset:truncated-depth-0', section: 'document', x: 0, y: 0, manuallyPlaced: false, }, { resourceId: 'asset:truncated-depth-1', section: 'document', x: slotWidth, y: 0, manuallyPlaced: false, }, { resourceId: 'asset:truncated-depth-2', section: 'document', x: slotWidth * 2, y: 0, manuallyPlaced: false, }, ]; const layoutUpdates: ProjectResourceCanvasPosition[][] = []; let layoutReads = 0; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_local_project_resource_graph') { return { resourceIds: existingPositions.map( ({ resourceId }) => resourceId, ), referenceEdges: [ { id: 'reference:truncated-0-1', kind: 'asset-reference', sourceResourceId: 'asset:truncated-depth-0', targetResourceId: 'asset:truncated-depth-1', cyclic: false, }, { id: 'reference:truncated-1-2', kind: 'asset-reference', sourceResourceId: 'asset:truncated-depth-1', targetResourceId: 'asset:truncated-depth-2', cyclic: false, }, ], taskFlows: [ { id: 'flow:untrusted-producer', kind: 'task-flow', sourceTaskId: 'art-director', targetTaskId: 'design-foundation', sourceResourceIds: ['asset:truncated-depth-0'], targetResourceIds: ['asset:truncated-depth-1'], cyclic: true, }, ], connectionIndex: [ { resourceId: 'asset:truncated-depth-0', upstreamReferenceResourceIds: [], downstreamReferenceResourceIds: ['asset:truncated-depth-1'], referenceEdgeIds: ['reference:truncated-0-1'], taskFlowIds: ['flow:untrusted-producer'], }, { resourceId: 'asset:truncated-depth-1', upstreamReferenceResourceIds: ['asset:truncated-depth-0'], downstreamReferenceResourceIds: ['asset:truncated-depth-2'], referenceEdgeIds: [ 'reference:truncated-0-1', 'reference:truncated-1-2', ], taskFlowIds: ['flow:untrusted-producer'], }, { resourceId: 'asset:truncated-depth-2', upstreamReferenceResourceIds: ['asset:truncated-depth-1'], downstreamReferenceResourceIds: [], referenceEdgeIds: ['reference:truncated-1-2'], taskFlowIds: [], }, ], producerAssignments: [ { resourceId: 'asset:truncated-depth-0', taskId: 'art-director', }, ], dependencyDepths: [ { resourceId: 'asset:truncated-depth-0', dependencyDepth: 0, }, { resourceId: 'asset:truncated-depth-1', dependencyDepth: 1, }, { resourceId: 'asset:truncated-depth-2', dependencyDepth: 2, }, ], unresolvedReferenceResourceIds: [], cyclicResourceIds: [], cyclicTaskIds: ['art-director'], producerMappingTruncated: true, }; } if (command === 'read_local_project_resource_canvas_layout') { layoutReads += 1; return { schemaVersion: 'game-creator-resource-layout.v1', projectId, mode: args?.mode, revision: 7, positions: structuredClone(existingPositions), updatedAt: 7, }; } if (command === 'update_local_project_resource_canvas_layout') { layoutUpdates.push( structuredClone( args?.positions as ProjectResourceCanvasPosition[], ), ); return { status: 'updated', layout: { schemaVersion: 'game-creator-resource-layout.v1', projectId, mode: args?.mode, revision: 8, positions: structuredClone( args?.positions as ProjectResourceCanvasPosition[], ), updatedAt: 8, }, }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; render( React.createElement(ProjectDevelopmentView, { projectName: '截断依赖图布局保护测试', projectPath, manifest, attachments: [], agentResults: [], supervisor: React.createElement('div', null, '项目总控'), }), ); await waitFor(() => expect(layoutReads).toBe(1)); const cards = [0, 1, 2].map((depth) => screen.getByRole('button', { name: new RegExp(`truncated-depth-${depth}\\.json`, 'u'), }), ); await waitFor(() => { expect(cards[0]?.getAttribute('style')).toContain('--resource-x: 0px'); expect(cards[1]?.getAttribute('style')).toContain( `--resource-x: ${slotWidth}px`, ); expect(cards[2]?.getAttribute('style')).toContain( `--resource-x: ${slotWidth * 2}px`, ); }); await act(async () => { await Promise.resolve(); }); expect(layoutUpdates).toEqual([]); }); it('restores historical resource positions but never moves or persists them from pointer input', async () => { const projectId = 'workbench-layout-persistence'; const projectPath = '/tmp/workbench-layout-persistence'; const resourceId = 'agent-result:design-foundation:layout-result-run'; const manifest = createGameCreationAppManifest(projectId, '布局持久化测试'); const agentResults = [ { agentId: 'design-foundation', runId: 'layout-result-run', label: '玩法策划 Agent', title: '布局持久化回执', content: '布局持久化正文', updatedAt: 1, }, ]; const persistedLayout: ProjectResourceCanvasLayout = { schemaVersion: 'game-creator-resource-layout.v1', projectId, mode: 'dependency', revision: 1, positions: [ { resourceId, section: 'document', x: 12, y: 24, manuallyPlaced: true, }, ], updatedAt: 100, }; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_local_project_resource_graph') { return resourceGraphForInputs(args); } if (command === 'read_local_project_resource_canvas_layout') { return structuredClone(persistedLayout); } if (command === 'update_local_project_resource_canvas_layout') { throw new Error('pointer input must not submit a manual layout CAS'); } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; function renderWorkbench() { render( React.createElement(ProjectDevelopmentView, { projectName: '布局持久化测试', projectPath, manifest, attachments: [], agentResults, supervisor: React.createElement('div', null, '项目总控'), }), ); } renderWorkbench(); const card = (await screen.findByText('布局持久化回执')).closest('button'); expect(card).not.toBeNull(); await waitFor(() => { expect(card?.getAttribute('style')).toContain('--resource-x: 12px'); expect(card?.getAttribute('style')).toContain('--resource-y: 24px'); }); const originalStyle = card?.getAttribute('style'); const layoutUpdatesBeforePointer = invoke.mock.calls.filter( ([command]) => command === 'update_local_project_resource_canvas_layout', ).length; fireEvent.pointerDown(card!, { pointerId: 11, button: 0, clientX: 20, clientY: 30, }); fireEvent.pointerMove(card!, { pointerId: 11, clientX: 100, clientY: 60, }); fireEvent.pointerUp(card!, { pointerId: 11, clientX: 100, clientY: 60, }); fireEvent.pointerCancel(card!, { pointerId: 11 }); expect(card?.getAttribute('style')).toBe(originalStyle); expect(card?.classList.contains('is-dragging')).toBe(false); expect( invoke.mock.calls.filter( ([command]) => command === 'update_local_project_resource_canvas_layout', ), ).toHaveLength(layoutUpdatesBeforePointer); fireEvent.click(card!); expect( screen.getByRole('region', { name: '布局持久化回执' }), ).not.toBeNull(); cleanup(); renderWorkbench(); const restoredCard = (await screen.findByText('布局持久化回执')).closest( 'button', ); await waitFor(() => { expect(restoredCard?.getAttribute('style')).toContain( '--resource-x: 12px', ); expect(restoredCard?.getAttribute('style')).toContain( '--resource-y: 24px', ); }); }); it('maps manifest asset kinds into stable type layout ordering', async () => { const projectId = 'workbench-layout-asset-subtypes'; const manifest = createGameCreationAppManifest( projectId, '资源子类型排序测试', ); manifest.assets.push( { id: 'ui-prototype-first-by-label', kind: 'ui-prototype', mediaType: 'image/png', localPath: 'assets/a-ui-prototype.png', source: { kind: 'generated', taskId: 'design-foundation' }, }, { id: 'art-spritesheet-last-by-label', kind: 'art-spritesheet', mediaType: 'image/png', localPath: 'assets/z-art-spritesheet.png', source: { kind: 'generated', taskId: 'art-asset-plan' }, }, ); const updates: Array<{ mode: 'dependency' | 'type'; positions: ProjectResourceCanvasPosition[]; }> = []; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_local_project_resource_graph') { return resourceGraphForInputs(args); } const mode = args?.mode as 'dependency' | 'type'; if (command === 'read_local_project_resource_canvas_layout') { return { schemaVersion: 'game-creator-resource-layout.v1', projectId, mode, revision: 0, positions: [], updatedAt: 0, }; } if (command === 'update_local_project_resource_canvas_layout') { const positions = structuredClone( args?.positions as ProjectResourceCanvasPosition[], ); updates.push({ mode, positions }); return { status: 'updated', layout: { schemaVersion: 'game-creator-resource-layout.v1', projectId, mode, revision: 1, positions, updatedAt: 1, }, }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; render( React.createElement(ProjectDevelopmentView, { projectName: '资源子类型排序测试', projectPath: '/tmp/workbench-layout-asset-subtypes', manifest, attachments: [], agentResults: [], supervisor: React.createElement('div', null, '项目总控'), }), ); await waitFor(() => expect(updates.some(({ mode }) => mode === 'dependency')).toBe(true), ); fireEvent.click(screen.getByRole('button', { name: '按类型' })); await waitFor(() => expect(updates.some(({ mode }) => mode === 'type')).toBe(true), ); const typePositions = updates.find( ({ mode }) => mode === 'type', )?.positions; expect(typePositions).toEqual( expect.arrayContaining([ expect.objectContaining({ resourceId: 'asset:art-spritesheet-last-by-label', x: 0, y: 0, }), expect.objectContaining({ resourceId: 'asset:ui-prototype-first-by-label', x: RESOURCE_CANVAS_CARD_WIDTH + RESOURCE_CANVAS_COLUMN_GAP, y: 0, }), ]), ); }); it('keeps newly reconciled resources visible when their automatic layout save fails', async () => { const projectId = 'workbench-layout-save-failure'; const manifest = createGameCreationAppManifest(projectId, '布局失败测试'); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_local_project_resource_graph') { return resourceGraphForInputs(args); } if (command === 'read_local_project_resource_canvas_layout') { return { schemaVersion: 'game-creator-resource-layout.v1', projectId, mode: 'dependency', revision: 2, positions: [], updatedAt: 200, }; } if (command === 'update_local_project_resource_canvas_layout') { throw new Error('disk full'); } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; render( React.createElement(ProjectDevelopmentView, { projectName: '布局失败测试', projectPath: '/tmp/workbench-layout-save-failure', manifest, attachments: [], agentResults: [ { agentId: 'design-foundation', runId: 'save-failure-run', label: '玩法策划 Agent', title: '自动排版失败回执', content: '自动排版失败正文', updatedAt: 1, }, ], supervisor: React.createElement('div', null, '项目总控'), }), ); expect( await screen.findByText('布局保存失败,已保留当前会话布局'), ).not.toBeNull(); expect( screen.getByText('自动排版失败回执').closest('button'), ).not.toBeNull(); }); it('keeps the landscape workbench inside the viewport with internal chat scrolling', () => { const styles = readFileSync( resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); expect(styles).toMatch( /\.game-project-workbench\s*\{[^}]*width:\s*calc\(100vw - 72px\)[^}]*padding-top:\s*40px/s, ); expect(styles).toMatch( /@media \(min-width: 761px\) and \(min-height: 680px\)[\s\S]*?\.game-project-workbench\s*\{[^}]*grid-template-rows:\s*minmax\(0, 1fr\) auto[^}]*height:\s*100dvh/, ); expect(styles).toMatch( /\.game-workbench-chat \.project-supervisor-conversation\s*\{[^}]*grid-template-rows:\s*minmax\(120px, 1fr\) minmax\(0, auto\) auto auto auto[^}]*height:\s*100%[^}]*overflow:\s*hidden/s, ); expect(styles).toMatch( /\.game-workbench-chat \.agent-runtime-status\s*\{[^}]*max-height:\s*clamp\(190px, 30dvh, 300px\)[^}]*overflow-y:\s*auto/s, ); expect(styles).toMatch( /\.game-workbench-chat \.project-runtime-summary\s*\{[^}]*position:\s*sticky[^}]*top:\s*-10px/s, ); expect(styles).toMatch( /\.game-workbench-chat \.project-supervisor-composer\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\) 82px/s, ); const agentDockRule = styles.match(/\.game-agent-dock\s*\{([^}]*)\}/s); expect(agentDockRule?.[1]).toContain('overflow: visible;'); expect(agentDockRule?.[1]).not.toContain('overflow-x:'); expect(agentDockRule?.[1]).toContain('z-index: 30;'); expect(agentDockRule?.[1]).toContain('isolation: isolate;'); expect(styles).toMatch( /@media \(max-width: 760px\)[\s\S]*?\.game-workbench-layout\s*\{[^}]*height:\s*auto/, ); }); it('enables the run presentation and renders registered images in the resource viewer', async () => { const manifest = createGameCreationAppManifest( 'workbench-runnable', '可运行工作台', ); const codePrototype = manifest.tasks.find( (task) => task.id === 'code-prototype', ); if (!codePrototype) { throw new Error('missing code-prototype seed task'); } codePrototype.status = 'completed'; manifest.preview = { status: 'running', url: 'http://127.0.0.1:4173', port: 4173, }; manifest.assets.push({ id: 'hero-art', kind: 'character', mediaType: 'image/png', localPath: 'assets/hero.png', source: { kind: 'generated', taskId: 'art-asset-plan', }, }); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'read_local_project_image_preview') { expect(args).toEqual({ projectPath: '/tmp/workbench-runnable', relativePath: 'assets/hero.png', }); return { path: 'assets/hero.png', mediaType: 'image/png', byteLen: 12, dataUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; render( React.createElement(ProjectDevelopmentView, { projectName: '可运行工作台', projectPath: '/tmp/workbench-runnable', manifest, attachments: [], recentRunStatus: 'completed', recentRunStopReason: null, supervisor: React.createElement('div', null, '项目总控'), onHomeOpen: vi.fn(), onProjectsOpen: vi.fn(), }), ); const runTab = screen.getByRole('tab', { name: '运行', }) as HTMLButtonElement; expect(runTab.disabled).toBe(false); fireEvent.click(runTab); expect(screen.getByLabelText('运行表现层')).not.toBeNull(); const previewFrame = screen.getByTitle( '可运行工作台 游戏运行画面', ) as HTMLIFrameElement; expect(previewFrame.getAttribute('src')).toBe('http://127.0.0.1:4173/'); expect(previewFrame.getAttribute('sandbox')).toBe( 'allow-scripts allow-same-origin allow-forms allow-pointer-lock', ); expect(screen.getByLabelText('测试切片控件')).not.toBeNull(); expect(screen.getByLabelText('资源信息面板')).not.toBeNull(); expect(screen.getByLabelText('数值微调面板')).not.toBeNull(); fireEvent.click(screen.getByRole('tab', { name: '资源管理' })); fireEvent.click(screen.getByRole('button', { name: '按类型' })); expect(screen.getByLabelText('资源类型视图')).not.toBeNull(); fireEvent.change(screen.getByLabelText('搜索项目资源'), { target: { value: 'hero.png' }, }); fireEvent.click(screen.getByRole('button', { name: /hero\.png/ })); expect(screen.getByRole('region', { name: 'hero.png' })).not.toBeNull(); expect(screen.getAllByText('assets/hero.png').length).toBeGreaterThan(0); const previewImage = await screen.findByRole('img', { name: 'hero.png 图片预览', }); expect(previewImage.getAttribute('src')).toBe( 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', ); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_image_preview', ), ).toHaveLength(1); fireEvent.error(previewImage); expect(screen.getByRole('alert').textContent).toBe( '图片内容无法解码,请重新生成或替换该资源', ); }); it('marks an unvalidated UI prototype as a candidate image', () => { const manifest = createGameCreationAppManifest( 'workbench-ui-candidate', '候选界面图测试', ); manifest.assets.push({ id: 'ui-prototype-candidate', kind: 'ui-prototype', mediaType: 'image/png', localPath: 'assets/ui-prototype.png', source: { kind: 'canvas', taskId: 'design-foundation', }, }); render( React.createElement(ProjectDevelopmentView, { projectName: '候选界面图测试', projectPath: '/tmp/workbench-ui-candidate', manifest, attachments: [], recentRunStatus: null, recentRunStopReason: null, agentRuntimeSummaries: [ { group: 'art', label: '美术 Agent', status: 'completed', statusLabel: '已完成', currentTask: '本轮工作已完成', currentAction: null, waitingOn: null, completedCount: 4, totalCount: 4, }, ], supervisor: React.createElement('div', null, '项目总控'), onHomeOpen: vi.fn(), onProjectsOpen: vi.fn(), }), ); const candidate = screen.getByRole('button', { name: /ui-prototype\.png(待视觉验收)/, }); expect(candidate.textContent).toContain('画板 · 候选界面图'); expect(candidate.textContent).toContain('assets/ui-prototype.png'); expect(screen.getByText('仅完成计划')).not.toBeNull(); expect( screen.getByText('美术资源计划已完成,尚未生成或登记图片'), ).not.toBeNull(); }); it('refuses to embed a non-loopback game preview in the client workbench', () => { const manifest = createGameCreationAppManifest( 'workbench-remote-preview', '远程预览拒绝测试', ); const codePrototype = manifest.tasks.find( (task) => task.id === 'code-prototype', ); if (!codePrototype) { throw new Error('missing code-prototype seed task'); } codePrototype.status = 'completed'; manifest.preview = { status: 'running', url: 'https://example.com/game', port: 443, }; render( React.createElement(ProjectDevelopmentView, { projectName: '远程预览拒绝测试', projectPath: '/tmp/workbench-remote-preview', manifest, attachments: [], recentRunStatus: 'completed', recentRunStopReason: null, supervisor: React.createElement('div', null, '项目总控'), onHomeOpen: vi.fn(), onProjectsOpen: vi.fn(), }), ); fireEvent.click(screen.getByRole('tab', { name: '运行' })); expect(screen.queryByTitle('远程预览拒绝测试 游戏运行画面')).toBeNull(); expect(screen.getByText('客户端运行画面尚未载入')).not.toBeNull(); }); } export function registerUserSurfaceBoundaryTests() { it('keeps the user surface to chat, upload, config and command confirmation', async () => { renderAppAt('/?main'); expect(screen.getByLabelText('聊天')).not.toBeNull(); expect(screen.getByLabelText('Agent 状态')).not.toBeNull(); const composerInput = screen.getByLabelText('创作想法'); expect(composerInput).not.toBeNull(); expect(screen.getByText('上传')).not.toBeNull(); expect(screen.getByRole('button', { name: '命令' })).not.toBeNull(); expect(screen.getByRole('button', { name: '能力' })).not.toBeNull(); expect(screen.getByRole('button', { name: '配置' })).not.toBeNull(); expect(screen.getByRole('button', { name: 'LLM状态' })).not.toBeNull(); expect(screen.getByRole('button', { name: '显示目录' })).not.toBeNull(); expect(screen.queryByLabelText('项目摘要')).toBeNull(); expect( (screen.getByRole('button', { name: '灵感草稿' }) as HTMLButtonElement) .disabled, ).toBe(false); expect( (screen.getByRole('button', { name: '项目状态' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '权限' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '审计' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '运行' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '资产' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '打开画板' }) as HTMLButtonElement) .disabled, ).toBe(false); expect( ( screen.getByRole('button', { name: '导入画板资产', }) as HTMLButtonElement ).disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '任务' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '索引确认' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( ( screen.getByRole('button', { name: '资产登记确认', }) as HTMLButtonElement ).disabled, ).toBe(true); expect( ( screen.getByRole('button', { name: '记忆写入确认', }) as HTMLButtonElement ).disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '预览确认' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( ( screen.getByRole('button', { name: '打开预览确认', }) as HTMLButtonElement ).disabled, ).toBe(true); expect( ( screen.getByRole('button', { name: '停止预览确认', }) as HTMLButtonElement ).disabled, ).toBe(true); expect( (screen.getByRole('button', { name: 'Agent确认' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( ( screen.getByRole('button', { name: '读对话确认', }) as HTMLButtonElement ).disabled, ).toBe(true); expect( ( screen.getByRole('button', { name: '存对话确认', }) as HTMLButtonElement ).disabled, ).toBe(true); expect( (screen.getByRole('button', { name: 'Trace' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '文件' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '索引' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '记忆' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '短期记忆' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '黑板' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '记到黑板' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '覆盖黑板' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '清空黑板' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '快照' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '快照列表' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '历史' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '白名单' }) as HTMLButtonElement) .disabled, ).toBe(false); expect( (screen.getByRole('button', { name: '静态自检' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '启动预览' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '打开预览' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '预览状态' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '停止预览' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '刷新状态' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '刷新 Agent' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '状态' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '终止' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '重试' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '继续' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '继续说明' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '输出' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '活动' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( (screen.getByRole('button', { name: '上下文包' }) as HTMLButtonElement) .disabled, ).toBe(true); expect( ( screen.getByRole('button', { name: /拆解创作方向/, }) as HTMLButtonElement ).disabled, ).toBe(true); fireEvent.click(screen.getByRole('button', { name: '打开画板' })); expect(composerInput).toHaveProperty('value', '/canvas '); fireEvent.click(screen.getByRole('button', { name: '灵感草稿' })); expect(composerInput).toHaveProperty( 'value', '像素风厨房弹幕小游戏:玩家用方向键躲避飞来的食材,收集调料加分,60 秒内尽量高分,失败后可一键重开。', ); expect(screen.queryByText('game.generate_draft')).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '白名单' })); expect(await screen.findByText(/可运行受限命令:/)).not.toBeNull(); expect(screen.getByRole('button', { name: '切换项目' })).not.toBeNull(); expect(screen.getByText('想做什么游戏?')).not.toBeNull(); expect(screen.getAllByText('暂无最近运行证据').length).toBeGreaterThan(0); expect(screen.queryByLabelText('工作区管理')).toBeNull(); expect(screen.queryByLabelText('开发环境')).toBeNull(); expect(screen.queryByLabelText('运行时配置')).toBeNull(); expect(screen.queryByText('Agent 能力')).toBeNull(); expect(screen.queryByText('编排 Trace')).toBeNull(); }); } export function registerProjectSupervisorSurfaceTests() { it('keeps the Project Supervisor welcome and empty runtime surface before the first message', async () => { const projectPath = '/tmp/launcher-empty-supervisor-game'; const manifest = createGameCreationAppManifest( 'local-project-draft', 'launcher-empty-supervisor-game', ); const supervisorHarness = createProjectSupervisorRuntimeHarness({ projectPath, initialSessionExists: false, }); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: 'launcher-empty-supervisor-game', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'get_local_game_manifest') { return manifest; } return supervisorHarness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: supervisorHarness.listen }, }; renderLauncherProjectsAt('/?launcher'); fireEvent.change(screen.getByLabelText('项目目录'), { target: { value: projectPath }, }); fireEvent.click(screen.getByRole('button', { name: '打开' })); const supervisorSurface = await screen.findByLabelText('项目总控对话'); const messageList = within(supervisorSurface).getByLabelText('项目总控消息'); expect( await within(messageList).findByText('想做什么游戏?'), ).not.toBeNull(); expect( within(supervisorSurface).getByLabelText('项目总控 Agent 状态'), ).not.toBeNull(); expect( within(supervisorSurface).getByText('项目总控 Agent · 尚未开始'), ).not.toBeNull(); expect( within(supervisorSurface).getByText('告诉陶泥儿你想做什么游戏'), ).not.toBeNull(); expect( within(supervisorSurface).getByRole('button', { name: '发送' }), ).toHaveProperty('disabled', false); }); it('hydrates a persisted needs-reconciliation Supervisor runtime without an active Session index', async () => { const projectPath = '/tmp/launcher-reconciliation-supervisor-game'; const manifest = createGameCreationAppManifest( 'local-project-draft', 'launcher-reconciliation-supervisor-game', ); const reconciliationRuntime = { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'project-supervisor', taskId: 'project-supervisor', sessionId: 'persisted-supervisor-session', runId: 'persisted-reconciliation-run', source: 'project-supervisor', status: 'needs-reconciliation', phase: 'needs-reconciliation', currentTask: '帮我生成一个贪吃蛇', currentGoal: '完成贪吃蛇原型', currentAction: '等待核对 Provider 回复交接', waitingOn: '人工核对', nextStep: '核对后继续或取消', plan: [], observations: [], allowedTools: [], pendingToolAction: null, lastResponse: null, error: 'tool-plan-unknown', updatedAt: 7000, }; const cancelledQueue = { total: 2, pending: 1, running: 0, waitingForConfirmation: 0, waitingForUserInput: 0, cancelled: 1, completed: 0, failed: 0, latestRunId: 'persisted-reconciliation-run', updatedAt: 8000, }; const supervisorHarness = createProjectSupervisorRuntimeHarness({ projectPath, initialSessionExists: false, initialRuntime: reconciliationRuntime, runtimeMapLoader: async () => [reconciliationRuntime], }); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: 'launcher-reconciliation-supervisor-game', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'get_local_game_manifest') { return manifest; } if (command === 'cancel_game_creator_agent_runtime_task') { const state = supervisorHarness.runtimeState({ ...reconciliationRuntime, status: 'cancelled', phase: 'cancelled', currentAction: '待核对的旧任务已结束', waitingOn: '队列中的下一个任务', error: null, taskQueue: cancelledQueue, updatedAt: 8000, }); return { ...supervisorHarness.runtimeResult(state), taskQueue: cancelledQueue, }; } return supervisorHarness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: supervisorHarness.listen }, }; renderLauncherProjectsAt('/?launcher'); fireEvent.change(screen.getByLabelText('项目目录'), { target: { value: projectPath }, }); fireEvent.click(screen.getByRole('button', { name: '打开' })); const supervisorSurface = await screen.findByLabelText('项目总控对话'); expect( await within(supervisorSurface).findByText('项目总控 Agent · 失败'), ).not.toBeNull(); expect( within(supervisorSurface).getByText('当前阶段:待核对'), ).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('read_game_creator_agent_runtimes', { projectPath, }); const reconcileButton = within(supervisorSurface).getByRole('button', { name: '已核对,结束旧任务', }); fireEvent.click(reconcileButton); await waitFor(() => { expect(invoke).toHaveBeenCalledWith( 'cancel_game_creator_agent_runtime_task', { projectPath, agentId: 'project-supervisor', runId: 'persisted-reconciliation-run', }, ); }); expect(invoke).not.toHaveBeenCalledWith( 'confirm_retry_game_creator_agent_runtime_task', expect.anything(), ); expect( await within(supervisorSurface).findByText( '旧任务已结束,队列中还有 1 个待处理任务,队列将继续处理', ), ).not.toBeNull(); expect( within(supervisorSurface).queryByRole('button', { name: '在当前项目重试总控', }), ).toBeNull(); }); it('allows retrying a cancelled reconciled Supervisor only after its queue is empty', async () => { const projectPath = '/tmp/launcher-reconciliation-empty-queue'; const manifest = createGameCreationAppManifest( 'local-project-draft', 'launcher-reconciliation-empty-queue', ); const runId = 'reconciliation-empty-queue-run'; const reconciliationRuntime = { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'project-supervisor', taskId: 'project-supervisor', sessionId: 'reconciliation-empty-queue-session', runId, source: 'project-supervisor', status: 'needs-reconciliation', phase: 'needs-reconciliation', currentTask: '生成贪吃蛇原型', currentGoal: '完成可玩原型', currentAction: '等待核对 Provider 回复交接', waitingOn: '人工核对', nextStep: '核对后结束旧任务', plan: [], observations: [], allowedTools: [], pendingToolAction: null, lastResponse: null, error: 'tool-plan-unknown', updatedAt: 7000, }; const emptyQueue = { total: 1, pending: 0, running: 0, waitingForConfirmation: 0, waitingForUserInput: 0, cancelled: 1, completed: 0, failed: 0, latestRunId: runId, updatedAt: 8000, }; const supervisorHarness = createProjectSupervisorRuntimeHarness({ projectPath, sessionId: 'reconciliation-empty-queue-session', initialSessionExists: false, initialRuntime: reconciliationRuntime, runtimeMapLoader: async () => [reconciliationRuntime], }); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: 'launcher-reconciliation-empty-queue', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'get_local_game_manifest') { return manifest; } if (command === 'cancel_game_creator_agent_runtime_task') { const state = supervisorHarness.runtimeState({ ...reconciliationRuntime, status: 'cancelled', phase: 'cancelled', currentAction: '待核对的旧任务已结束', waitingOn: '', error: null, taskQueue: emptyQueue, updatedAt: 8000, }); return { ...supervisorHarness.runtimeResult(state), taskQueue: emptyQueue, }; } if (command === 'confirm_retry_game_creator_agent_runtime_task') { const nextRunId = String(args?.nextRunId ?? ''); const state = supervisorHarness.runtimeState({ ...reconciliationRuntime, runId: nextRunId, status: 'running', phase: 'planning', currentAction: '重新生成项目总控计划', waitingOn: 'Agent 输出计划或回复', error: null, updatedAt: 9000, }); return { ...supervisorHarness.runtimeResult(state), acceptedRunId: nextRunId, }; } return supervisorHarness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: supervisorHarness.listen }, }; renderLauncherProjectsAt('/?launcher'); fireEvent.change(screen.getByLabelText('项目目录'), { target: { value: projectPath }, }); fireEvent.click(screen.getByRole('button', { name: '打开' })); const supervisorSurface = await screen.findByLabelText('项目总控对话'); fireEvent.click( await within(supervisorSurface).findByRole('button', { name: '已核对,结束旧任务', }), ); expect( await within(supervisorSurface).findByText( '旧任务已结束,当前队列为空,可重新启动项目总控', ), ).not.toBeNull(); const retryButton = await within(supervisorSurface).findByRole('button', { name: '在当前项目重试总控', }); fireEvent.click(retryButton); await waitFor(() => { expect(invoke).toHaveBeenCalledWith( 'confirm_retry_game_creator_agent_runtime_task', { projectPath, agentId: 'project-supervisor', runId, nextRunId: expect.stringMatching(/^project-supervisor-retry-/), }, ); }); const cancelCallIndex = invoke.mock.calls.findIndex( ([command]) => command === 'cancel_game_creator_agent_runtime_task', ); const retryCallIndex = invoke.mock.calls.findIndex( ([command]) => command === 'confirm_retry_game_creator_agent_runtime_task', ); expect(cancelCallIndex).toBeGreaterThanOrEqual(0); expect(retryCallIndex).toBeGreaterThan(cancelCallIndex); }); it('starts the game-chat surface as project-free chat with project and settings controls', () => { render( React.createElement(App, { projectSupervisorOnly: true, gameChatOnly: true, }), ); const surface = screen.getByLabelText('游戏创作聊天'); expect(within(surface).getByText('未选择项目')).not.toBeNull(); expect(within(surface).getByLabelText('最新状态')).not.toBeNull(); expect(within(surface).getByText('请选择项目目录')).not.toBeNull(); expect( within(surface).getByRole('button', { name: '选择项目路径' }), ).not.toBeNull(); expect( within(surface).getByRole('button', { name: '设置' }), ).not.toBeNull(); expect( ( within(surface).getByLabelText( '项目总控对话内容', ) as HTMLTextAreaElement ).disabled, ).toBe(true); expect(screen.queryByLabelText('游戏运行')).toBeNull(); expect(document.querySelector('iframe')).toBeNull(); expect(document.querySelector('.game-chat-shell')?.className).toBe( 'game-chat-shell', ); }); it('opens the dedicated game-chat release without platform authentication', async () => { const fetchMock = vi .spyOn(globalThis, 'fetch') .mockRejectedValue(new Error('platform api-server unavailable')); render(React.createElement(GameChatReleaseApp)); await act(async () => { await Promise.resolve(); }); expect(screen.getByLabelText('游戏创作聊天')).not.toBeNull(); expect(fetchMock).not.toHaveBeenCalled(); }); it('submits one initial game-chat message per page across StrictMode remounts and terminal updates', async () => { const projectPath = '/tmp/game-chat-initial-message-latch'; const prompt = '继续完成当前贪吃蛇'; const harness = createProjectSupervisorRuntimeHarness({ projectPath }); const manifest = createGameCreationAppManifest( 'game-chat-initial-message-latch', '初始消息闩锁', ); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: '初始消息闩锁', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'get_local_game_preview_status') { return { status: 'stopped', url: null, port: null, root: null, }; } if (command === 'get_local_game_manifest') { return manifest; } return harness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: harness.listen }, }; const renderInitialMessageApp = (message: string) => React.createElement( React.StrictMode, null, React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, gameChatOnly: true, initialSupervisorMessage: message, }), ); const rendered = render(renderInitialMessageApp(prompt)); await waitFor(() => { expect( invoke.mock.calls.filter( ([command]) => command === 'start_game_creator_supervisor_runtime_task', ), ).toHaveLength(1); }); const firstRunId = String( invoke.mock.calls.find( ([command]) => command === 'start_game_creator_supervisor_runtime_task', )?.[1]?.runId ?? '', ); expect(firstRunId).not.toBe(''); for (const [index, status] of [ 'cancelled', 'failed', 'completed', ].entries()) { act(() => { harness.emitRuntime( harness.runtimeState({ runId: firstRunId, status, phase: status, updatedAt: 4000 + index, }), ); }); rendered.rerender(renderInitialMessageApp(`${prompt}-${status}`)); } expect( invoke.mock.calls.filter( ([command]) => command === 'start_game_creator_supervisor_runtime_task', ), ).toHaveLength(1); const initCallCount = invoke.mock.calls.filter( ([command]) => command === 'init_local_game_project', ).length; rendered.unmount(); render(renderInitialMessageApp(prompt)); await waitFor(() => { expect( invoke.mock.calls.filter( ([command]) => command === 'init_local_game_project', ).length, ).toBeGreaterThan(initCallCount); expect( (screen.getByLabelText('项目总控对话内容') as HTMLTextAreaElement) .disabled, ).toBe(false); }); expect( invoke.mock.calls.filter( ([command]) => command === 'start_game_creator_supervisor_runtime_task', ), ).toHaveLength(1); }); it('removes the initial game-chat message from the launch URL before a page reload', () => { const searchParams = new URLSearchParams( 'game-chat=&projectPath=%2Ftmp%2Ftcs&initialMessage=%E7%BB%A7%E7%BB%AD%E8%B4%AA%E5%90%83%E8%9B%87', ); let replacedUrl = ''; expect( consumeInitialGameChatMessage( searchParams, { pathname: '/index.html', hash: '#game-chat' }, (url) => { replacedUrl = url; }, ), ).toBe('继续贪吃蛇'); expect(searchParams.has('initialMessage')).toBe(false); expect(replacedUrl).not.toContain('initialMessage'); expect(replacedUrl).toContain('projectPath=%2Ftmp%2Ftcs'); expect( consumeInitialGameChatMessage( searchParams, { pathname: '/index.html', hash: '#game-chat' }, () => { throw new Error('重载后不应再次改写 URL'); }, ), ).toBe(''); }); it('does not deliver a launch message when the opened game-chat project changed', async () => { const launchProjectPath = '/tmp/game-chat-initial-project'; const openedProjectPath = '/tmp/game-chat-replacement-project'; const prompt = '这条消息只能投递给启动项目'; const harness = createProjectSupervisorRuntimeHarness({ projectPath: openedProjectPath, }); const manifest = createGameCreationAppManifest( 'game-chat-replacement-project', '替换项目', ); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath: launchProjectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: '启动项目', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'init_local_game_project') { return { projectPath: openedProjectPath, manifestPath: `${openedProjectPath}/.agent/manifest.json`, manifest, }; } if (command === 'get_local_game_preview_status') { return { status: 'stopped', url: null, port: null, root: null, }; } return harness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: harness.listen }, }; render( React.createElement(App, { initialProjectPath: launchProjectPath, projectSupervisorOnly: true, gameChatOnly: true, initialSupervisorMessage: prompt, }), ); await waitFor(() => { expect( (screen.getByLabelText('项目总控对话内容') as HTMLTextAreaElement) .disabled, ).toBe(false); expect(invoke).toHaveBeenCalledWith('read_project_permission_policy', { projectPath: openedProjectPath, }); }); expect( invoke.mock.calls.filter( ([command]) => command === 'start_game_creator_supervisor_runtime_task', ), ).toHaveLength(0); expect(screen.queryByText(prompt)).toBeNull(); }); it('keeps the non-empty folder confirmation when game-chat initializes a picked project', async () => { const projectPath = '/tmp/game-chat-non-empty'; const initialSupervisorMessage = '不要在确认前启动这一轮'; const harness = createProjectSupervisorRuntimeHarness({ projectPath }); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'pick_local_project_directory') { return projectPath; } if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: false, projectName: null, recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'is_local_project_directory_non_empty') { return true; } if (command === 'get_local_game_preview_status') { return { status: 'stopped', url: null, port: null, root: null, }; } return harness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: harness.listen }, }; render( React.createElement(App, { projectSupervisorOnly: true, gameChatOnly: true, initialSupervisorMessage, }), ); fireEvent.click(screen.getByRole('button', { name: '选择项目路径' })); const dialog = await screen.findByRole('dialog', { name: '文件夹不是空的', }); expect(within(dialog).getByText(projectPath)).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'init_local_game_project', expect.anything(), ); expect( invoke.mock.calls.filter( ([command]) => command === 'start_game_creator_supervisor_runtime_task', ), ).toHaveLength(0); fireEvent.click(within(dialog).getByRole('button', { name: '继续新建' })); await waitFor(() => { expect(invoke).toHaveBeenCalledWith('init_local_game_project', { projectPath, projectId: 'local-project-draft', name: 'game-chat-non-empty', }); }); expect(await screen.findByText('game-chat-non-empty')).not.toBeNull(); }); it('embeds only a running loopback preview in the game-chat surface with the shared sandbox', async () => { const projectPath = '/tmp/game-chat-loopback'; const harness = createProjectSupervisorRuntimeHarness({ projectPath }); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: 'game-chat-loopback', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'get_local_game_preview_status') { return { status: 'running', url: 'http://127.0.0.1:4173', port: 4173, root: `${projectPath}/game`, }; } return harness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: harness.listen }, }; render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, gameChatOnly: true, }), ); const preview = await screen.findByLabelText('游戏运行'); const iframe = within(preview).getByTitle( 'game-chat-loopback 游戏运行画面', ); expect(iframe.getAttribute('src')).toBe('http://127.0.0.1:4173/'); expect(iframe.getAttribute('sandbox')).toBe( 'allow-scripts allow-same-origin allow-forms allow-pointer-lock', ); expect(iframe.getAttribute('allow')).toBe('autoplay; fullscreen; gamepad'); expect( document.querySelector('.game-chat-shell.has-preview'), ).not.toBeNull(); }); it('routes game-chat /preview through the built-in confirmation flow instead of starting an autonomous run', async () => { const projectPath = '/tmp/game-chat-manual-preview'; const harness = createProjectSupervisorRuntimeHarness({ projectPath }); let previewStarted = false; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: 'game-chat-manual-preview', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'get_local_game_preview_status') { return previewStarted ? { status: 'running', url: 'http://127.0.0.1:4310', port: 4310, root: `${projectPath}/game`, } : { status: 'stopped', url: null, port: null, root: null, }; } if (command === 'read_project_permission_policy') { return { path: '.agent/policy.json', policy: { deniedCommands: [], confirmCommands: ['preview.start'], }, }; } if (command === 'start_local_game_preview') { previewStarted = true; return { status: 'running', url: 'http://127.0.0.1:4310', port: 4310, root: `${projectPath}/game`, }; } return harness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: harness.listen }, }; render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, gameChatOnly: true, }), ); const surface = await screen.findByLabelText('游戏创作聊天'); const composer = within(surface).getByLabelText('项目总控对话内容'); await waitFor(() => { expect((composer as HTMLTextAreaElement).disabled).toBe(false); }); fireEvent.change(composer, { target: { value: '/preview' } }); fireEvent.click(within(surface).getByRole('button', { name: '发送' })); const commandConfirmation = await within(surface).findByLabelText('项目总控 Agent 待确认命令'); expect( within(commandConfirmation).getByText('preview.start'), ).not.toBeNull(); expect( invoke.mock.calls.some( ([command]) => command === 'start_game_creator_supervisor_runtime_task', ), ).toBe(false); fireEvent.click( within(commandConfirmation).getByRole('button', { name: '确认' }), ); const policyConfirmation = await within(surface).findByLabelText('项目总控 Agent 待确认操作'); fireEvent.click( within(policyConfirmation).getByRole('button', { name: '确认' }), ); expect(await screen.findByLabelText('游戏运行')).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('start_local_game_preview', { projectPath, }); expect( invoke.mock.calls.some( ([command]) => command === 'start_game_creator_supervisor_runtime_task', ), ).toBe(false); }); it('removes the game-chat game region after the registered preview stops', async () => { const projectPath = '/tmp/game-chat-stopped-preview'; const harness = createProjectSupervisorRuntimeHarness({ projectPath }); let previewStatusReads = 0; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: 'game-chat-stopped-preview', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'get_local_game_preview_status') { previewStatusReads += 1; return previewStatusReads === 1 ? { status: 'running', url: 'http://127.0.0.1:4174', port: 4174, root: `${projectPath}/game`, } : { status: 'stopped', url: null, port: null, root: null, }; } return harness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: harness.listen }, }; render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, gameChatOnly: true, }), ); expect(await screen.findByLabelText('游戏运行')).not.toBeNull(); await waitFor( () => { expect(screen.queryByLabelText('游戏运行')).toBeNull(); expect( document.querySelector('.game-chat-shell.has-preview'), ).toBeNull(); }, { timeout: 2500 }, ); }); it('does not render a game region for a running remote preview URL', async () => { const projectPath = '/tmp/game-chat-remote-preview'; const harness = createProjectSupervisorRuntimeHarness({ projectPath }); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: 'game-chat-remote-preview', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'get_local_game_preview_status') { return { status: 'running', url: 'https://preview.example.com/game', port: 443, root: `${projectPath}/game`, }; } return harness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: harness.listen }, }; render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, gameChatOnly: true, }), ); await waitFor(() => { expect(invoke).toHaveBeenCalledWith('get_local_game_preview_status', { projectPath, }); }); expect(await screen.findByLabelText('游戏创作聊天')).not.toBeNull(); expect(screen.queryByLabelText('游戏运行')).toBeNull(); expect(document.querySelector('iframe')).toBeNull(); expect(document.querySelector('.game-chat-shell.has-preview')).toBeNull(); }); it('filters game-chat events to the active parent run, sorts and deduplicates them', () => { const duplicate = gameChatRuntimeEvent({ runId: 'active-parent-run', summary: '总控事件 2', updatedAt: 20, }); const supervisor = gameChatRuntimeState({ runId: 'active-parent-run', source: 'project-supervisor-game-chat', recentEvents: [ gameChatRuntimeEvent({ runId: 'active-parent-run', summary: '总控事件 1', updatedAt: 10, }), gameChatRuntimeEvent({ runId: 'previous-parent-run', summary: '总控旧 run 事件', updatedAt: 50, }), duplicate, duplicate, ], }); const currentChild = gameChatRuntimeState({ agentId: 'design-foundation', taskId: 'design-foundation', sessionId: 'design-session', runId: 'design-run', source: 'agent-ready-task-scheduler', parentAgentId: 'project-supervisor', parentRunId: 'active-parent-run', updatedAt: 30, recentEvents: [ gameChatRuntimeEvent({ agentId: 'design-foundation', runId: 'design-run', summary: '当前子 Agent 事件', updatedAt: 30, }), gameChatRuntimeEvent({ agentId: 'design-foundation', runId: 'previous-design-run', summary: '子 Agent 旧 run 事件', updatedAt: 60, }), ], }); const staleChild = gameChatRuntimeState({ agentId: 'art-asset-plan', taskId: 'art-asset-plan', sessionId: 'art-session', runId: 'art-run', source: 'agent-delegate', parentAgentId: 'project-supervisor', parentRunId: 'stale-parent-run', updatedAt: 40, recentEvents: [ gameChatRuntimeEvent({ agentId: 'art-asset-plan', runId: 'art-run', summary: '过期父 run 事件', updatedAt: 40, }), ], }); expect(supervisor.recentEvents?.map((event) => event.summary)).toEqual([ '总控事件 1', '总控旧 run 事件', '总控事件 2', '总控事件 2', ]); const events = collectGameChatRuntimeEvents(supervisor, { 'design-foundation': currentChild, 'art-asset-plan': staleChild, }); expect(events.map((item) => item.event.summary)).toEqual([ '当前子 Agent 事件', '总控事件 2', '总控事件 1', ]); expect(events.map((item) => item.agentLabel)).toEqual([ '玩法策划 Agent', '项目总控 Agent', '项目总控 Agent', ]); }); it('turns user-visible game-chat runtime outputs into chronological chat messages and filters protocol payloads', () => { const runId = 'game-chat-runtime-message-run'; const runtime = gameChatRuntimeState({ runId, recentEvents: [ gameChatRuntimeEvent({ runId, eventType: 'turn.started', summary: 'Synthetic start event must stay out of chat', updatedAt: 30, }), gameChatRuntimeEvent({ runId, eventType: 'turn.failed', status: 'failed', phase: 'failed', summary: 'Root terminal event is covered by durable public status', updatedAt: 35, }), gameChatRuntimeEvent({ runId, eventType: 'turn.progress', summary: 'Generated prototype progress', detail: 'internal loop iteration 4', updatedAt: 40, }), gameChatRuntimeEvent({ runId, eventType: 'tool.request', summary: 'agent.runtime.tool.request', detail: '{"tool":"agent.delegate","arguments":{"secret":"x"}}', publicText: null, updatedAt: 50, }), gameChatRuntimeEvent({ runId, eventType: 'action', summary: 'call tool agent.delegate', detail: 'code agent repair collision', publicText: 'code agent repair collision', updatedAt: 60, }), gameChatRuntimeEvent({ runId, eventType: 'observation', summary: 'command.output_read:ok', detail: '{"output":"private process output"}', publicText: null, updatedAt: 70, }), gameChatRuntimeEvent({ runId, eventType: 'observation', summary: 'preview.validate:ok', detail: '{"passed":true,"revision":3}', updatedAt: 80, }), gameChatRuntimeEvent({ runId, eventType: 'turn.progress', summary: 'legacy output without stable event identity', eventId: '', publicText: 'legacy output without stable event identity', updatedAt: 90, }), ], }); const designRuntime = gameChatRuntimeState({ agentId: 'design-director', taskId: 'design-director', sessionId: 'design-director-session', runId: 'design-director-child-run', source: 'agent-delegate', parentAgentId: 'project-supervisor', parentRunId: runId, recentEvents: [ gameChatRuntimeEvent({ agentId: 'design-director', runId: 'design-director-child-run', eventType: 'turn.started', summary: 'Design Agent started and must stay visible', updatedAt: 32, }), ], updatedAt: 32, }); const messages = gameChatRuntimeEventMessages(runtime, { 'design-director': designRuntime, }); expect(messages.map((message) => message.updatedAt)).toEqual([ 32, 40, 60, 80, ]); expect(messages.map((message) => message.text)).toEqual([ expect.stringContaining('Design Agent started and must stay visible'), expect.stringContaining('Generated prototype progress'), expect.stringContaining('code agent repair collision'), expect.stringContaining('preview.validate:ok'), ]); expect(messages.every((message) => message.role === 'assistant')).toBe( true, ); expect( messages.every((message) => message.messageId?.startsWith('game-chat-runtime-event:'), ), ).toBe(true); expect(messages.map((message) => message.text).join('\n')).not.toContain( 'private process output', ); expect(messages.map((message) => message.text).join('\n')).not.toContain( 'agent.runtime.tool.request', ); for (const continuation of [ { kind: 'receipt', source: 'agent-delegate-receipt' }, { kind: 'isolated join', source: 'agent-isolated-join' }, ]) { const continuationRunId = `supervisor-${continuation.kind.replaceAll(' ', '-')}-continuation`; const supervisorContinuation = gameChatRuntimeState({ runId: continuationRunId, source: continuation.source, parentAgentId: null, parentRunId: runId, recentEvents: [ gameChatRuntimeEvent({ runId: continuationRunId, eventType: 'turn.started', summary: `Supervisor ${continuation.kind} continuation started`, updatedAt: 100, }), gameChatRuntimeEvent({ runId: continuationRunId, eventType: 'turn.failed', status: 'failed', phase: 'failed', summary: `Supervisor ${continuation.kind} continuation failed`, updatedAt: 110, }), ], }); expect( gameChatRuntimeEventMessages(supervisorContinuation, {}).map( (message) => message.text, ), ).toEqual([ expect.stringContaining( `Supervisor ${continuation.kind} continuation started`, ), expect.stringContaining( `Supervisor ${continuation.kind} continuation failed`, ), ]); } }); it('turns only professional final-reply streams into labeled game-chat messages', () => { const makeStream = ( agentId: string, status: 'ready' | 'committed', accumulatedText: string, responseRevision = 1, ) => ({ schemaVersion: 'game-creator-runtime-response-stream.v1', agentId, taskId: agentId, sessionId: 'supervisor-session-active', runId: 'game-chat-final-reply-run', requestKind: 'final-reply', requestSlot: `final-reply-loop-1-revision-${responseRevision}`, appliedSteerCursor: 0, responseRevision, sequence: 2, status, accumulatedText, finishReason: 'stop', startedAt: 100, updatedAt: 200 + responseRevision, }); const messages = gameChatFinalReplyMessages([ makeStream('design-director', 'committed', '玩法方向已完成'), makeStream('art-director', 'ready', '视觉方向已完成'), makeStream('art-asset-plan', 'committed', '平台美术图集已生成并登记'), makeStream('code-director', 'ready', '程序方案已完成'), makeStream('code-prototype', 'ready', '代码原型已完成'), makeStream('preview-readiness', 'committed', '预览就绪检查已完成'), makeStream('preview-playtest', 'ready', '试玩验证已完成'), { ...makeStream('code-prototype', 'ready', 'tool plan should be hidden'), requestKind: 'tool-plan', }, ]); expect(messages).toHaveLength(7); expect(messages.map((message) => message.text)).toEqual([ expect.stringContaining('玩法方向已完成'), expect.stringContaining('视觉方向已完成'), expect.stringContaining('平台美术图集已生成并登记'), expect.stringContaining('程序方案已完成'), expect.stringContaining('代码原型已完成'), expect.stringContaining('预览就绪检查已完成'), expect.stringContaining('试玩验证已完成'), ]); expect(messages[0]?.messageId).toContain('design-director'); expect(messages[0]?.messageId).toContain('game-chat-final-reply:'); expect(messages.every((message) => message.agentId)).toBe(true); const hydrated = mergeGameChatFinalReplyMessagesIntoHistory( [messages[0]!], messages, ); expect( hydrated.filter( (message) => message.messageId === messages[0]?.messageId, ), ).toHaveLength(1); }); it('persists professional final-reply streams with stable ids and does not duplicate them after hydration', async () => { const projectPath = '/tmp/game-chat-final-reply-hydration'; const runId = 'game-chat-final-reply-hydration-run'; const rootRuntime = gameChatRuntimeState({ sessionId: 'supervisor-session-active', runId, status: 'running', phase: 'execution', updatedAt: 100, }); const makeRuntimeResult = ( agentId: string, status: 'ready' | 'committed', text: string, updatedAt: number, ) => { const state = gameChatRuntimeState({ agentId, taskId: agentId, sessionId: 'supervisor-session-active', runId: `${agentId}-${runId}`, source: 'agent-delegate', parentAgentId: 'project-supervisor', parentRunId: runId, status: 'completed', phase: 'completed', updatedAt, }); return { state, sessionPath: `${projectPath}/.agent/runtime/${agentId}.json`, eventPath: `${projectPath}/.agent/runtime/${agentId}.jsonl`, responseStream: { schemaVersion: 'game-creator-runtime-response-stream.v1', agentId, taskId: agentId, sessionId: 'supervisor-session-active', runId: state.runId, requestKind: 'final-reply', requestSlot: 'final-reply-loop-1-revision-1', appliedSteerCursor: 0, responseRevision: 1, sequence: 2, status, accumulatedText: text, finishReason: 'stop', startedAt: updatedAt - 20, updatedAt, }, }; }; const professionalResults = [ makeRuntimeResult('design-director', 'committed', '玩法方向已完成', 170), makeRuntimeResult('art-director', 'ready', '视觉方向已完成', 180), makeRuntimeResult( 'art-asset-plan', 'committed', '平台美术图集已生成并登记', 190, ), makeRuntimeResult('code-director', 'ready', '程序方案已完成', 195), makeRuntimeResult('code-prototype', 'ready', '代码原型已完成', 200), makeRuntimeResult( 'preview-readiness', 'committed', '预览就绪已完成', 210, ), makeRuntimeResult('preview-playtest', 'ready', '试玩验证已完成', 220), ]; const harness = createProjectSupervisorRuntimeHarness({ projectPath, initialRuntime: rootRuntime, }); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: 'game-chat-final-reply-hydration', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'read_game_creator_agent_runtimes') { return professionalResults; } return harness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: harness.listen }, }; const renderRelease = () => render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, gameChatOnly: true, }), ); let rendered = renderRelease(); await waitFor(() => { expect(screen.getByText(/视觉方向已完成/)).not.toBeNull(); expect(screen.getByText(/平台美术图集已生成并登记/)).not.toBeNull(); expect(screen.getByText(/代码原型已完成/)).not.toBeNull(); expect(screen.getByText(/预览就绪已完成/)).not.toBeNull(); expect(screen.getByText(/试玩验证已完成/)).not.toBeNull(); }); const finalReplyAppends = () => invoke.mock.calls.filter( ([command, args]) => command === 'append_local_conversation_message' && String(args?.messageId ?? '').startsWith('game-chat-final-reply:'), ); await waitFor(() => { expect(finalReplyAppends()).toHaveLength(7); }); rendered.unmount(); rendered = renderRelease(); await waitFor(() => { expect(screen.getByText(/代码原型已完成/)).not.toBeNull(); }); expect(finalReplyAppends()).toHaveLength(7); rendered.unmount(); }); it('persists game-chat runtime event messages with stable ids and does not duplicate them after hydration', async () => { const projectPath = '/tmp/game-chat-runtime-message-hydration'; const runId = 'game-chat-runtime-message-hydration-run'; const events = [ gameChatRuntimeEvent({ sessionId: 'supervisor-session-active', runId, eventType: 'turn.progress', summary: 'First visible runtime output', updatedAt: 40, }), gameChatRuntimeEvent({ sessionId: 'supervisor-session-active', runId, eventType: 'observation', summary: 'Second visible runtime output', updatedAt: 50, }), ]; const manifest = createGameCreationAppManifest( 'game-chat-runtime-message-hydration', 'game-chat-runtime-message-hydration', ); const harness = createProjectSupervisorRuntimeHarness({ projectPath, initialRuntime: { sessionId: 'supervisor-session-active', runId, status: 'running', phase: 'execution', recentEvents: events, updatedAt: 60, }, }); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: manifest.name, recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'init_local_game_project') { return { projectPath, manifestPath: `${projectPath}/.agent/manifest.json`, manifest, }; } if (command === 'get_local_game_manifest') { return manifest; } if (command === 'get_local_game_preview_status') { return { status: 'stopped', url: null, port: null, root: null }; } return harness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: harness.listen }, }; const renderApp = () => render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, gameChatOnly: true, }), ); let rendered = renderApp(); const runtimeEventAppends = () => invoke.mock.calls.filter( ([command, args]) => command === 'append_local_conversation_message' && args?.agentId === null && String(args?.messageId ?? '').startsWith('game-chat-runtime-event:'), ); await waitFor(() => { expect(runtimeEventAppends()).toHaveLength(2); }); const firstRuntimeMessage = screen .getByText(/First visible runtime output/) .closest('p'); const secondRuntimeMessage = screen .getByText(/Second visible runtime output/) .closest('p'); expect( firstRuntimeMessage?.querySelector('time')?.getAttribute('datetime'), ).toBe('1970-01-01T00:00:40.000Z'); expect(firstRuntimeMessage?.querySelector('time')?.textContent).toMatch( /^\d{2}:\d{2}:\d{2}$/u, ); expect( secondRuntimeMessage?.querySelector('time')?.getAttribute('datetime'), ).toBe('1970-01-01T00:00:50.000Z'); expect( invoke.mock.calls.filter( ([command]) => command === 'start_game_creator_supervisor_runtime_task', ), ).toHaveLength(0); rendered.unmount(); rendered = renderApp(); await waitFor(() => { expect( screen.getByText(/First visible runtime output/).closest('p'), ).not.toBeNull(); expect( screen.getByText(/Second visible runtime output/).closest('p'), ).not.toBeNull(); }); expect(runtimeEventAppends()).toHaveLength(2); rendered.unmount(); }); it('keeps an unstructured image inspection neutral instead of presenting tool success as visual approval', () => { const runId = 'game-chat-unstructured-image-inspection'; const runtime = gameChatRuntimeState({ runId, recentEvents: [ gameChatRuntimeEvent({ runId, eventType: 'observation', status: 'ok', phase: 'tool-observation', summary: 'image.inspect:ok · 视觉检查已完成,共分析 2 张图片', detail: JSON.stringify({ inspectionKind: null, validationProfile: null, passed: null, checks: null, issues: null, }), updatedAt: 25, }), ], }); const progress = buildGameChatProgressEvidence(runtime, {}, null); expect(progress?.evidence).toEqual([ expect.objectContaining({ label: '视觉分析完成', tone: 'work', }), ]); }); it('presents an unstructured image inspection tool failure from its production summary', () => { const runId = 'game-chat-unstructured-image-inspection-failure'; const runtime = gameChatRuntimeState({ runId, recentEvents: [ gameChatRuntimeEvent({ runId, eventType: 'observation', status: 'running', phase: 'tool-observation', summary: 'image.inspect:failed · 视觉模型调用失败', detail: null, updatedAt: 26, }), ], }); const progress = buildGameChatProgressEvidence(runtime, {}, null); expect(progress?.evidence).toEqual([ expect.objectContaining({ label: '视觉分析未完成', tone: 'fail', }), ]); }); it('uses structured image inspection verdicts for pass and fail presentation', () => { for (const [passed, label, tone] of [ [true, '视觉检查通过', 'pass'], [false, '视觉检查未通过', 'fail'], ] as const) { const runId = `game-chat-structured-image-inspection-${passed}`; const runtime = gameChatRuntimeState({ runId, recentEvents: [ gameChatRuntimeEvent({ runId, eventType: 'observation', status: passed ? 'ok' : 'failed', phase: 'tool-observation', summary: `image.inspect:${passed ? 'ok' : 'failed'} · UI 原型视觉检查${passed ? '已通过' : '未通过'}`, detail: JSON.stringify({ passed }), updatedAt: passed ? 26 : 27, }), ], }); expect( buildGameChatProgressEvidence(runtime, {}, null)?.evidence, ).toEqual([expect.objectContaining({ label, tone })]); } }); it('requires an explicit playtest pass before presenting preview validation as passed', () => { const runId = 'game-chat-preview-without-playtest-pass'; const runtime = gameChatRuntimeState({ runId, recentEvents: [ gameChatRuntimeEvent({ runId, eventType: 'observation', status: 'ok', phase: 'tool-observation', summary: 'preview.validate:ok · 浏览器验证已完成', detail: JSON.stringify({ diagnostics: [], diagnosticsCount: 0, passed: true, revision: 8, }), updatedAt: 26, }), ], }); const progress = buildGameChatProgressEvidence(runtime, {}, null); expect(progress?.evidence).toEqual([ expect.objectContaining({ label: '试玩未通过', tone: 'fail', }), ]); }); it('does not infer a preview pass from an ok summary when structured evidence is missing', () => { const runId = 'game-chat-preview-without-structured-evidence'; const runtime = gameChatRuntimeState({ runId, recentEvents: [ gameChatRuntimeEvent({ runId, eventType: 'observation', status: 'ok', phase: 'tool-observation', summary: 'preview.validate:ok · 浏览器验证已完成', detail: null, updatedAt: 27, }), ], }); const progress = buildGameChatProgressEvidence(runtime, {}, null); expect(progress?.evidence).toEqual([ expect.objectContaining({ label: '试玩未通过', tone: 'fail', }), ]); }); it('derives a playable revision only from a passed preview validation with a positive integer revision', () => { const runId = 'game-chat-playable-revision-run'; const invalidPlaytest = gameChatPreviewPlaytestRuntime({ parentRunId: runId, revision: 1, updatedAt: 3000, }); const invalidEvidence = [ gameChatRuntimeEvent({ agentId: 'preview-playtest', taskId: 'preview-playtest', sessionId: invalidPlaytest.sessionId, runId: invalidPlaytest.runId, eventType: 'observation', summary: 'preview.validate:failed', detail: JSON.stringify({ passed: true, playtestPassed: true, revision: 8, }), updatedAt: 8000, }), gameChatRuntimeEvent({ agentId: 'preview-playtest', taskId: 'preview-playtest', sessionId: invalidPlaytest.sessionId, runId: invalidPlaytest.runId, eventType: 'observation', summary: 'preview.validate:ok · 验证未通过', detail: JSON.stringify({ passed: false, playtestPassed: false, revision: 7, }), updatedAt: 7000, }), gameChatRuntimeEvent({ agentId: 'preview-playtest', taskId: 'preview-playtest', sessionId: invalidPlaytest.sessionId, runId: invalidPlaytest.runId, eventType: 'observation', summary: 'preview.validate:ok · 缺少 revision', detail: JSON.stringify({ passed: true, playtestPassed: true }), updatedAt: 6000, }), gameChatRuntimeEvent({ agentId: 'preview-playtest', taskId: 'preview-playtest', sessionId: invalidPlaytest.sessionId, runId: invalidPlaytest.runId, eventType: 'observation', summary: 'preview.validate:ok · revision 非正整数', detail: JSON.stringify({ passed: true, playtestPassed: true, revision: 0, }), updatedAt: 5000, }), gameChatRuntimeEvent({ agentId: 'preview-playtest', taskId: 'preview-playtest', sessionId: invalidPlaytest.sessionId, runId: invalidPlaytest.runId, eventType: 'observation', summary: 'preview.validate:ok · revision 不是整数', detail: JSON.stringify({ passed: true, playtestPassed: true, revision: 1.5, }), updatedAt: 4000, }), ]; invalidPlaytest.recentEvents = invalidEvidence; const runtimeWithOnlyInvalidEvidence = gameChatRuntimeState({ runId, }); expect( latestGameChatPlayableRevision(runtimeWithOnlyInvalidEvidence, { 'preview-playtest': invalidPlaytest, }), ).toBeNull(); const previewPlaytest = gameChatPreviewPlaytestRuntime({ parentRunId: runId, revision: 9, updatedAt: 9000, }); previewPlaytest.recentEvents?.push( gameChatRuntimeEvent({ agentId: 'preview-playtest', taskId: 'preview-playtest', sessionId: previewPlaytest.sessionId, runId: previewPlaytest.runId, eventType: 'observation', status: 'completed', phase: 'tool-observation', summary: 'preview.validate:ok · revision 10 首次验证', detail: JSON.stringify({ diagnosticsCount: 0, passed: true, playtestPassed: true, revision: 10, }), updatedAt: 8500, }), gameChatRuntimeEvent({ agentId: 'preview-playtest', taskId: 'preview-playtest', sessionId: previewPlaytest.sessionId, runId: previewPlaytest.runId, eventType: 'observation', status: 'completed', phase: 'tool-observation', summary: 'preview.validate:ok · revision 10 最新验证', detail: JSON.stringify({ diagnosticsCount: 0, passed: true, playtestPassed: true, revision: 10, }), updatedAt: 8800, }), ); expect( latestGameChatPlayableRevision(runtimeWithOnlyInvalidEvidence, { 'preview-playtest': previewPlaytest, }), ).toEqual({ runId, revision: 10, validatedAt: 8800 }); const staleParentPlaytest = gameChatPreviewPlaytestRuntime({ parentRunId: 'different-parent-run', revision: 11, updatedAt: 11000, }); expect( latestGameChatPlayableRevision(runtimeWithOnlyInvalidEvidence, { 'preview-playtest': staleParentPlaytest, }), ).toBeNull(); const wrongSourcePlaytest = { ...gameChatPreviewPlaytestRuntime({ parentRunId: runId, revision: 12, updatedAt: 12000, }), source: 'agent-delegate' as const, }; expect( latestGameChatPlayableRevision(runtimeWithOnlyInvalidEvidence, { 'preview-playtest': wrongSourcePlaytest, }), ).toBeNull(); const passedRevision = gameChatPreviewPlaytestRuntime({ parentRunId: runId, revision: 20, updatedAt: 20000, }); const higherFailedRevision = gameChatPreviewPlaytestRuntime({ parentRunId: runId, revision: 21, updatedAt: 21000, }); higherFailedRevision.recentEvents = [ gameChatRuntimeEvent({ agentId: 'preview-playtest', taskId: 'preview-playtest', sessionId: higherFailedRevision.sessionId, runId: higherFailedRevision.runId, eventType: 'observation', status: 'failed', phase: 'tool-observation', summary: 'preview.validate:failed · revision 21', detail: JSON.stringify({ passed: false, playtestPassed: false, revision: 21, }), updatedAt: 21000, }), ]; expect( latestGameChatPlayableRevision(runtimeWithOnlyInvalidEvidence, { 'preview-playtest-passed': passedRevision, 'preview-playtest-higher-failed': higherFailedRevision, }), ).toBeNull(); const sameRevisionLaterFailed = gameChatPreviewPlaytestRuntime({ parentRunId: runId, revision: 22, updatedAt: 22000, }); sameRevisionLaterFailed.recentEvents?.push( gameChatRuntimeEvent({ agentId: 'preview-playtest', taskId: 'preview-playtest', sessionId: sameRevisionLaterFailed.sessionId, runId: sameRevisionLaterFailed.runId, eventType: 'observation', status: 'failed', phase: 'tool-observation', summary: 'preview.validate:failed · revision 22 后续回归', detail: JSON.stringify({ passed: false, playtestPassed: false, revision: 22, }), updatedAt: 22100, }), ); expect( latestGameChatPlayableRevision(runtimeWithOnlyInvalidEvidence, { 'preview-playtest': sameRevisionLaterFailed, }), ).toBeNull(); }); it('keeps game-chat status compact and shows the capped latest twenty events in runtime details', () => { const runtime = gameChatRuntimeState({ runId: 'game-chat-events-run', recentEvents: Array.from({ length: 23 }, (_, index) => gameChatRuntimeEvent({ runId: 'game-chat-events-run', summary: `状态事件 ${index + 1}`, updatedAt: index + 1, }), ), }); expect(runtime.recentEvents).toHaveLength(23); renderGameChatStatus({ runtime }); const statusCard = screen.getByLabelText('最新状态'); expect(statusCard.textContent).not.toContain('状态事件 23'); expect(statusCard.textContent).not.toContain('疑似停滞'); expect(statusCard.textContent).not.toContain('已持续'); expect(statusCard.textContent).toContain('运行详情'); fireEvent.click( within(statusCard).getByRole('button', { name: '运行详情' }), ); const details = screen.getByRole('dialog', { name: '运行详情' }); const eventList = within(details).getByLabelText('最近运行活动'); expect(eventList.querySelectorAll(':scope > div')).toHaveLength(20); const expandedEventTexts = Array.from( eventList.querySelectorAll(':scope > div'), (element) => element.textContent, ); expect(expandedEventTexts[0]).toContain('状态事件 23'); expect( expandedEventTexts.some((text) => text?.includes('状态事件 4')), ).toBe(true); expect( expandedEventTexts.some((text) => text?.includes('状态事件 3')), ).toBe(false); expect(eventList.querySelectorAll('time')).toHaveLength(20); }); it('keeps three stage records in the scrollable lane with the newest messages and timestamps visible after them', () => { const styles = readFileSync( resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); expect(styles).toMatch( /\.game-chat-conversation\.supervisor-chat-only-shell\s*\{[^}]*grid-template-rows:\s*64px auto minmax\(0, 1fr\) auto/s, ); expect(styles).toMatch( /\.supervisor-chat-only-message-list\s*\{[^}]*min-height:\s*0[^}]*overflow-y:\s*auto/s, ); const updatedAt = Date.UTC(2026, 7, 5, 2, 55, 3); renderGameChatStatus({ runtime: gameChatRuntimeState({ updatedAt }), viewOverrides: { visibleMessages: [ ...Array.from({ length: 3 }, (_, index) => ({ role: 'assistant' as const, text: `【Supervisor 阶段记录】\n阶段 ${index + 1}`, messageId: `stage-${index + 1}`, updatedAt: updatedAt + index, })), { role: 'assistant', text: '这是阶段卡片之后的最新持久消息', messageId: 'latest-persisted', updatedAt: updatedAt + 3, }, ], transientReply: '这是当前仍在流式输出的新消息', transientReplyUpdatedAt: updatedAt + 4, }, }); const messageList = screen.getByLabelText('项目总控消息'); const messages = Array.from( messageList.querySelectorAll(':scope > .message'), ); expect(messages).toHaveLength(5); expect( messages .slice(0, 3) .every((message) => message.classList.contains('game-chat-stage-record'), ), ).toBe(true); expect(messages[3]?.textContent).toContain('阶段卡片之后的最新持久消息'); expect(messages[4]?.textContent).toContain('当前仍在流式输出的新消息'); expect(messages[4]?.getAttribute('aria-label')).toBe( '项目总控 Agent 实时回复', ); expect(messages[4]?.querySelector('time')?.getAttribute('datetime')).toBe( new Date(updatedAt + 4).toISOString(), ); expect(messageList.querySelectorAll(':scope > .message time')).toHaveLength( 5, ); }); it('falls back to unknown time for out-of-range message and stream timestamps', () => { expect(() => renderGameChatStatus({ runtime: gameChatRuntimeState({ updatedAt: Number.MAX_VALUE }), viewOverrides: { visibleMessages: [ { role: 'assistant', text: '带无效时间戳的持久消息', messageId: 'invalid-time-persisted', updatedAt: Number.MAX_VALUE, }, ], transientReply: '带无效时间戳的实时消息', transientReplyUpdatedAt: Number.MAX_VALUE, }, }), ).not.toThrow(); const messageTimes = screen .getByLabelText('项目总控消息') .querySelectorAll(':scope > .message time'); expect(messageTimes).toHaveLength(2); for (const time of messageTimes) { expect(time.textContent).toBe('时间未知'); expect(time.hasAttribute('datetime')).toBe(false); } }); it('refreshes elapsed time every ten seconds and warns after five minutes without parent or child progress', () => { vi.useFakeTimers(); const now = Date.UTC(2026, 7, 5, 2, 55, 3); vi.setSystemTime(now); try { const runId = 'game-chat-stalled-runtime-run'; const startedAt = now - 9 * 60 * 1000 - 28 * 1000; const runtime = gameChatRuntimeState({ runId, startedAt, updatedAt: now - 6 * 60 * 1000, recentEvents: [ gameChatRuntimeEvent({ runId, eventType: 'turn.progress', summary: '事件窗口内最早进度', updatedAt: now - 6 * 60 * 1000, }), ], }); const childRuntime = gameChatRuntimeState({ agentId: 'code-director', taskId: 'code-director', sessionId: 'game-chat-stalled-code-session', runId: 'game-chat-stalled-code-run', source: 'agent-ready-task-scheduler', parentAgentId: 'project-supervisor', parentRunId: runId, updatedAt: now - 5 * 60 * 1000 - 10 * 1000, recentEvents: [ gameChatRuntimeEvent({ agentId: 'code-director', taskId: 'code-director', sessionId: 'game-chat-stalled-code-session', runId: 'game-chat-stalled-code-run', summary: '程序 Agent 最近进度', updatedAt: now - 4 * 60 * 1000 - 55 * 1000, }), ], }); renderGameChatStatus({ runtime, runtimeByAgentId: { 'code-director': childRuntime }, }); let status = screen.getByLabelText('最新状态'); expect(status.textContent).toContain('运行中'); expect(status.textContent).not.toContain('疑似停滞'); expect(status.textContent).toContain('已持续 9 分 28 秒'); expect( within(status).getByTitle('最近运行活动时间').textContent, ).toContain('已持续 9 分 28 秒'); act(() => { vi.advanceTimersByTime(10_000); }); status = screen.getByLabelText('最新状态'); expect(status.textContent).toContain('运行中 · 疑似停滞'); expect(status.textContent).toContain('已持续 9 分 38 秒'); expect(status.textContent).toContain('5 分 5 秒无新进度'); } finally { cleanup(); vi.useRealTimers(); } }); it('does not report expected waits as stalled', () => { const now = Date.UTC(2026, 7, 5, 2, 55, 3); vi.spyOn(Date, 'now').mockReturnValue(now); const runId = 'game-chat-expected-wait-run'; const staleAt = now - 10 * 60 * 1000; const baseRuntime = gameChatRuntimeState({ runId, updatedAt: staleAt, recentEvents: [ gameChatRuntimeEvent({ runId, eventType: 'turn.started', summary: '本轮开始', updatedAt: staleAt, }), ], }); const pendingToolAction = { actionId: 'action-1', actionFingerprint: 'fingerprint-1', tool: 'file.write', inputSummary: '写入游戏代码', reason: '需要确认', requestedAt: staleAt, }; const userInputRequest = { schemaVersion: 'game-creator-agent-user-input.v1', requestId: 'request-1', agentId: 'project-supervisor', taskId: 'project-supervisor', sessionId: baseRuntime.sessionId, runId, actionId: 'action-2', status: 'pending' as const, questions: [], allowFreeform: true, responseId: null, requestedAt: staleAt, updatedAt: staleAt, }; const cases: Array<{ label: string; runtime: AgentRuntimeState; runtimeByAgentId?: Record; viewOverrides?: Partial< React.ComponentProps >; }> = [ { label: 'waiting-for-user-input status', runtime: { ...baseRuntime, status: 'waiting-for-user-input' }, }, { label: 'userInputRequest', runtime: { ...baseRuntime, userInputRequest }, }, { label: 'waiting-for-confirmation phase', runtime: { ...baseRuntime, phase: 'waiting-for-confirmation' }, }, { label: 'pendingToolAction', runtime: { ...baseRuntime, pendingToolAction }, }, ...[ 'waiting-for-provider-retry', 'waiting-for-visual-asset', 'waiting-for-process-session', 'waiting-for-delegate-receipts', 'waiting-for-isolated-join', 'waiting-for-manifest-tasks', 'paused', 'pausing', ].map((phase) => ({ label: `${phase} phase`, runtime: { ...baseRuntime, phase }, })), { label: 'pause-requested goal', runtime: { ...baseRuntime, goalStatus: 'pause-requested' }, }, { label: 'pendingConfirmation', runtime: baseRuntime, viewOverrides: { pendingConfirmation: { commandId: 'preview.start', detail: '启动预览', projectPath: '/tmp/game-chat-status', }, }, }, { label: 'pendingCommand', runtime: baseRuntime, viewOverrides: { pendingCommand: { id: 'preview.start' } }, }, { label: 'child provider retry', runtime: baseRuntime, runtimeByAgentId: { 'code-director': gameChatRuntimeState({ agentId: 'code-director', taskId: 'code-director', sessionId: 'game-chat-waiting-code-session', runId: 'game-chat-waiting-code-run', source: 'agent-ready-task-scheduler', parentAgentId: 'project-supervisor', parentRunId: runId, phase: 'waiting-for-provider-retry', updatedAt: staleAt, }), }, }, ]; for (const candidate of cases) { renderGameChatStatus(candidate); const text = screen.getByLabelText('最新状态').textContent ?? ''; expect(text, candidate.label).not.toContain('疑似停滞'); expect(text, candidate.label).not.toContain('无新进度'); cleanup(); } }); it('still reports a stalled child when another child is in an expected wait', () => { const now = Date.UTC(2026, 7, 5, 2, 55, 3); vi.spyOn(Date, 'now').mockReturnValue(now); const runId = 'game-chat-mixed-stall-run'; const staleAt = now - 10 * 60 * 1000; const runtime = gameChatRuntimeState({ runId, updatedAt: staleAt, }); const waitingChild = gameChatRuntimeState({ agentId: 'art-director', taskId: 'art-director', sessionId: 'game-chat-waiting-art-session', runId: 'game-chat-waiting-art-run', source: 'agent-ready-task-scheduler', parentAgentId: 'project-supervisor', parentRunId: runId, phase: 'waiting-for-provider-retry', updatedAt: now - 10 * 1000, }); const stalledChild = gameChatRuntimeState({ agentId: 'code-prototype', taskId: 'code-prototype', sessionId: 'game-chat-stalled-code-session', runId: 'game-chat-stalled-code-run', source: 'agent-ready-task-scheduler', parentAgentId: 'project-supervisor', parentRunId: runId, updatedAt: staleAt, }); renderGameChatStatus({ runtime, runtimeByAgentId: { 'art-director': waitingChild, 'code-prototype': stalledChild, }, }); const text = screen.getByLabelText('最新状态').textContent ?? ''; expect(text).toContain('运行中 · 疑似停滞'); expect(text).toContain('10 分 0 秒无新进度'); }); it('freezes elapsed time when the run reaches a terminal state', () => { vi.useFakeTimers(); const now = Date.UTC(2026, 7, 5, 2, 55, 3); vi.setSystemTime(now); try { const runId = 'game-chat-terminal-duration-run'; const runtime = gameChatRuntimeState({ runId, status: 'completed', phase: 'completed', updatedAt: now - 60 * 1000, recentEvents: [ gameChatRuntimeEvent({ runId, eventType: 'turn.started', summary: '本轮开始', updatedAt: now - 10 * 60 * 1000, }), gameChatRuntimeEvent({ runId, eventType: 'turn.completed', status: 'completed', phase: 'completed', summary: '本轮完成', updatedAt: now - 60 * 1000, }), ], }); const childRuntime = gameChatRuntimeState({ agentId: 'code-director', taskId: 'code-director', sessionId: 'game-chat-terminal-duration-code-session', runId: 'game-chat-terminal-duration-code-run', source: 'agent-ready-task-scheduler', parentAgentId: 'project-supervisor', parentRunId: runId, status: 'completed', phase: 'completed', updatedAt: now - 30 * 1000, recentEvents: [ gameChatRuntimeEvent({ agentId: 'code-director', taskId: 'code-director', sessionId: 'game-chat-terminal-duration-code-session', runId: 'game-chat-terminal-duration-code-run', eventType: 'turn.completed', status: 'completed', phase: 'completed', summary: '子任务晚于父 Run 收口', updatedAt: now - 30 * 1000, }), ], }); renderGameChatStatus({ runtime, runtimeByAgentId: { 'code-director': childRuntime }, }); expect(screen.getByLabelText('最新状态').textContent).toContain( '已持续 9 分 0 秒', ); act(() => { vi.advanceTimersByTime(60 * 60 * 1000); }); expect(screen.getByLabelText('最新状态').textContent).toContain( '已持续 9 分 0 秒', ); } finally { cleanup(); vi.useRealTimers(); } }); it('hides internal loop iteration wording from game-chat latest status events', () => { const runtime = gameChatRuntimeState({ runId: 'game-chat-loop-wording-run', recentEvents: [ gameChatRuntimeEvent({ runId: 'game-chat-loop-wording-run', eventType: 'turn.progress', summary: 'Agent 已形成第 8 轮有效进度', detail: '生成 Agent 工具计划(第 9 轮)', updatedAt: 9000, }), ], }); renderGameChatStatus({ runtime }); expect(document.body.textContent).not.toMatch(/第\s*\d+\s*轮/u); fireEvent.click(screen.getByRole('button', { name: '运行详情' })); const eventList = screen.getByLabelText('最近运行活动'); expect(eventList.textContent).toContain('Agent 已形成本轮有效进度'); expect(eventList.textContent).toContain('生成 Agent 工具计划(本轮)'); expect(document.body.textContent).not.toMatch(/第\s*\d+\s*轮/u); }); it('shows and archives the explicit mud point interruption from a failed art child runtime', () => { const rootRunId = 'game-chat-mud-point-root-run'; const runtime = gameChatRuntimeState({ runId: rootRunId, status: 'failed', phase: 'failed', error: '专业 Agent 执行失败', updatedAt: 9100, }); const artRuntime = gameChatRuntimeState({ agentId: 'art-asset-plan', taskId: 'art-asset-plan', sessionId: 'game-chat-mud-point-art-session', runId: 'game-chat-mud-point-art-run', source: 'agent-delegate', parentAgentId: 'project-supervisor', parentRunId: rootRunId, status: 'failed', phase: 'failed', error: '平台图片生成任务失败:可消费泥点不足:需要 10,扣除退款占用后可用 2;operationId=private-operation-id', updatedAt: 9000, }); const runtimeByAgentId = { 'art-asset-plan': artRuntime }; renderGameChatStatus({ runtime, runtimeByAgentId }); const status = screen.getByLabelText('最新状态').textContent ?? ''; expect(status).toContain(MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE); expect(status).not.toContain('operationId'); expect(status).not.toContain('private-operation-id'); const progress = buildGameChatProgressEvidence( runtime, runtimeByAgentId, null, ); if (!progress) { throw new Error('missing failed game-chat progress fixture'); } const stageRecord = formatGameChatStageRecord(runtime, progress, []); expect(stageRecord).toContain(MUD_POINT_INSUFFICIENT_INTERRUPTION_MESSAGE); expect(stageRecord).not.toContain('operationId'); expect(stageRecord).not.toContain('private-operation-id'); }); it('counts only the seven first-playable tasks in game-chat progress', () => { const manifest = createGameCreationAppManifest( 'game-chat-progress-total', 'game-chat-progress-total', ); manifest.tasks = manifest.tasks.map((task) => isGameChatStageTask(task.id) ? { ...task, status: 'completed' as const } : task, ); const runtime = gameChatRuntimeState({ runId: 'game-chat-progress-total-run', status: 'running', phase: 'execution', updatedAt: 9000, }); renderGameChatStatus({ runtime, manifest }); const status = screen.getByLabelText('最新状态'); expect(status.textContent).toContain('任务图 7/7'); expect(status.textContent).not.toContain('publish-strategy'); expect(status.textContent).not.toContain('publish-package'); fireEvent.click(within(status).getByRole('button', { name: '运行详情' })); const progress = screen.getByLabelText('Supervisor 进度播报'); expect(progress.textContent).toContain('任务图 7/7'); }); it('keeps one live progress card while persisting every public game-chat output as a message', async () => { const projectPath = '/tmp/game-chat-progress-broadcast'; const supervisorRunId = 'game-chat-progress-run'; const previewFailure = gameChatRuntimeEvent({ runId: supervisorRunId, eventType: 'observation', status: 'failed', phase: 'tool-observation', summary: 'preview.validate:failed', detail: JSON.stringify({ diagnostics: [ { message: '角色仍会穿过右侧墙体' }, '失败后重开按钮没有响应', ], diagnosticsCount: 2, passed: false, playtestPassed: false, revision: 7, }), updatedAt: 7100, }); const initialSupervisorOverrides = { runId: supervisorRunId, source: 'project-supervisor-game-chat', runProfile: 'autonomous-game-build' as const, status: 'running', phase: 'execution', loopIteration: 4, currentTask: '完成首版可运行原型', currentAction: '核对首版试玩诊断', planRevision: 1, planSteps: [ { index: 0, title: '完成基础玩法', status: 'completed' }, { index: 1, title: '核对首版试玩诊断', status: 'in_progress' }, { index: 2, title: '修复并复测', status: 'pending' }, ], activePlanStepIndex: 1, recentEvents: [previewFailure], updatedAt: 7200, }; let professionalRuntimes: Array> = []; const harness = createProjectSupervisorRuntimeHarness({ projectPath, initialRuntime: initialSupervisorOverrides, runtimeMapLoader: async () => professionalRuntimes, }); const initialSupervisor = harness.runtimeState( initialSupervisorOverrides, ) as AgentRuntimeState; const codePrototypeRuntime = harness.runtimeState({ agentId: 'code-prototype', taskId: 'code-prototype', sessionId: 'code-prototype-progress-session', runId: 'code-prototype-progress-run', source: 'agent-ready-task-scheduler', parentAgentId: 'project-supervisor', parentRunId: supervisorRunId, delegationId: 'code-prototype-progress-delegation', status: 'running', phase: 'execution', loopIteration: 2, currentTask: '修复首版程序问题', currentAction: '修复角色碰撞与重开逻辑', planSteps: [ { index: 0, title: '修复角色碰撞与重开逻辑', status: 'in_progress', }, ], activePlanStepIndex: 0, updatedAt: 7300, }) as AgentRuntimeState; professionalRuntimes = [codePrototypeRuntime]; harness.setRuntimeReader(async () => ({ ...harness.runtimeResult(initialSupervisor), recentEvents: initialSupervisor.recentEvents, })); const manifest = createGameCreationAppManifest( 'game-chat-progress-broadcast', 'game-chat-progress-broadcast', ); manifest.tasks = manifest.tasks.map((task) => { if ( ['design-director', 'art-director', 'code-director'].includes(task.id) ) { return { ...task, status: 'completed' as const }; } if (task.id === 'code-prototype') { return { ...task, status: 'running' as const }; } return task; }); let runtimeUpdateHandler: | ((event: { payload: { projectPath: string; agentId: string; runId: string; status: string; phase: string; runtime: Record; }; }) => void) | null = null; const listen = vi.fn( async ( eventName: string, handler: (event: { payload: { projectPath: string; agentId: string; runId: string; status: string; phase: string; runtime: Record; }; }) => void, ) => { if (eventName === 'game-creator-agent-runtime-update') { runtimeUpdateHandler = handler; } return harness.listen(eventName, handler); }, ); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: 'game-chat-progress-broadcast', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'init_local_game_project') { return { projectPath, manifestPath: `${projectPath}/.agent/manifest.json`, manifest, }; } if (command === 'get_local_game_preview_status') { return { status: 'stopped', url: null, port: null, root: null, }; } return harness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen }, }; render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, gameChatOnly: true, }), ); const status = await screen.findByLabelText('最新状态'); expect(status.textContent).toContain('任务图 3/7 · 进行中 1 · 计划 1/3'); expect(status.textContent).toContain('核对首版试玩诊断'); expect(status.textContent).toContain('1 个专业 Agent 活跃'); const runtimeEventAppends = () => invoke.mock.calls.filter( ([command, args]) => command === 'append_local_conversation_message' && args?.agentId === null && String(args?.messageId ?? '').startsWith('game-chat-runtime-event:'), ); await waitFor(() => { expect(runtimeEventAppends()).toHaveLength(1); }); fireEvent.click(within(status).getByRole('button', { name: '运行详情' })); const progress = await screen.findByLabelText('Supervisor 进度播报'); expect(screen.getAllByLabelText('Supervisor 进度播报')).toHaveLength(1); expect(progress.getAttribute('data-runtime-owned')).toBe('true'); expect(progress.getAttribute('data-run-id')).toBe(supervisorRunId); expect(within(progress).getByText('本轮生成进度')).not.toBeNull(); expect(within(progress).queryByText(/第 4 轮/u)).toBeNull(); expect( within(progress).getByText('任务图 3/7 · 进行中 1 · 计划 1/3'), ).not.toBeNull(); expect(within(progress).getByText('核对首版试玩诊断')).not.toBeNull(); expect(within(progress).getByText('活跃专业 Agent')).not.toBeNull(); expect( within(progress).getByText('程序原型 Agent · 修复角色碰撞与重开逻辑'), ).not.toBeNull(); expect(within(progress).getByText('试玩未通过')).not.toBeNull(); expect( within(progress).getByText( 'revision 7 · 诊断 2 项 · 角色仍会穿过右侧墙体 · 失败后重开按钮没有响应', ), ).not.toBeNull(); const delegateDecision = gameChatRuntimeEvent({ runId: supervisorRunId, eventType: 'action', phase: 'tool-action', summary: '调用工具 agent.delegate', detail: 'code-prototype 根据试玩诊断返工碰撞与重开逻辑', updatedAt: 7400, }); const updatedSupervisor = harness.runtimeState({ ...initialSupervisorOverrides, loopIteration: 5, planRevision: 2, planSteps: [ { index: 0, title: '完成基础玩法', status: 'completed' }, { index: 1, title: '核对首版试玩诊断', status: 'completed' }, { index: 2, title: '安排程序 Agent 返工', status: 'in_progress' }, ], activePlanStepIndex: 2, recentEvents: [previewFailure, delegateDecision], updatedAt: 7500, }) as AgentRuntimeState; const updateHandler = runtimeUpdateHandler; if (!updateHandler) { throw new Error('missing game creator runtime update listener'); } act(() => { updateHandler({ payload: { projectPath, agentId: 'project-supervisor', runId: supervisorRunId, status: 'running', phase: 'execution', runtime: { ...harness.runtimeResult(updatedSupervisor), recentEvents: updatedSupervisor.recentEvents, }, }, }); }); await waitFor(() => { expect(within(progress).getByText('本轮生成进度')).not.toBeNull(); expect(within(progress).queryByText(/第 5 轮/u)).toBeNull(); expect( within(progress).getByText('任务图 3/7 · 进行中 1 · 计划 2/3'), ).not.toBeNull(); expect(within(progress).getByText('安排程序 Agent 返工')).not.toBeNull(); expect(within(progress).getByText('返工决定')).not.toBeNull(); expect( within(progress).getByText( 'code-prototype 根据试玩诊断返工碰撞与重开逻辑', ), ).not.toBeNull(); }); expect(screen.getByLabelText('Supervisor 进度播报')).toBe(progress); expect(screen.getAllByLabelText('Supervisor 进度播报')).toHaveLength(1); await waitFor(() => { expect(runtimeEventAppends()).toHaveLength(2); }); }); it('renders registered image outcomes in one runtime-owned game-chat Supervisor card', async () => { const projectPath = '/tmp/game-chat-result-images'; const harness = createProjectSupervisorRuntimeHarness({ projectPath }); const manifest = createGameCreationAppManifest( 'game-chat-result-images', 'game-chat-result-images', ); manifest.assets.push( { id: 'art-spritesheet', kind: 'art-spritesheet', mediaType: 'image/png', localPath: 'assets/art-spritesheet.png', source: { kind: 'canvas', taskId: 'art-asset-plan' }, }, { id: 'ui-prototype', kind: 'ui-prototype', mediaType: 'image/webp', localPath: 'assets/ui-prototype.webp', source: { kind: 'canvas', taskId: 'design-foundation' }, }, { id: 'audio-manifest', kind: 'audio', mediaType: 'audio/mpeg', localPath: 'assets/theme.mp3', source: { kind: 'generated' }, }, { id: 'invalid-image-path', kind: 'reference', mediaType: 'image/png', localPath: 'assets/../outside.png', source: { kind: 'generated' }, }, ); expect(collectGameChatResultImages(manifest)).toEqual([ { key: 'art-spritesheet:assets/art-spritesheet.png', label: '美术素材图集', mediaType: 'image/png', path: 'assets/art-spritesheet.png', }, { key: 'ui-prototype:assets/ui-prototype.webp', label: '界面原型', mediaType: 'image/webp', path: 'assets/ui-prototype.webp', }, ]); const pngDataUrl = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII='; const webpDataUrl = 'data:image/webp;base64,UklGRgAAAABXRUJQVlA4'; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: 'game-chat-result-images', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'init_local_game_project') { return { projectPath, manifestPath: `${projectPath}/.agent/manifest.json`, manifest, }; } if (command === 'get_local_game_preview_status') { return { status: 'stopped', url: null, port: null, root: null, }; } if (command === 'read_local_project_image_preview') { const relativePath = String(args?.relativePath ?? ''); const mediaType = relativePath.endsWith('.webp') ? 'image/webp' : 'image/png'; return { path: relativePath, mediaType, byteLen: 12, dataUrl: mediaType === 'image/webp' ? webpDataUrl : pngDataUrl, }; } return harness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: harness.listen }, }; render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, gameChatOnly: true, }), ); fireEvent.click( await screen.findByRole('button', { name: '运行详情 · 成果 2' }), ); const imageCard = await screen.findByLabelText('Supervisor 成果图片'); expect(imageCard.getAttribute('data-runtime-owned')).toBe('true'); expect(screen.getAllByLabelText('Supervisor 成果图片')).toHaveLength(1); expect(within(imageCard).getByText('2/2')).not.toBeNull(); expect( within(imageCard) .getByRole('img', { name: '美术素材图集 · assets/art-spritesheet.png', }) .getAttribute('src'), ).toBe(pngDataUrl); expect( within(imageCard) .getByRole('img', { name: '界面原型 · assets/ui-prototype.webp', }) .getAttribute('src'), ).toBe(webpDataUrl); expect( invoke.mock.calls.filter( ([command]) => command === 'read_local_project_image_preview', ), ).toEqual([ [ 'read_local_project_image_preview', { projectPath, relativePath: 'assets/art-spritesheet.png', }, ], [ 'read_local_project_image_preview', { projectPath, relativePath: 'assets/ui-prototype.webp', }, ], ]); fireEvent.click( within(imageCard).getByRole('button', { name: '查看 美术素材图集', }), ); const viewer = screen.getByRole('dialog', { name: '美术素材图集 图片查看器', }); const scale = within(viewer).getByLabelText('图片缩放比例'); expect(scale.textContent).toBe('100%'); fireEvent.click(within(viewer).getByRole('button', { name: '放大图片' })); expect(scale.textContent).toBe('125%'); const stage = within(viewer).getByLabelText('可缩放拖拽图片区域'); fireEvent.wheel(stage, { deltaY: -1 }); expect(scale.textContent).toBe('150%'); const dispatchPointer = ( type: 'pointerdown' | 'pointermove' | 'pointerup', clientX: number, clientY: number, ) => { const event = new Event(type, { bubbles: true }); Object.defineProperties(event, { button: { value: 0 }, clientX: { value: clientX }, clientY: { value: clientY }, pointerId: { value: 1 }, }); fireEvent(stage, event); }; dispatchPointer('pointerdown', 100, 80); dispatchPointer('pointermove', 136, 104); dispatchPointer('pointerup', 136, 104); const viewerImage = within(viewer).getByRole('img', { name: '美术素材图集 · assets/art-spritesheet.png', }); expect(viewerImage.getAttribute('style')).toContain( 'translate(36px, 24px) scale(1.5)', ); fireEvent.click(within(viewer).getByRole('button', { name: '复位图片' })); expect(scale.textContent).toBe('100%'); expect(viewerImage.getAttribute('style')).toContain( 'translate(0px, 0px) scale(1)', ); fireEvent.keyDown(window, { key: 'Escape' }); expect( screen.queryByRole('dialog', { name: '美术素材图集 图片查看器', }), ).toBeNull(); expect( invoke.mock.calls.filter( ([command]) => command === 'append_local_conversation_message', ), ).toHaveLength(0); }); it('persists one terminal game-chat stage record and keeps it in the next round', async () => { const projectPath = '/tmp/game-chat-stage-record'; const harness = createProjectSupervisorRuntimeHarness({ projectPath }); const manifest = createGameCreationAppManifest( 'game-chat-stage-record', 'game-chat-stage-record', ); manifest.tasks = manifest.tasks.map((task) => isGameChatStageTask(task.id) ? { ...task, status: 'completed' as const } : task, ); manifest.assets.push({ id: 'stage-art', kind: 'art-spritesheet', mediaType: 'image/png', localPath: 'assets/art-spritesheet.png', source: { kind: 'canvas', taskId: 'art-asset-plan' }, }); let runtimeUpdateHandler: | ((event: { payload: { projectPath: string; agentId: string; runId: string; status: string; phase: string; runtime: Record; }; }) => void) | null = null; const listen = vi.fn( async ( eventName: string, handler: (event: { payload: { projectPath: string; agentId: string; runId: string; status: string; phase: string; runtime: Record; }; }) => void, ) => { if (eventName === 'game-creator-agent-runtime-update') { runtimeUpdateHandler = handler; } return harness.listen(eventName, handler); }, ); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: 'game-chat-stage-record', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'init_local_game_project') { return { projectPath, manifestPath: `${projectPath}/.agent/manifest.json`, manifest, }; } if (command === 'get_local_game_preview_status') { return { status: 'stopped', url: null, port: null, root: null, }; } if (command === 'get_local_game_manifest') { return manifest; } if (command === 'read_local_project_image_preview') { return { path: String(args?.relativePath ?? ''), mediaType: 'image/png', byteLen: 12, dataUrl: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', }; } return harness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen }, }; render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, gameChatOnly: true, }), ); const surface = await screen.findByLabelText('游戏创作聊天'); const composer = within(surface).getByLabelText('项目总控对话内容'); await waitFor(() => { expect((composer as HTMLTextAreaElement).disabled).toBe(false); }); fireEvent.change(composer, { target: { value: '完成这一轮并保留证据' } }); fireEvent.click(within(surface).getByRole('button', { name: '发送' })); await waitFor(() => { expect( invoke.mock.calls.some( ([command]) => command === 'start_game_creator_supervisor_runtime_task', ), ).toBe(true); }); const startCall = invoke.mock.calls.find( ([command]) => command === 'start_game_creator_supervisor_runtime_task', ); const firstRunId = String(startCall?.[1]?.runId ?? ''); expect(firstRunId).not.toBe(''); const previewPassed = gameChatRuntimeEvent({ runId: firstRunId, eventType: 'observation', summary: 'preview.validate:ok · 浏览器验证已通过', detail: JSON.stringify({ diagnosticsCount: 0, passed: true, playtestPassed: true, revision: 9, }), updatedAt: 9100, }); const rework = gameChatRuntimeEvent({ runId: firstRunId, eventType: 'action', summary: '调用工具 agent.delegate', detail: '根据上一版试玩诊断安排程序 Agent 完成返工', updatedAt: 9000, }); const terminalRuntime = harness.runtimeState({ runId: firstRunId, runProfile: 'autonomous-game-build', status: 'idle', phase: 'completed', loopIteration: 6, currentAction: '等待下一轮输入', planSteps: [ { index: 0, title: '完成返工', status: 'completed' }, { index: 1, title: '通过试玩', status: 'completed' }, ], activePlanStepIndex: null, recentEvents: [rework, previewPassed], updatedAt: 9200, }); harness.appendSupervisorMessage({ schemaVersion: 'game-creator-conversation.v1', role: 'assistant', content: '第一轮已经完成。', agentId: 'project-supervisor', messageId: 'stage-record-final-assistant', updatedAt: 9200, }); const updateHandler = runtimeUpdateHandler; if (!updateHandler) { throw new Error('missing game creator runtime update listener'); } act(() => { updateHandler({ payload: { projectPath, agentId: 'project-supervisor', runId: firstRunId, status: 'idle', phase: 'completed', runtime: { ...harness.runtimeResult(terminalRuntime), recentEvents: [rework, previewPassed], }, }, }); }); const stageRecordText = await screen.findByText( /【Supervisor 阶段记录】[\s\S]*本轮生成进度 · 本轮已完成/, ); const stageRecord = stageRecordText.closest('p'); expect(stageRecord?.className).toContain('game-chat-stage-record'); expect(screen.queryByLabelText('Supervisor 进度播报')).toBeNull(); expect(stageRecordText.textContent).toContain('试玩通过:revision 9'); expect(stageRecordText.textContent).toContain( '返工决定:根据上一版试玩诊断安排程序 Agent 完成返工', ); expect(stageRecordText.textContent).toContain( '成果图片:美术素材图集(assets/art-spritesheet.png)', ); await waitFor(() => { expect( invoke.mock.calls.filter( ([command, args]) => command === 'append_local_conversation_message' && args?.agentId === null && String( (args?.message as { content?: string } | undefined)?.content ?? '', ).startsWith('【Supervisor 阶段记录】'), ), ).toHaveLength(1); }); fireEvent.change(composer, { target: { value: '开始下一轮' } }); fireEvent.click(within(surface).getByRole('button', { name: '发送' })); await waitFor(() => { expect( invoke.mock.calls.filter( ([command]) => command === 'start_game_creator_supervisor_runtime_task', ), ).toHaveLength(2); }); expect(screen.getAllByText(/【Supervisor 阶段记录】/)).toHaveLength(1); expect(screen.getByText(/试玩通过:revision 9/)).not.toBeNull(); }); it('defers the terminal game-chat stage record until the refreshed manifest is terminal', async () => { const projectPath = '/tmp/game-chat-stage-record-manifest-race'; const harness = createProjectSupervisorRuntimeHarness({ projectPath }); const pendingManifest = createGameCreationAppManifest( 'game-chat-stage-record-manifest-race', 'game-chat-stage-record-manifest-race', ); const terminalManifest = createGameCreationAppManifest( 'game-chat-stage-record-manifest-race', 'game-chat-stage-record-manifest-race', ); terminalManifest.tasks = terminalManifest.tasks.map((task) => task.id === 'preview-playtest' ? { ...task, status: 'failed' as const } : [ 'design-director', 'art-director', 'art-asset-plan', 'code-director', 'code-prototype', 'preview-readiness', ].includes(task.id) ? { ...task, status: 'completed' as const } : task, ); let manifestReady = false; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: 'game-chat-stage-record-manifest-race', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'init_local_game_project') { return { projectPath, manifestPath: `${projectPath}/.agent/manifest.json`, manifest: pendingManifest, }; } if (command === 'get_local_game_preview_status') { return { status: 'stopped', url: null, port: null, root: null, }; } if (command === 'get_local_game_manifest') { return manifestReady ? terminalManifest : pendingManifest; } return harness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: harness.listen }, }; render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, gameChatOnly: true, }), ); const composer = screen.getByRole('textbox') as HTMLTextAreaElement; const form = composer.form; if (!form) { throw new Error('missing game-chat composer form'); } const stageRecordAppends = () => invoke.mock.calls.filter( ([command, args]) => command === 'append_local_conversation_message' && args?.agentId === null && isGameChatStageRecordMessage( String( (args?.message as { content?: string } | undefined)?.content ?? '', ), ), ); await waitFor(() => { expect(composer.disabled).toBe(false); }); fireEvent.change(composer, { target: { value: 'manifest race' } }); fireEvent.submit(form); await waitFor(() => { expect( invoke.mock.calls.some( ([command]) => command === 'start_game_creator_supervisor_runtime_task', ), ).toBe(true); }); const startCall = invoke.mock.calls.find( ([command]) => command === 'start_game_creator_supervisor_runtime_task', ); const runId = String(startCall?.[1]?.runId ?? ''); expect(runId).not.toBe(''); act(() => { harness.emitRuntime( harness.runtimeState({ runId, status: 'failed', phase: 'failed', currentAction: 'preview-playtest failed', recentEvents: [ gameChatRuntimeEvent({ runId, eventType: 'observation', status: 'failed', phase: 'tool-observation', summary: 'preview.validate failed', detail: JSON.stringify({ diagnosticsCount: 1, passed: false, playtestPassed: false, revision: 9, }), updatedAt: 9100, }), ], updatedAt: 9200, }), ); }); await waitFor(() => { expect(stageRecordAppends()).toHaveLength(0); }); manifestReady = true; fireEvent.change(composer, { target: { value: '/tasks' } }); fireEvent.submit(form); await waitFor(() => { expect( invoke.mock.calls.some( ([command, args]) => command === 'get_local_game_manifest' && args?.commandId === 'task.list', ), ).toBe(true); }); await waitFor(() => { expect(stageRecordAppends()).toHaveLength(1); }); const stageRecord = String( (stageRecordAppends()[0]?.[1] as { message?: { content?: string } }) ?.message?.content ?? '', ); expect(stageRecord).toContain('6/7'); }); it('archives a terminal game-chat run restored during initial hydration exactly once', async () => { const projectPath = '/tmp/game-chat-stage-record-initial-terminal'; const harness = createProjectSupervisorRuntimeHarness({ projectPath }); const manifest = createGameCreationAppManifest( 'game-chat-stage-record-initial-terminal', 'game-chat-stage-record-initial-terminal', ); manifest.tasks = manifest.tasks.map((task) => isGameChatStageTask(task.id) ? { ...task, status: 'completed' as const } : task, ); const runId = 'game-chat-stage-record-initial-terminal-run'; const terminalRuntime = harness.runtimeState({ runId, status: 'completed', phase: 'completed', currentAction: 'preview complete', recentEvents: [ gameChatRuntimeEvent({ runId, eventType: 'observation', status: 'completed', phase: 'tool-observation', summary: 'preview.validate:ok · 试玩验证已通过', detail: JSON.stringify({ diagnosticsCount: 0, passed: true, playtestPassed: true, revision: 11, }), updatedAt: 9100, }), ], updatedAt: 9200, }); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: manifest.name, recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'init_local_game_project') { return { projectPath, manifestPath: `${projectPath}/.agent/manifest.json`, manifest, }; } if (command === 'get_local_game_manifest') { return manifest; } if (command === 'read_game_creator_agent_runtime') { return harness.runtimeResult(terminalRuntime); } return harness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: harness.listen }, }; render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, gameChatOnly: true, }), ); const stageRecordAppends = () => invoke.mock.calls.filter( ([command, args]) => command === 'append_local_conversation_message' && args?.agentId === null && isGameChatStageRecordMessage( String( (args?.message as { content?: string } | undefined)?.content ?? '', ), ), ); await waitFor(() => { expect(stageRecordAppends()).toHaveLength(1); }); expect(screen.getAllByText(/【Supervisor 阶段记录】/)).toHaveLength(1); // A later refresh/runtime snapshot for the same run must not append again. fireEvent.change(screen.getByRole('textbox'), { target: { value: '/tasks' }, }); fireEvent.submit( (screen.getByRole('textbox') as HTMLTextAreaElement).form!, ); await waitFor(() => { expect( invoke.mock.calls.some( ([command, args]) => command === 'get_local_game_manifest' && args?.commandId === 'task.list', ), ).toBe(true); }); expect(stageRecordAppends()).toHaveLength(1); }); it('does not duplicate a historical terminal game-chat stage record during restart hydration', async () => { const projectPath = '/tmp/game-chat-stage-record-restart-hydration'; const runId = 'game-chat-stage-record-restart-run'; const terminalRuntime = gameChatRuntimeState({ sessionId: 'supervisor-session-active', runId, status: 'completed', phase: 'completed', currentAction: 'preview complete', updatedAt: 9200, }); const manifest = createGameCreationAppManifest( 'game-chat-stage-record-restart-hydration', 'game-chat-stage-record-restart-hydration', ); manifest.tasks = manifest.tasks.map((task) => isGameChatStageTask(task.id) ? { ...task, status: 'completed' as const } : task, ); const progress = buildGameChatProgressEvidence( terminalRuntime, {}, manifest, ); if (!progress) { throw new Error('missing terminal game-chat progress fixture'); } const historicalStageRecord = formatGameChatStageRecord( terminalRuntime, progress, collectGameChatResultImages(manifest), ); const harness = createProjectSupervisorRuntimeHarness({ projectPath, initialRuntime: terminalRuntime, projectMessages: [ { role: 'assistant', content: historicalStageRecord, agentId: null, messageId: 'historical-game-chat-stage-record', updatedAt: 9201, }, ], }); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: manifest.name, recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'init_local_game_project') { return { projectPath, manifestPath: `${projectPath}/.agent/manifest.json`, manifest, }; } if (command === 'get_local_game_manifest') { return manifest; } if (command === 'get_local_game_preview_status') { return { status: 'stopped', url: null, port: null, root: null, }; } return harness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: harness.listen }, }; render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, gameChatOnly: true, }), ); const stageRecordAppends = () => invoke.mock.calls.filter( ([command, args]) => command === 'append_local_conversation_message' && args?.agentId === null && isGameChatStageRecordMessage( String( (args?.message as { content?: string } | undefined)?.content ?? '', ), ), ); await waitFor(() => { expect(screen.getAllByText(/【Supervisor 阶段记录】/)).toHaveLength(1); }); expect(stageRecordAppends()).toHaveLength(0); // A terminal runtime event can race the hydration refresh. The historical // record must remain the sole record and must not be appended again. act(() => { harness.emitRuntime( harness.runtimeState({ sessionId: terminalRuntime.sessionId, runId, status: 'completed', phase: 'completed', currentAction: 'preview complete', updatedAt: 9300, }), ); }); await waitFor(() => { expect(screen.getAllByText(/【Supervisor 阶段记录】/)).toHaveLength(1); }); expect(stageRecordAppends()).toHaveLength(0); }); it('starts and displays the first playable game-chat preview exactly once', async () => { const projectPath = '/tmp/game-chat-auto-preview'; const harness = createProjectSupervisorRuntimeHarness({ projectPath }); const pendingManifest = createGameCreationAppManifest( 'game-chat-auto-preview', 'game-chat-auto-preview', ); const completedManifest = createGameCreationAppManifest( 'game-chat-auto-preview', 'game-chat-auto-preview', ); completedManifest.tasks = completedManifest.tasks.map((task) => task.id === 'code-prototype' ? { ...task, status: 'completed' as const } : task, ); let playableVersionReady = false; let previewStarted = false; let acceptedRunId = ''; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: 'game-chat-auto-preview', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'get_local_game_preview_status') { return previewStarted ? { status: 'running', url: 'http://127.0.0.1:4311', port: 4311, root: `${projectPath}/game`, } : { status: 'stopped', url: null, port: null, root: null, }; } if (command === 'get_local_game_manifest') { return playableVersionReady ? completedManifest : pendingManifest; } if (command === 'start_local_game_preview') { expect(args).toEqual({ projectPath, expectedRevision: 1 }); previewStarted = true; return { url: 'http://127.0.0.1:4311', port: 4311, root: `${projectPath}/game`, }; } if (command === 'start_game_creator_supervisor_runtime_task') { acceptedRunId = String(args?.runId ?? ''); } return harness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: harness.listen }, }; render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, gameChatOnly: true, }), ); const surface = await screen.findByLabelText('游戏创作聊天'); const composer = within(surface).getByLabelText('项目总控对话内容'); await waitFor(() => { expect((composer as HTMLTextAreaElement).disabled).toBe(false); }); fireEvent.change(composer, { target: { value: '生成第一版可玩原型' }, }); fireEvent.click(within(surface).getByRole('button', { name: '发送' })); await waitFor(() => { expect(invoke).toHaveBeenCalledWith( 'start_game_creator_supervisor_runtime_task', { projectPath, sessionId: harness.sessionId, task: '生成第一版可玩原型', runId: expect.stringMatching(/^project-supervisor-task-/), runProfile: 'autonomous-game-build', source: 'project-supervisor-game-chat', }, ); }); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(0); expect(screen.queryByLabelText('游戏运行')).toBeNull(); expect(within(surface).getByText('预览未启动')).not.toBeNull(); playableVersionReady = true; harness.setProjectRevision(1); const firstPlayableRevision = gameChatPreviewPlaytestRuntime({ parentRunId: acceptedRunId, revision: 1, updatedAt: 3900, }); await act(async () => { harness.emitRuntime( harness.runtimeState({ runId: acceptedRunId, runProfile: 'autonomous-game-build', status: 'running', phase: 'verification', currentTask: '验证首个可玩版本', currentGoal: '交付首个可玩版本', currentAction: '可玩版本已经通过验证', updatedAt: 4000, }), ); harness.emitAgentRuntime(firstPlayableRevision); await Promise.resolve(); }); expect( await screen.findByLabelText('游戏运行', {}, { timeout: 3000 }), ).not.toBeNull(); const previewFrame = screen.getByTitle( 'game-chat-auto-preview 游戏运行画面', ) as HTMLIFrameElement; expect(previewFrame.src).toMatch(/^http:\/\/127\.0\.0\.1:4311\//); await waitFor(() => { expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(1); }); expect(invoke).toHaveBeenCalledWith('append_local_permission_log', { projectPath, event: 'permission.confirm', commandId: 'preview.start', }); expect( invoke.mock.calls.filter( ([command]) => command === 'start_game_creator_supervisor_runtime_task', ), ).toHaveLength(1); }); it('restores an authorized playable game-chat preview after restart without starting it twice', async () => { const projectPath = '/tmp/game-chat-auto-preview-restart'; const runId = 'game-chat-auto-preview-restart-run'; const revision = 7; const harness = createProjectSupervisorRuntimeHarness({ projectPath, initialProjectRevision: revision, initialRuntime: { sessionId: 'supervisor-session-active', runId, status: 'completed', phase: 'completed', updatedAt: 9200, }, runtimeMapLoader: async () => [ gameChatPreviewPlaytestRuntime({ parentRunId: runId, revision, updatedAt: 9100, }), ], }); const manifest = createGameCreationAppManifest( 'game-chat-auto-preview-restart', 'game-chat-auto-preview-restart', ); manifest.tasks = manifest.tasks.map((task) => task.id === 'code-prototype' ? { ...task, status: 'completed' as const } : task, ); let previewStarted = false; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: manifest.name, recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'init_local_game_project') { return { projectPath, manifestPath: `${projectPath}/.agent/manifest.json`, manifest, }; } if (command === 'get_local_game_manifest') { return manifest; } if (command === 'get_local_game_project_revision') { return { revision }; } if (command === 'get_local_game_preview_status') { return previewStarted ? { status: 'running', url: 'http://127.0.0.1:4327', port: 4327, root: `${projectPath}/game`, } : { status: 'stopped', url: null, port: null, root: null }; } if (command === 'start_local_game_preview') { expect(args).toEqual({ projectPath, expectedRevision: revision }); previewStarted = true; return { url: 'http://127.0.0.1:4327', port: 4327, root: `${projectPath}/game`, }; } return harness.invoke(command, args); }, ); window.localStorage.setItem( 'genarrative.game-chat.auto-preview-authorization.v2', JSON.stringify({ afterRevision: 0, afterValidatedAt: 0, authorizationId: 'restored-preview-authorization', projectPath, runId, }), ); window.__TAURI__ = { core: { invoke }, event: { listen: harness.listen }, }; const renderRelease = () => render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, gameChatOnly: true, }), ); let rendered = renderRelease(); await waitFor( () => { expect(invoke.mock.calls).toContainEqual( expect.arrayContaining(['start_local_game_preview']), ); }, { timeout: 5000 }, ); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(1); expect(screen.getByLabelText('游戏运行')).not.toBeNull(); expect( window.localStorage.getItem( 'genarrative.game-chat.auto-preview-authorization.v2', ), ).toBeNull(); rendered.unmount(); rendered = renderRelease(); await waitFor(() => { expect(screen.getByLabelText('游戏运行')).not.toBeNull(); }); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(1); rendered.unmount(); }); it('refreshes a running game-chat iframe after a later run completes without restarting the preview', async () => { const projectPath = '/tmp/game-chat-preview-revision'; const harness = createProjectSupervisorRuntimeHarness({ projectPath }); const playableManifest = createGameCreationAppManifest( 'game-chat-preview-revision', 'game-chat-preview-revision', ); playableManifest.tasks = playableManifest.tasks.map((task) => task.id === 'code-prototype' ? { ...task, status: 'completed' as const } : task, ); const acceptedRunIds: string[] = []; let previewStarted = false; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: 'game-chat-preview-revision', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'get_local_game_preview_status') { return previewStarted ? { status: 'running', url: 'http://127.0.0.1:4314', port: 4314, root: `${projectPath}/game`, } : { status: 'stopped', url: null, port: null, root: null, }; } if (command === 'get_local_game_manifest') { return playableManifest; } if (command === 'start_local_game_preview') { expect(args).toEqual({ projectPath, expectedRevision: 1 }); previewStarted = true; return { url: 'http://127.0.0.1:4314', port: 4314, root: `${projectPath}/game`, }; } if (command === 'start_game_creator_supervisor_runtime_task') { acceptedRunIds.push(String(args?.runId ?? '')); } return harness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: harness.listen }, }; render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, gameChatOnly: true, }), ); const surface = await screen.findByLabelText('游戏创作聊天'); const composer = within(surface).getByLabelText('项目总控对话内容'); await waitFor(() => { expect((composer as HTMLTextAreaElement).disabled).toBe(false); }); fireEvent.change(composer, { target: { value: '生成第一版可玩原型' }, }); fireEvent.click(within(surface).getByRole('button', { name: '发送' })); await waitFor(() => { expect(acceptedRunIds).toHaveLength(1); }); harness.setProjectRevision(1); const firstPreviewPassed = gameChatPreviewPlaytestRuntime({ parentRunId: acceptedRunIds[0], revision: 1, updatedAt: 3900, }); await act(async () => { harness.emitRuntime( harness.runtimeState({ runId: acceptedRunIds[0], runProfile: 'autonomous-game-build', status: 'running', phase: 'verification', currentTask: '验证第一版可玩原型', currentGoal: '交付第一版可玩原型', currentAction: '第一版已经通过浏览器验证', updatedAt: 4000, }), ); harness.emitAgentRuntime(firstPreviewPassed); await Promise.resolve(); }); const firstFrame = (await screen.findByTitle( 'game-chat-preview-revision 游戏运行画面', {}, { timeout: 3000 }, )) as HTMLIFrameElement; const firstFrameUrl = firstFrame.src; expect(acceptedRunIds).toHaveLength(1); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(1); await act(async () => { harness.emitRuntime( harness.runtimeState({ runId: acceptedRunIds[0], runProfile: 'autonomous-game-build', status: 'completed', phase: 'completed', currentTask: '第一版已经完成', currentGoal: '第一版已经完成', currentAction: '完成交付', lastResponse: '第一版已经完成', updatedAt: 5000, }), ); await Promise.resolve(); }); await waitFor(() => { expect((composer as HTMLTextAreaElement).disabled).toBe(false); }); const previewStatusCallsBeforeRevision = invoke.mock.calls.filter( ([command]) => command === 'get_local_game_preview_status', ).length; fireEvent.change(composer, { target: { value: '更新第二版玩法和平衡性' }, }); fireEvent.click(within(surface).getByRole('button', { name: '发送' })); await waitFor(() => { expect(acceptedRunIds).toHaveLength(2); expect( invoke.mock.calls.filter( ([command]) => command === 'get_local_game_preview_status', ).length, ).toBeGreaterThan(previewStatusCallsBeforeRevision); }); expect(screen.getByTitle('game-chat-preview-revision 游戏运行画面')).toBe( firstFrame, ); expect(firstFrame.src).toBe(firstFrameUrl); harness.setProjectRevision(2); const secondPreviewPassed = gameChatPreviewPlaytestRuntime({ parentRunId: acceptedRunIds[1], revision: 2, updatedAt: 7900, }); await act(async () => { harness.emitRuntime( harness.runtimeState({ runId: acceptedRunIds[1], runProfile: 'autonomous-game-build', status: 'completed', phase: 'completed', currentTask: '第二版已经完成', currentGoal: '第二版已经完成', currentAction: '完成第二版验证', lastResponse: '第二版已经完成', updatedAt: 8000, }), ); harness.emitAgentRuntime(secondPreviewPassed); await Promise.resolve(); }); await waitFor( () => { const refreshedFrame = screen.getByTitle( 'game-chat-preview-revision 游戏运行画面', ) as HTMLIFrameElement; expect( refreshedFrame !== firstFrame || refreshedFrame.src !== firstFrameUrl, ).toBe(true); }, { timeout: 3000 }, ); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(1); expect( invoke.mock.calls.filter( ([command]) => command === 'start_game_creator_supervisor_runtime_task', ), ).toHaveLength(2); }, 10000); it('keeps a same-run steer authorization until a newer revision refreshes the running iframe', async () => { const projectPath = '/tmp/game-chat-same-run-steer-preview'; const driver = await renderGameChatAutoPreviewDriver({ projectPath, port: 4381, startPreview: async (args) => { expect(args).toEqual({ projectPath, expectedRevision: 1 }); return { url: 'http://127.0.0.1:4381', port: 4381, root: `${projectPath}/game`, }; }, }); await driver.submit('生成首个可玩版本'); await driver.emitValidation(1, 1000); const firstFrame = (await screen.findByTitle( 'game-chat-same-run-steer-preview 游戏运行画面', {}, { timeout: 3000 }, )) as HTMLIFrameElement; const firstFrameUrl = firstFrame.src; await waitFor(() => { expect(driver.readAuthorization()).toBeNull(); }); await driver.submit('保持同一 Run 并继续调整当前版本'); await waitFor(() => { expect(driver.readAuthorization()).toMatchObject({ afterRevision: 1, afterValidatedAt: 1000, projectPath, runId: driver.parentRunId, authorizationId: expect.any(String), }); }); const steerAuthorizationId = driver.readAuthorization()?.authorizationId; const previewStatusReads = driver.invoke.mock.calls.filter( ([command]) => command === 'get_local_game_preview_status', ).length; await waitFor( () => { expect( driver.invoke.mock.calls.filter( ([command]) => command === 'get_local_game_preview_status', ).length, ).toBeGreaterThan(previewStatusReads); }, { timeout: 3000 }, ); expect(driver.readAuthorization()?.authorizationId).toBe( steerAuthorizationId, ); expect( screen.getByTitle('game-chat-same-run-steer-preview 游戏运行画面'), ).toBe(firstFrame); expect(firstFrame.src).toBe(firstFrameUrl); await driver.emitValidation(2, 2000); await waitFor( () => { const refreshedFrame = screen.getByTitle( 'game-chat-same-run-steer-preview 游戏运行画面', ) as HTMLIFrameElement; expect( refreshedFrame !== firstFrame || refreshedFrame.src !== firstFrameUrl, ).toBe(true); expect(driver.readAuthorization()).toBeNull(); }, { timeout: 3000 }, ); expect( driver.invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(1); expect( driver.invoke.mock.calls.filter( ([command]) => command === 'steer_game_creator_agent_runtime_task', ), ).toHaveLength(1); }, 10000); it('waits for new validation evidence after a revision-drift start failure before retrying', async () => { const projectPath = '/tmp/game-chat-preview-revision-drift'; let setProjectRevisionOnDrift: ((revision: number) => void) | null = null; const driver = await renderGameChatAutoPreviewDriver({ projectPath, port: 4382, startPreview: async (args) => { const expectedRevision = Number(args.expectedRevision); if (expectedRevision === 1) { setProjectRevisionOnDrift?.(2); throw new Error( '本地游戏项目已在验证后发生变化:expectedRevision=1 · currentRevision=2', ); } expect(expectedRevision).toBe(2); return { url: 'http://127.0.0.1:4382', port: 4382, root: `${projectPath}/game`, }; }, }); setProjectRevisionOnDrift = driver.harness.setProjectRevision; await driver.submit('生成后验证 revision 漂移恢复'); await driver.emitValidation(1, 1000); await waitFor(() => { expect( driver.invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(1); expect(driver.readAuthorization()).not.toBeNull(); }); await act(async () => { driver.harness.emitAgentRuntime( gameChatPreviewPlaytestRuntime({ parentRunId: driver.parentRunId, revision: 1, updatedAt: 1000, }), ); await Promise.resolve(); await Promise.resolve(); }); expect( driver.invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(1); expect(driver.readAuthorization()).not.toBeNull(); await driver.emitValidation(2, 2000); expect( await screen.findByLabelText('游戏运行', {}, { timeout: 3000 }), ).not.toBeNull(); expect( driver.invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(2); expect( driver.invoke.mock.calls .filter(([command]) => command === 'start_local_game_preview') .at(-1), ).toEqual([ 'start_local_game_preview', { projectPath, expectedRevision: 2 }, ]); expect(driver.readAuthorization()).toBeNull(); }, 10000); it('does not let a deferred policy result clear a replacement authorization', async () => { await assertNewGameChatAuthorizationSupersedesDeferredAttempt('policy'); }, 10000); it('atomically stops a stale deferred start without clearing its replacement authorization', async () => { await assertNewGameChatAuthorizationSupersedesDeferredAttempt('start'); }, 10000); it('allows a new authorization and later validation to retry the same revision after a terminal attempt', async () => { const projectPath = '/tmp/game-chat-preview-same-revision-retry'; const driver = await renderGameChatAutoPreviewDriver({ projectPath, port: 4383, startPreview: async (args, callIndex) => { expect(args).toEqual({ projectPath, expectedRevision: 1 }); if (callIndex === 1) { throw new Error('模拟 preview.start 非瞬时失败'); } return { url: 'http://127.0.0.1:4383', port: 4383, root: `${projectPath}/game`, }; }, }); await driver.submit('生成同 revision 首次验证'); await driver.emitValidation(1, 1000); await waitFor( () => { expect( driver.invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(1); expect(driver.readAuthorization()).toBeNull(); }, { timeout: 3000 }, ); await driver.submit('同一 Run 新授权后重新验证同一 revision'); await waitFor( () => { expect(driver.readAuthorization()).toMatchObject({ afterRevision: 1, afterValidatedAt: 1000, authorizationId: expect.any(String), }); }, { timeout: 3000 }, ); await driver.emitValidation(1, 2000); expect( await screen.findByLabelText('游戏运行', {}, { timeout: 3000 }), ).not.toBeNull(); expect( driver.invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(2); expect(driver.readAuthorization()).toBeNull(); }, 10000); it('retries the authorized game-chat preview after a transient project write lock', async () => { const projectPath = '/tmp/game-chat-auto-preview-lock-retry'; const harness = createProjectSupervisorRuntimeHarness({ projectPath }); const completedManifest = createGameCreationAppManifest( 'game-chat-auto-preview-lock-retry', 'game-chat-auto-preview-lock-retry', ); completedManifest.tasks = completedManifest.tasks.map((task) => task.id === 'code-prototype' ? { ...task, status: 'completed' as const } : task, ); let previewStarted = false; let startAttempts = 0; let acceptedRunId = ''; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: 'game-chat-auto-preview-lock-retry', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'get_local_game_preview_status') { return previewStarted ? { status: 'running', url: 'http://127.0.0.1:4313', port: 4313, root: `${projectPath}/game`, } : { status: 'stopped', url: null, port: null, root: null, }; } if (command === 'get_local_game_manifest') { return completedManifest; } if (command === 'start_local_game_preview') { expect(args).toEqual({ projectPath, expectedRevision: 3 }); startAttempts += 1; if (startAttempts === 1) { throw new Error( `项目正在被其他写操作占用:${projectPath}/.agent/project.lock`, ); } previewStarted = true; return { url: 'http://127.0.0.1:4313', port: 4313, root: `${projectPath}/game`, }; } if (command === 'start_game_creator_supervisor_runtime_task') { acceptedRunId = String(args?.runId ?? ''); } return harness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: harness.listen }, }; render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, gameChatOnly: true, }), ); const surface = await screen.findByLabelText('游戏创作聊天'); const composer = within(surface).getByLabelText('项目总控对话内容'); await waitFor(() => { expect((composer as HTMLTextAreaElement).disabled).toBe(false); }); fireEvent.change(composer, { target: { value: '生成后自动预览并处理瞬时写锁' }, }); fireEvent.click(within(surface).getByRole('button', { name: '发送' })); await waitFor(() => { expect(acceptedRunId).toMatch(/^project-supervisor-task-/); }); harness.setProjectRevision(3); const playableRevision = gameChatPreviewPlaytestRuntime({ parentRunId: acceptedRunId, revision: 3, updatedAt: 3900, }); await act(async () => { harness.emitRuntime( harness.runtimeState({ runId: acceptedRunId, runProfile: 'autonomous-game-build', status: 'running', phase: 'verification', updatedAt: 4000, }), ); harness.emitAgentRuntime(playableRevision); await Promise.resolve(); }); expect( await screen.findByLabelText('游戏运行', {}, { timeout: 3000 }), ).not.toBeNull(); expect(startAttempts).toBe(2); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(2); }); it('adopts the accepted game-chat run when start initially returns the previous idle state', async () => { const projectPath = '/tmp/game-chat-accepted-run'; const harness = createProjectSupervisorRuntimeHarness({ projectPath }); const completedManifest = createGameCreationAppManifest( 'game-chat-accepted-run', 'game-chat-accepted-run', ); completedManifest.tasks = completedManifest.tasks.map((task) => task.id === 'code-prototype' ? { ...task, status: 'completed' as const } : task, ); const previousIdleRuntime = harness.runtimeState({ runId: 'game-chat-previous-idle', status: 'idle', phase: 'idle', updatedAt: 1000, }); let acceptedRunId = ''; let postStartRuntimeReads = 0; let allowCompletion = false; let previewStarted = false; const acceptedRunEvent = gameChatRuntimeEvent({ runId: 'placeholder', summary: '新受理 Run 正在执行', updatedAt: 4000, }); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: 'game-chat-accepted-run', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'get_local_game_preview_status') { return previewStarted ? { status: 'running', url: 'http://127.0.0.1:4312', port: 4312, root: `${projectPath}/game`, } : { status: 'stopped', url: null, port: null, root: null, }; } if (command === 'get_local_game_manifest') { return completedManifest; } if (command === 'start_local_game_preview') { expect(args).toEqual({ projectPath, expectedRevision: 4 }); previewStarted = true; return { url: 'http://127.0.0.1:4312', port: 4312, root: `${projectPath}/game`, }; } if (command === 'start_game_creator_supervisor_runtime_task') { acceptedRunId = String(args?.runId ?? ''); return { ...harness.runtimeResult(previousIdleRuntime), acceptedRunId, }; } if (command === 'read_game_creator_agent_runtime' && acceptedRunId) { postStartRuntimeReads += 1; const state = harness.runtimeState({ runId: acceptedRunId, runProfile: 'autonomous-game-build', status: allowCompletion ? 'completed' : 'running', phase: allowCompletion ? 'completed' : 'execution', currentTask: '生成 accepted run 首版游戏', currentGoal: '完成 accepted run 可玩原型', currentAction: allowCompletion ? '完成交付' : '执行当前计划', waitingOn: allowCompletion ? '' : '专业任务收束', nextStep: allowCompletion ? '' : '继续执行', lastResponse: allowCompletion ? 'accepted run 已完成' : null, updatedAt: allowCompletion ? 5000 : 4000, }); return { ...harness.runtimeResult(state), recentEvents: [ { ...acceptedRunEvent, runId: acceptedRunId, status: allowCompletion ? 'completed' : 'running', phase: allowCompletion ? 'completed' : 'execution', summary: allowCompletion ? '新受理 Run 已完成' : acceptedRunEvent.summary, updatedAt: allowCompletion ? 5000 : 4000, }, ], }; } return harness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: harness.listen }, }; render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, gameChatOnly: true, }), ); const surface = await screen.findByLabelText('游戏创作聊天'); const composer = within(surface).getByLabelText('项目总控对话内容'); await waitFor(() => { expect((composer as HTMLTextAreaElement).disabled).toBe(false); }); fireEvent.change(composer, { target: { value: '生成并接管真实受理 Run' }, }); fireEvent.click(within(surface).getByRole('button', { name: '发送' })); await waitFor(() => { expect(acceptedRunId).toMatch(/^project-supervisor-task-/); expect(postStartRuntimeReads).toBeGreaterThan(0); expect(screen.getByLabelText('最新状态').textContent).toContain( '生成 accepted run 首版游戏', ); }); expect(screen.queryByText('等待新的运行事件')).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '运行详情' })); await waitFor( () => { expect( within(screen.getByLabelText('最近运行活动')).getByText( /新受理 Run 正在执行/, ), ).not.toBeNull(); }, { timeout: 2500 }, ); harness.setProjectRevision(4); await act(async () => { harness.emitAgentRuntime( gameChatPreviewPlaytestRuntime({ parentRunId: acceptedRunId, revision: 4, updatedAt: 4100, }), ); await Promise.resolve(); }); expect(await screen.findByLabelText('游戏运行')).not.toBeNull(); await waitFor(() => { expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(1); }); const startCallIndex = invoke.mock.calls.findIndex( ([command]) => command === 'start_game_creator_supervisor_runtime_task', ); const acceptedReadCallIndex = invoke.mock.calls.findIndex( ([command], index) => index > startCallIndex && command === 'read_game_creator_agent_runtime', ); const manifestCallIndex = invoke.mock.calls.findIndex( ([command], index) => index > startCallIndex && command === 'get_local_game_manifest', ); expect(acceptedReadCallIndex).toBeGreaterThan(startCallIndex); expect(manifestCallIndex).toBeGreaterThan(acceptedReadCallIndex); allowCompletion = true; await waitFor( () => { expect(screen.getByLabelText('最新状态').textContent).toContain( '本轮已完成', ); expect(screen.getByLabelText('最近运行活动').textContent).toContain( '新受理 Run 已完成', ); }, { timeout: 2500 }, ); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(1); }); it('honors an explicit preview.start deny after a playable revision is verified', async () => { const projectPath = '/tmp/game-chat-preview-denied'; const harness = createProjectSupervisorRuntimeHarness({ projectPath }); const completedManifest = createGameCreationAppManifest( 'game-chat-preview-denied', 'game-chat-preview-denied', ); completedManifest.tasks = completedManifest.tasks.map((task) => task.id === 'code-prototype' ? { ...task, status: 'completed' as const } : task, ); let acceptedRunId = ''; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: 'game-chat-preview-denied', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'get_local_game_preview_status') { return { status: 'stopped', url: null, port: null, root: null, }; } if (command === 'get_local_game_manifest') { return completedManifest; } if (command === 'read_project_permission_policy') { return { path: '.agent/policy.json', policy: { deniedCommands: ['preview.start'], confirmCommands: [], }, }; } if (command === 'start_game_creator_supervisor_runtime_task') { acceptedRunId = String(args?.runId ?? ''); } return harness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: harness.listen }, }; render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, gameChatOnly: true, }), ); const surface = await screen.findByLabelText('游戏创作聊天'); const composer = within(surface).getByLabelText('项目总控对话内容'); await waitFor(() => { expect((composer as HTMLTextAreaElement).disabled).toBe(false); }); fireEvent.change(composer, { target: { value: '生成但不要自动启动' } }); fireEvent.click(within(surface).getByRole('button', { name: '发送' })); await waitFor(() => { expect(acceptedRunId).toMatch(/^project-supervisor-task-/); }); harness.setProjectRevision(5); const deniedPlayableRevision = gameChatPreviewPlaytestRuntime({ parentRunId: acceptedRunId, revision: 5, updatedAt: 3900, }); await act(async () => { harness.emitRuntime( harness.runtimeState({ runId: acceptedRunId, runProfile: 'autonomous-game-build', status: 'running', phase: 'verification', updatedAt: 4000, }), ); harness.emitAgentRuntime(deniedPlayableRevision); await Promise.resolve(); }); await waitFor( () => { expect(within(surface).getByLabelText('最新状态').textContent).toContain( '项目权限策略拒绝执行:preview.start', ); }, { timeout: 2500 }, ); expect( invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', ), ).toHaveLength(0); expect(screen.queryByLabelText('游戏运行')).toBeNull(); }); it('keeps game-chat full-width before preview and switches to desktop 2:1 or mobile vertical layout', () => { const styles = readFileSync( resolve(process.cwd(), 'apps/ai-game-creator-shell/src/styles.css'), 'utf8', ); expect(styles).toMatch( /\.game-chat-shell\s*\{[^}]*grid-template-columns:\s*minmax\(0, 1fr\)/s, ); expect(styles).toMatch( /\.game-chat-shell\.has-preview\s*\{[^}]*grid-template-columns:\s*minmax\(0, 2fr\) minmax\(320px, 1fr\)/s, ); expect(styles).toMatch( /@media \(max-width: 760px\)[\s\S]*?\.game-chat-shell\.has-preview\s*\{[^}]*grid-template-rows:\s*minmax\(220px, 42dvh\) minmax\(0, 1fr\)[^}]*grid-template-columns:\s*minmax\(0, 1fr\)/, ); }); it('loads and continues the active Project Supervisor Session in the standalone chat surface', async () => { const projectPath = '/tmp/supervisor-chat-only-game'; const historyMessage = '已持久化的项目总控历史'; const harness = createProjectSupervisorRuntimeHarness({ projectPath, expectedRunProfile: 'standard', supervisorMessages: [ { schemaVersion: 'game-creator-conversation.v1', role: 'assistant', content: historyMessage, agentId: 'project-supervisor', messageId: 'supervisor-chat-only-history', updatedAt: 2000, }, ], }); window.__TAURI__ = { core: { invoke: harness.invoke }, event: { listen: harness.listen }, }; render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, supervisorChatOnly: true, }), ); const surface = await screen.findByLabelText('项目总控 Agent 纯聊天'); const messageList = within(surface).getByLabelText('项目总控消息'); expect(await within(messageList).findByText(historyMessage)).not.toBeNull(); expect(harness.invoke).toHaveBeenCalledWith( 'list_game_creator_agent_sessions', { projectPath, agentId: 'project-supervisor', }, ); expect(harness.invoke).toHaveBeenCalledWith('read_local_conversation', { projectPath, agentId: 'project-supervisor', sessionId: harness.sessionId, }); expect( within(surface).getByRole('button', { name: '设置' }), ).not.toBeNull(); expect(screen.queryByLabelText('选择 Agent')).toBeNull(); expect(screen.queryByLabelText('项目总控 Agent 状态')).toBeNull(); expect(screen.queryByLabelText('专业 Agent 协作状态')).toBeNull(); fireEvent.change(within(surface).getByLabelText('项目总控对话内容'), { target: { value: '继续完成可玩原型' }, }); fireEvent.click(within(surface).getByRole('button', { name: '发送' })); await waitFor(() => { expect(harness.invoke).toHaveBeenCalledWith( 'start_game_creator_supervisor_runtime_task', { projectPath, sessionId: harness.sessionId, task: '继续完成可玩原型', runId: expect.stringMatching(/^project-supervisor-task-/), runProfile: 'standard', }, ); }); expect(harness.invoke).not.toHaveBeenCalledWith( 'create_game_creator_agent_session', expect.anything(), ); expect(harness.invoke).not.toHaveBeenCalledWith( 'chat_with_game_creator_agent', expect.anything(), ); }); it('keeps an unsent standalone Project Supervisor draft across window navigation reloads', async () => { const projectPath = '/tmp/supervisor-chat-only-draft'; const harness = createProjectSupervisorRuntimeHarness({ projectPath }); window.__TAURI__ = { core: { invoke: harness.invoke }, event: { listen: harness.listen }, }; const renderSupervisorChat = () => render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, supervisorChatOnly: true, }), ); renderSupervisorChat(); const firstSurface = await screen.findByLabelText('项目总控 Agent 纯聊天'); await within(firstSurface).findByLabelText('项目总控消息'); fireEvent.change(within(firstSurface).getByLabelText('项目总控对话内容'), { target: { value: '这条草稿还没有发送' }, }); cleanup(); renderSupervisorChat(); const restoredInput = await screen.findByLabelText('项目总控对话内容'); expect(restoredInput).toHaveProperty('value', '这条草稿还没有发送'); }); it('shows and handles runtime recovery confirmation in the standalone Project Supervisor chat', async () => { const projectPath = '/tmp/supervisor-chat-only-resume'; const harness = createProjectSupervisorRuntimeHarness({ projectPath }); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'resume_game_creator_agent_runtime_tasks') { throw new Error('项目权限策略要求用户确认:agent.resume'); } if (command === 'confirm_resume_game_creator_agent_runtime_tasks') { return []; } return harness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: harness.listen }, }; render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, supervisorChatOnly: true, }), ); const surface = await screen.findByLabelText('项目总控 Agent 纯聊天'); const detail = await within(surface).findByText( `恢复 ${projectPath} 中未完成的 Agent Runtime 任务`, ); expect(within(surface).queryByText(/项目总控 Agent 恢复失败/)).toBeNull(); const confirmation = detail.closest('.pending-command'); expect(confirmation).not.toBeNull(); fireEvent.click( within(confirmation as HTMLElement).getByRole('button', { name: '确认', }), ); await waitFor(() => { expect(invoke).toHaveBeenCalledWith( 'confirm_resume_game_creator_agent_runtime_tasks', { projectPath }, ); expect( within(surface).queryByText( `恢复 ${projectPath} 中未完成的 Agent Runtime 任务`, ), ).toBeNull(); }); }); it('confirms and rejects pending actions in the standalone Project Supervisor chat', async () => { const projectPath = '/tmp/supervisor-chat-only-confirmation'; const runId = 'supervisor-chat-only-confirmation-run'; const harness = createProjectSupervisorRuntimeHarness({ projectPath, initialRuntime: { runId, status: 'waiting-for-confirmation', phase: 'waiting-for-confirmation', pendingToolAction: { actionId: 'standalone-action-confirm', actionFingerprint: 'standalone-fingerprint-confirm', tool: 'file.write', inputSummary: 'game/index.html', reason: null, requestedAt: 3000, }, }, }); harness.setConfirmRuntime( harness.runtimeState({ runId, status: 'waiting-for-confirmation', phase: 'waiting-for-confirmation', pendingToolAction: { actionId: 'standalone-action-reject', actionFingerprint: 'standalone-fingerprint-reject', tool: 'command.exec', inputSummary: 'npm test', reason: null, requestedAt: 4000, }, }), ); harness.setRejectRuntime( harness.runtimeState({ runId, status: 'running', phase: 'planning', pendingToolAction: null, }), ); window.__TAURI__ = { core: { invoke: harness.invoke }, event: { listen: harness.listen }, }; render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, supervisorChatOnly: true, }), ); const surface = await screen.findByLabelText('项目总控 Agent 纯聊天'); let pendingAction = await within(surface).findByLabelText('项目总控 Agent 待确认动作'); expect(within(pendingAction).getByText('file.write')).not.toBeNull(); expect(within(pendingAction).getByText('game/index.html')).not.toBeNull(); fireEvent.click( within(pendingAction).getByRole('button', { name: '确认' }), ); await waitFor(() => { expect(harness.invoke).toHaveBeenCalledWith( 'confirm_game_creator_agent_runtime_task', { projectPath, agentId: 'project-supervisor', runId, actionId: 'standalone-action-confirm', note: '用户已确认待执行工具动作', }, ); }); pendingAction = await within(surface).findByLabelText('项目总控 Agent 待确认动作'); expect(within(pendingAction).getByText('command.exec')).not.toBeNull(); expect(within(pendingAction).getByText('npm test')).not.toBeNull(); fireEvent.click( within(pendingAction).getByRole('button', { name: '拒绝' }), ); await waitFor(() => { expect(harness.invoke).toHaveBeenCalledWith( 'reject_game_creator_agent_runtime_task', { projectPath, agentId: 'project-supervisor', runId, actionId: 'standalone-action-reject', note: '用户已拒绝待执行工具动作', }, ); }); }); it('answers structured questions in the standalone Project Supervisor chat', async () => { const projectPath = '/tmp/supervisor-chat-only-user-input'; const sessionId = 'supervisor-chat-only-user-input-session'; const runId = 'supervisor-chat-only-user-input-run'; const request = agentRuntimeUserInputRequest({ agentId: 'project-supervisor', sessionId, runId, }); const harness = createProjectSupervisorRuntimeHarness({ projectPath, sessionId, initialRuntime: { runId, status: 'waiting-for-user-input', phase: 'waiting-for-user-input', currentTask: '准备首版角色规范图', currentAction: '等待用户补充关键信息', waitingOn: '你的澄清回答', nextStep: '提交全部回答后继续同一 Run', userInputRequest: request, updatedAt: 6000, }, }); window.__TAURI__ = { core: { invoke: harness.invoke }, event: { listen: harness.listen }, }; render( React.createElement(App, { initialProjectPath: projectPath, projectSupervisorOnly: true, supervisorChatOnly: true, }), ); const surface = await screen.findByLabelText('项目总控 Agent 纯聊天'); const card = await within(surface).findByLabelText('Needs input'); expect(within(card).getByText('1. 美术方向')).not.toBeNull(); expect( within(card).getByText('首版角色规范图采用哪种美术方向?'), ).not.toBeNull(); expect(within(card).getByText('优先验证轮廓与动作可读性。')).not.toBeNull(); expect( ( within(surface).getByLabelText( '项目总控对话内容', ) as HTMLTextAreaElement ).disabled, ).toBe(true); fireEvent.click(within(card).getByRole('button', { name: /像素风/ })); fireEvent.click(within(card).getByRole('button', { name: '提交回答' })); await waitFor(() => { expect(harness.invoke).toHaveBeenCalledWith( 'answer_game_creator_agent_runtime_user_input', { projectPath, agentId: 'project-supervisor', runId, actionId: request.actionId, requestId: request.requestId, responseId: expect.stringMatching(/^app-user-input-/), answers: { visual_direction: '像素风' }, }, ); }); await waitFor(() => { expect(within(surface).queryByLabelText('Needs input')).toBeNull(); }); }); it('opens an existing project into the active Supervisor Session, restores history, then starts and steers the same run', async () => { const projectPath = '/tmp/launcher-supervisor-game'; const manifest = createGameCreationAppManifest( 'local-project-draft', 'launcher-supervisor-game', ); const supervisorHarness = createProjectSupervisorRuntimeHarness({ projectPath, supervisorMessages: [ { schemaVersion: 'game-creator-conversation.v1', role: 'assistant', content: '已恢复的项目总控历史', agentId: 'project-supervisor', messageId: 'launcher-supervisor-history', updatedAt: 2000, }, ], }); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: 'launcher-supervisor-game', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'get_local_game_manifest') { return manifest; } return supervisorHarness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: supervisorHarness.listen }, }; renderLauncherProjectsAt('/?launcher'); fireEvent.change(screen.getByLabelText('项目目录'), { target: { value: projectPath }, }); fireEvent.click(screen.getByRole('button', { name: '打开' })); const supervisorSurface = await screen.findByLabelText('项目总控对话'); expect( await within(supervisorSurface).findByText('已恢复的项目总控历史'), ).not.toBeNull(); expect(invoke).toHaveBeenCalledWith('list_game_creator_agent_sessions', { projectPath, agentId: 'project-supervisor', }); expect(invoke).toHaveBeenCalledWith('read_local_conversation', { projectPath, agentId: 'project-supervisor', sessionId: supervisorHarness.sessionId, }); expect(screen.getByLabelText('专业 Agent 协作状态')).not.toBeNull(); expect(screen.queryByLabelText('选择 Agent')).toBeNull(); expect(screen.queryByRole('dialog', { name: 'Agent 对话' })).toBeNull(); expect(supervisorHarness.listen).not.toHaveBeenCalledWith( 'game-creator-agent-progress', expect.any(Function), ); const policyReadCountBeforeChat = invoke.mock.calls.filter( ([command]) => command === 'read_project_permission_policy', ).length; fireEvent.change(screen.getByLabelText('项目需求'), { target: { value: '先完成正式客户端玩法拆解' }, }); fireEvent.click( within(supervisorSurface).getByRole('button', { name: '发送' }), ); await waitFor(() => { expect(invoke).toHaveBeenCalledWith( 'start_game_creator_supervisor_runtime_task', { projectPath, sessionId: supervisorHarness.sessionId, task: '先完成正式客户端玩法拆解', runId: expect.stringMatching(/^project-supervisor-task-/), runProfile: 'autonomous-game-build', }, ); }); const startCall = invoke.mock.calls.find( ([command]) => command === 'start_game_creator_supervisor_runtime_task', ); const runId = String(startCall?.[1]?.runId ?? ''); await waitFor(() => { expect( ( within(supervisorSurface).getByRole('button', { name: '发送', }) as HTMLButtonElement ).disabled, ).toBe(false); }); fireEvent.change(screen.getByLabelText('项目需求'), { target: { value: '补充:优先复用现有素材' }, }); fireEvent.click( within(supervisorSurface).getByRole('button', { name: '发送' }), ); await waitFor(() => { expect(invoke).toHaveBeenCalledWith( 'steer_game_creator_agent_runtime_task', { projectPath, agentId: 'project-supervisor', sessionId: supervisorHarness.sessionId, runId, steerId: expect.stringMatching(/^project-supervisor-steer-/), instruction: '补充:优先复用现有素材', runProfile: 'autonomous-game-build', }, ); }); expect( invoke.mock.calls.filter( ([command]) => command === 'start_game_creator_supervisor_runtime_task', ), ).toHaveLength(1); expect( invoke.mock.calls.filter( ([command]) => command === 'read_project_permission_policy', ), ).toHaveLength(policyReadCountBeforeChat); }); it('keeps the workbench professional Agent status truthful through polling without Tauri events', async () => { const projectPath = '/tmp/launcher-runtime-status-game'; const supervisorRunId = 'supervisor-runtime-status-run'; const manifest = createGameCreationAppManifest( 'local-project-draft', 'launcher-runtime-status-game', ); expect(manifest.tasks.every((task) => task.status === 'pending')).toBe( true, ); let exposeProfessionalRuntimes = false; let failProfessionalRuntimeReads = false; let failProfessionalConversationReads = false; let designConversationMode: 'initial' | 'updated' = 'initial'; let failProfessionalAction = false; let failProfessionalRetry = true; let professionalRuntimeReadCount = 0; let professionalConversationReadCount = 0; let releaseProfessionalRetry: (() => void) | null = null; let professionalRuntimes: Array> = []; const supervisorHarness = createProjectSupervisorRuntimeHarness({ projectPath, initialRuntime: { runId: supervisorRunId, status: 'running', phase: 'waiting-for-delegate-receipts', currentTask: '协调专业 Agent 完成首版原型', currentAction: '等待专业 Agent 返回真实 Runtime 状态', waitingOn: '专业 Agent 回执', updatedAt: 5000, }, runtimeMapLoader: async () => { professionalRuntimeReadCount += 1; if (failProfessionalRuntimeReads) { throw new Error('模拟专业 Runtime 瞬时读取失败'); } return exposeProfessionalRuntimes ? professionalRuntimes : []; }, }); professionalRuntimes = [ supervisorHarness.runtimeState({ agentId: 'design-director', taskId: 'design-director', sessionId: 'design-runtime-status-session', runId: 'design-runtime-status-run', source: 'agent-delegate', parentAgentId: 'project-supervisor', parentRunId: supervisorRunId, delegationId: 'design-runtime-status-delegation', status: 'failed', phase: 'failed', currentTask: '拆解首版玩法', currentAction: '策划 Runtime 已失败', error: 'agentLlm.design-director 后台 Agent 工具计划调用 LLM 失败:kind=transport fingerprint=internal-error-fingerprint chars=545', updatedAt: 6100, }), supervisorHarness.runtimeState({ agentId: 'art-director', taskId: 'art-director', sessionId: 'art-runtime-status-session', runId: 'art-runtime-status-run', source: 'agent-delegate', parentAgentId: 'project-supervisor', parentRunId: supervisorRunId, delegationId: 'art-runtime-status-delegation', status: 'waiting-for-confirmation', phase: 'waiting-for-confirmation', currentTask: '写入美术计划', currentAction: '等待确认写入美术计划', waitingOn: '用户确认 file.write', pendingToolAction: { actionId: 'art-file-write-action', actionFingerprint: 'art-file-write-fingerprint', tool: 'file.write', inputSummary: 'path=assets/art-plan.md · contentChars=7325', reason: null, requestedAt: 6200, }, updatedAt: 6200, }), supervisorHarness.runtimeState({ agentId: 'code-director', taskId: 'code-director', sessionId: 'code-runtime-status-session', runId: 'code-runtime-status-run', source: 'agent-delegate-retry', parentAgentId: 'project-supervisor', parentRunId: supervisorRunId, delegationId: 'code-runtime-status-delegation', status: 'completed', phase: 'completed', currentTask: '完成程序实现拆解', currentAction: '程序方案已完成', updatedAt: 6300, }), supervisorHarness.runtimeState({ agentId: 'art-polish', taskId: 'art-polish', sessionId: 'stale-art-session', runId: 'stale-art-run', source: 'agent-delegate', parentAgentId: 'project-supervisor', parentRunId: 'stale-supervisor-run', delegationId: 'stale-art-delegation', status: 'running', phase: 'action', currentTask: '旧父 Run 的美术任务不得显示', currentAction: '旧父 Run 的状态不得显示', updatedAt: 9999, }), ]; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: 'launcher-runtime-status-game', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'get_local_game_manifest') { return manifest; } if ( command === 'read_local_conversation' && args?.agentId === 'code-director' ) { professionalConversationReadCount += 1; if (failProfessionalConversationReads) { throw new Error('模拟专业成果持久对话瞬时读取失败'); } return { path: `${projectPath}/.agent/conversations/agents/code-director.jsonl`, agentId: 'code-director', sessionId: 'code-runtime-status-session', messages: [ { schemaVersion: 'game-creator-conversation.v1', role: 'assistant', content: '已完成程序实现拆解,并给出可执行的模块边界。', agentId: 'code-director', messageId: 'agent-finalization-11111111111111111111111111111111', updatedAt: 6300, }, { schemaVersion: 'game-creator-conversation.v1', role: 'assistant', content: '新一轮程序任务失败,这不是可交付成果。', agentId: 'code-director', messageId: null, updatedAt: 6400, }, ], }; } if ( command === 'read_local_conversation' && args?.agentId === 'design-director' ) { professionalConversationReadCount += 1; if (failProfessionalConversationReads) { throw new Error('模拟专业成果持久对话瞬时读取失败'); } const designFinalization = designConversationMode === 'updated' ? { content: '更新后的策划历史成果,替换同 Agent 的旧回执。', messageId: 'agent-finalization-33333333333333333333333333333333', updatedAt: 7200, } : { content: '上一轮已经完成并验收的策划成果。', messageId: 'agent-finalization-22222222222222222222222222222222', updatedAt: 5900, }; return { path: `${projectPath}/.agent/conversations/agents/design-director.jsonl`, agentId: 'design-director', sessionId: 'design-runtime-status-session', messages: [ { schemaVersion: 'game-creator-conversation.v1', role: 'assistant', content: designFinalization.content, agentId: 'design-director', messageId: designFinalization.messageId, updatedAt: designFinalization.updatedAt, }, { schemaVersion: 'game-creator-conversation.v1', role: 'assistant', content: '当前新一轮策划失败,不得覆盖旧成果。', agentId: 'design-director', messageId: null, updatedAt: 6100, }, ], }; } if ( command === 'read_local_conversation' && args?.agentId === 'art-asset-plan' ) { professionalConversationReadCount += 1; if (failProfessionalConversationReads) { throw new Error('模拟专业成果持久对话瞬时读取失败'); } return { path: `${projectPath}/.agent/conversations/agents/art-asset-plan.jsonl`, agentId: 'art-asset-plan', sessionId: 'art-history-session', messages: [ { schemaVersion: 'game-creator-conversation.v1', role: 'assistant', content: '历史美术资源计划回执,尚不等于图片资产。', agentId: 'art-asset-plan', messageId: 'agent-finalization-44444444444444444444444444444444', updatedAt: 5800, }, ], }; } if ( command === 'read_local_conversation' && args?.agentId === 'balance-seed' ) { professionalConversationReadCount += 1; if (failProfessionalConversationReads) { throw new Error('模拟专业成果持久对话瞬时读取失败'); } return { path: `${projectPath}/.agent/conversations/agents/balance-seed.jsonl`, agentId: 'balance-seed', sessionId: 'balance-history-session', messages: [ { schemaVersion: 'game-creator-conversation.v1', role: 'assistant', content: '历史数值方案回执。', agentId: 'balance-seed', messageId: 'agent-finalization-55555555555555555555555555555555', updatedAt: 5700, }, ], }; } if (command === 'confirm_retry_game_creator_agent_runtime_task') { if (failProfessionalRetry) { throw new Error( 'agentLlm.design-director retry失败:kind=transport fingerprint=private-retry-fingerprint chars=256', ); } await new Promise((resolve) => { releaseProfessionalRetry = resolve; }); return supervisorHarness.runtimeResult( supervisorHarness.runtimeState({ agentId: String(args?.agentId ?? ''), taskId: String(args?.agentId ?? ''), sessionId: 'design-runtime-status-session', runId: String(args?.nextRunId ?? ''), source: 'agent-delegate-retry', parentAgentId: 'project-supervisor', parentRunId: supervisorRunId, status: 'running', phase: 'planning', currentTask: '在当前项目重试策划任务', currentAction: '重新生成 Agent 工具计划', updatedAt: 7000, }), ); } if ( failProfessionalAction && command === 'confirm_game_creator_agent_runtime_task' ) { throw new Error( 'agentLlm.art-director 工具确认失败:kind=transport fingerprint=private-action-fingerprint chars=384', ); } return supervisorHarness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke } }; renderLauncherProjectsAt('/?launcher'); fireEvent.change(screen.getByLabelText('项目目录'), { target: { value: projectPath }, }); fireEvent.click(screen.getByRole('button', { name: '打开' })); expect(await screen.findByLabelText('项目总控对话')).not.toBeNull(); await waitFor(() => { expect(professionalRuntimeReadCount).toBeGreaterThan(0); }); expect(screen.queryByLabelText('专业 Agent 实时状态')).toBeNull(); const supervisorStatusPanel = screen.getByLabelText('项目总控 Agent 状态'); supervisorStatusPanel.scrollTop = 120; const readCountBeforeExpose = professionalRuntimeReadCount; exposeProfessionalRuntimes = true; const professionalList = await screen.findByLabelText( '专业 Agent 实时状态', {}, { timeout: 2500 }, ); expect(professionalRuntimeReadCount).toBeGreaterThan(readCountBeforeExpose); expect( within(professionalList).getByText('策划 Agent 服务连接失败,请稍后重试'), ).not.toBeNull(); expect(professionalList.textContent).not.toContain('fingerprint'); expect(professionalList.textContent).not.toContain('chars=545'); expect(professionalList.textContent).not.toContain('contentChars'); expect(supervisorStatusPanel.scrollTop).toBe(0); expect( within(professionalList).getByText('等待确认后继续工作'), ).not.toBeNull(); expect(within(professionalList).getByText('本轮工作已完成')).not.toBeNull(); expect( within(professionalList).queryByText('旧父 Run 的状态不得显示'), ).toBeNull(); await waitFor(() => { expect(invoke).toHaveBeenCalledWith('read_local_conversation', { projectPath, agentId: 'code-director', }); }); const codeAgentCard = within(professionalList) .getByText('程序 Agent') .closest('article')!; const resultButton = await within(codeAgentCard).findByRole('button', { name: '查看成果', }); fireEvent.click(resultButton); const resultDialog = await screen.findByRole('dialog', { name: '程序 Agent 文本回执', }); expect(resultDialog.textContent).toContain( '已完成程序实现拆解,并给出可执行的模块边界。', ); fireEvent.click(within(resultDialog).getByRole('button', { name: '关闭' })); const documentResources = screen.getByLabelText('文档'); expect( within(documentResources).getByText('程序 Agent 文本回执'), ).not.toBeNull(); expect( within(documentResources).getByText('策划 Agent 文本回执'), ).not.toBeNull(); expect( within(documentResources).getByText('美术资源计划 Agent 文本回执'), ).not.toBeNull(); expect( within(documentResources).getByText('数值 Agent 文本回执'), ).not.toBeNull(); expect(documentResources.textContent).not.toContain( '当前新一轮策划失败,不得覆盖旧成果。', ); expect( within(documentResources).getByText('历史成果 · 策划 Agent'), ).not.toBeNull(); failProfessionalConversationReads = true; const conversationReadCountBeforeFailure = professionalConversationReadCount; professionalRuntimes = professionalRuntimes.map((runtime) => runtime.agentId === 'design-director' ? { ...runtime, updatedAt: 7100 } : runtime, ); await waitFor( () => { expect(professionalConversationReadCount).toBeGreaterThan( conversationReadCountBeforeFailure, ); expect( within(screen.getByLabelText('文档')).getByText( '策划 Agent 文本回执', ), ).not.toBeNull(); expect( within(screen.getByLabelText('文档')).getByText( '程序 Agent 文本回执', ), ).not.toBeNull(); }, { timeout: 2500 }, ); failProfessionalConversationReads = false; designConversationMode = 'updated'; const conversationReadCountBeforeUpdate = professionalConversationReadCount; professionalRuntimes = professionalRuntimes.map((runtime) => runtime.agentId === 'design-director' ? { ...runtime, updatedAt: 7200 } : runtime, ); await waitFor( () => { expect(professionalConversationReadCount).toBeGreaterThan( conversationReadCountBeforeUpdate, ); }, { timeout: 2500 }, ); const updatedDesignCard = await waitFor(() => { const card = within(screen.getByLabelText('文档')).getByRole('button', { name: /^策划 Agent 文本回执/u, }); expect(card.getAttribute('data-resource-id')).toContain( 'agent-finalization-33333333333333333333333333333333', ); return card; }); fireEvent.click(updatedDesignCard); const updatedDesignDialog = await screen.findByRole('region', { name: '策划 Agent 文本回执', }); expect( within(updatedDesignDialog).getByText( '更新后的策划历史成果,替换同 Agent 的旧回执。', ), ).not.toBeNull(); fireEvent.click( within(updatedDesignDialog).getByRole('button', { name: '收起资源' }), ); const dock = screen.getByLabelText('子 Agent 状态栏'); await waitFor(() => { expect( within( within(dock).getByRole('article', { name: /策划 Agent/ }), ).getByText('失败'), ).not.toBeNull(); expect( within( within(dock).getByRole('article', { name: /美术 Agent/ }), ).getByText('待确认'), ).not.toBeNull(); expect( within( within(dock).getByRole('article', { name: /程序 Agent/ }), ).getByText('已完成'), ).not.toBeNull(); }); const artPendingAction = within(professionalList).getByLabelText('美术 Agent待确认动作'); failProfessionalAction = true; fireEvent.click( within(artPendingAction).getByRole('button', { name: '确认' }), ); expect( await screen.findByText('专业 Agent 服务连接失败,请稍后重试'), ).not.toBeNull(); expect(screen.queryByText(/private-action-fingerprint/)).toBeNull(); failProfessionalAction = false; fireEvent.click( within(artPendingAction).getByRole('button', { name: '确认' }), ); await waitFor(() => { expect(invoke).toHaveBeenCalledWith( 'confirm_game_creator_agent_runtime_task', { projectPath, agentId: 'art-director', runId: 'art-runtime-status-run', actionId: 'art-file-write-action', note: '用户已确认专业 Agent 待执行工具动作', }, ); }); failProfessionalRuntimeReads = true; const readCountBeforeFailure = professionalRuntimeReadCount; await waitFor( () => { expect(professionalRuntimeReadCount).toBeGreaterThan( readCountBeforeFailure, ); }, { timeout: 2500 }, ); expect( within( within(dock).getByRole('article', { name: /策划 Agent/ }), ).getByText('失败'), ).not.toBeNull(); expect( within( within(dock).getByRole('article', { name: /美术 Agent/ }), ).getByText('待确认'), ).not.toBeNull(); expect( within( within(dock).getByRole('article', { name: /程序 Agent/ }), ).getByText('已完成'), ).not.toBeNull(); let retryButton = within(professionalList).getByRole('button', { name: '在当前项目重试', }); fireEvent.click(retryButton); expect( await within(retryButton.closest('article')!).findByText( '策划 Agent 服务连接失败,请稍后重试', ), ).not.toBeNull(); expect(retryButton.closest('article')?.textContent).not.toContain( 'private-retry-fingerprint', ); failProfessionalRetry = false; retryButton = within(professionalList).getByRole('button', { name: '在当前项目重试', }); fireEvent.click(retryButton); const retryingButton = within(professionalList).getByRole('button', { name: '正在提交重试…', }); expect(retryingButton.hasAttribute('disabled')).toBe(true); expect( within(retryingButton.closest('article')!).getByRole('status') .textContent, ).toBe('正在提交重试…'); await waitFor(() => { expect(invoke).toHaveBeenCalledWith( 'confirm_retry_game_creator_agent_runtime_task', { projectPath, agentId: 'design-director', runId: 'design-runtime-status-run', nextRunId: expect.stringMatching(/^project-professional-retry-/), }, ); }); releaseProfessionalRetry?.(); await within(professionalList).findByText( '重试请求已受理,正在同步新一轮状态', ); failProfessionalRuntimeReads = false; exposeProfessionalRuntimes = false; const readCountBeforeAuthoritativeEmpty = professionalRuntimeReadCount; await waitFor( () => { expect(professionalRuntimeReadCount).toBeGreaterThan( readCountBeforeAuthoritativeEmpty, ); expect(screen.queryByLabelText('专业 Agent 实时状态')).toBeNull(); expect( within( within(dock).getByRole('article', { name: /策划 Agent/ }), ).getByText('等待中'), ).not.toBeNull(); expect( within(screen.getByLabelText('文档')).getByText( '策划 Agent 文本回执', ), ).not.toBeNull(); }, { timeout: 2500 }, ); }, 10_000); it('does not render a ready Supervisor stream twice when its assistant is already in restored history', async () => { const projectPath = '/tmp/launcher-supervisor-finalizing'; const reply = '已经落盘的唯一项目总控回复'; const manifest = createGameCreationAppManifest( 'local-project-draft', 'launcher-supervisor-finalizing', ); const runId = 'supervisor-finalizing-run'; const supervisorHarness = createProjectSupervisorRuntimeHarness({ projectPath, supervisorMessages: [ { schemaVersion: 'game-creator-conversation.v1', role: 'assistant', content: reply, agentId: 'project-supervisor', messageId: 'launcher-supervisor-final-assistant', updatedAt: 7000, }, ], initialRuntime: { runId, status: 'running', phase: 'finalizing', loopIteration: 1, appliedSteerCursor: 0, updatedAt: 7000, }, initialResponseStream: projectSupervisorResponseStream({ runId, sequence: 8, accumulatedText: reply, status: 'ready', }), }); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'inspect_local_project_directory') { return { projectPath, exists: true, isDirectory: true, isGameCreatorProject: true, projectName: 'launcher-supervisor-finalizing', recentRunStatus: null, recentRunStopReason: null, }; } if (command === 'get_local_game_manifest') { return manifest; } return supervisorHarness.invoke(command, args); }, ); window.__TAURI__ = { core: { invoke }, event: { listen: supervisorHarness.listen }, }; renderLauncherProjectsAt('/?launcher'); fireEvent.change(screen.getByLabelText('项目目录'), { target: { value: projectPath }, }); fireEvent.click(screen.getByRole('button', { name: '打开' })); const supervisorSurface = await screen.findByLabelText('项目总控对话'); expect(await within(supervisorSurface).findByText(reply)).not.toBeNull(); expect(within(supervisorSurface).getAllByText(reply)).toHaveLength(1); expect( within(supervisorSurface).queryByLabelText('项目总控 Agent 实时回复'), ).toBeNull(); }); it('rejects launcher project paths with control characters before Tauri calls', () => { const invoke = vi.fn(async () => undefined); window.__TAURI__ = { core: { invoke } }; renderLauncherProjectsAt('/?launcher'); fireEvent.change(screen.getByLabelText('项目目录'), { target: { value: '/tmp/bad\u0007path' }, }); fireEvent.click(screen.getAllByRole('button', { name: '新建项目' })[0]); expect(screen.getByText('项目目录不能包含控制字符')).not.toBeNull(); expect(invoke).not.toHaveBeenCalled(); }); } export function registerProjectWorkbenchNavigationTests() { it('keeps project switching inside the single-window client flow', async () => { const invoke = vi.fn(async () => undefined); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?main'); fireEvent.click(screen.getByRole('button', { name: '切换项目' })); expect(screen.getByText('请回到首页的项目组切换项目。')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'open_game_creator_launcher_window', ); submitChat('/switch-project'); expect( screen.getAllByText('请回到首页的项目组切换项目。').length, ).toBeGreaterThanOrEqual(2); expect(invoke).not.toHaveBeenCalledWith( 'open_game_creator_launcher_window', ); }); it('opens the current project directory from the main project window', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'append_local_permission_log') { return {}; } if (command === 'init_local_game_project') { const projectPath = String(args?.projectPath ?? ''); return { projectPath, manifestPath: `${projectPath}/.agent/manifest.json`, manifest, }; } if (command === 'read_project_permission_policy') { return emptyProjectPolicy(); } if (command === 'chat_with_game_creator_agent') { return { replyText: `主聊天回复:${String(args?.prompt ?? '')}`, }; } if (command === 'read_local_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: null, messages: [], }; } if (command === 'chat_with_game_creator_role_agent') { return { replyText: mockRoleAgentReply(), }; } if (command === 'append_local_conversation_message') { return { path: '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: null, messages: [], }; } if (command === 'open_local_project_directory') { return undefined; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('已打开:/tmp/authorized-game'); const revealButtons = screen.getAllByRole('button', { name: '显示目录' }); fireEvent.click(revealButtons[revealButtons.length - 1]); await waitFor(() => { expect(invoke).toHaveBeenCalledWith('open_local_project_directory', { projectPath: '/tmp/authorized-game', }); }); expect(await screen.findByText('已打开项目目录。')).not.toBeNull(); submitChat('/open-project'); await waitFor(() => { expect( invoke.mock.calls.filter( ([command]) => command === 'open_local_project_directory', ), ).toHaveLength(2); }); expect( screen.getAllByText('已打开项目目录。').length, ).toBeGreaterThanOrEqual(2); }); it('fills an asset registration draft from recent project files without registering immediately', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'append_local_permission_log') { return {}; } if (command === 'read_project_permission_policy') { return emptyProjectPolicy(); } if (command === 'init_local_game_project') { const projectPath = String(args?.projectPath ?? ''); return { projectPath, manifestPath: `${projectPath}/.agent/manifest.json`, manifest, }; } if (command === 'read_local_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: null, messages: [], }; } if (command === 'read_local_project_file') { throw new Error('missing trace'); } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [ { path: 'assets/uploads/hero.png', kind: 'file', size: 4, modifiedAt: 1700000001, }, ], }; } if (command === 'register_local_asset') { throw new Error('should only fill the chat draft'); } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; await act(async () => { renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); }); await screen.findByText('已打开:/tmp/authorized-game'); invoke.mockClear(); fireEvent.click(screen.getByRole('button', { name: '文件' })); expect(await screen.findByText(/本地项目文件:/)).not.toBeNull(); fireEvent.click( within(screen.getByLabelText('最近项目文件')).getByRole('button', { name: '登记资产 assets/uploads/hero.png', }), ); await waitFor(() => expect(document.activeElement).toBe(screen.getByLabelText('创作想法')), ); expect(screen.getByLabelText('创作想法')).toHaveProperty( 'value', '/asset-register assets/uploads/hero.png image image/png', ); expect( screen.queryByText('asset.register · assets/uploads/hero.png'), ).toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'register_local_asset', expect.anything(), ); }); } export function registerProjectAgentStatusTests() { it('shows runtime status and recent tasks in the main agent status list', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const runtimeTask = { schemaVersion: 'game-creator-agent-runtime-task.v1', agentId: 'design-director', taskId: 'design-director', sessionId: 'agent-session-design-director', runId: 'runtime-design-director-1', source: 'agent-background-task', task: '排队补齐世界观拆解', status: 'pending', phase: 'queued', currentAction: '等待当前任务完成', error: null, updatedAt: 10, }; const runtimeState = { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'design-director', taskId: 'design-director', sessionId: 'agent-session-design-director', runId: 'runtime-design-director-1', source: 'agent-background-task', status: 'running', phase: 'planning', currentTask: '拆解关卡节奏', currentGoal: '补齐第一关节奏目标', currentAction: '整理目标和约束', waitingOn: 'Agent 输出计划或回复', loopIteration: 2, maxLoopIterations: 3, toolActionBudget: 3, plan: ['读取项目上下文'], planSteps: [ { index: 0, title: '读取项目上下文', status: 'active', detail: '正在整理目标和约束', updatedAt: 11, }, ], activePlanStepIndex: 0, observations: ['已创建本轮 Agent Runtime run。'], taskQueue: { total: 2, pending: 1, running: 1, completed: 0, failed: 0, latestRunId: 'runtime-design-director-1', updatedAt: 10, }, allowedTools: ['conversation.read'], lastResponse: null, error: null, updatedAt: 10, }; const completedRuntimeState = { ...runtimeState, status: 'idle', phase: 'completed', currentAction: '等待下一轮输入', waitingOn: '开发者下一轮输入', nextStep: '等待下一轮输入', taskQueue: { total: 2, pending: 1, running: 0, completed: 1, failed: 0, latestRunId: 'runtime-design-director-1', updatedAt: 12, }, lastResponse: '已补齐第一关节奏目标。', updatedAt: 12, }; const completedRuntimeTask = { ...runtimeTask, status: 'completed', phase: 'completed', currentAction: '等待下一轮输入', updatedAt: 12, }; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'append_local_permission_log') { return {}; } if (command === 'init_local_game_project') { const projectPath = String(args?.projectPath ?? ''); return { projectPath, manifestPath: `${projectPath}/.agent/manifest.json`, manifest, }; } if (command === 'resume_game_creator_agent_runtime_tasks') { return []; } if (command === 'read_game_creator_agent_runtimes') { return [ { state: runtimeState, sessionPath: '/tmp/authorized-game/.agent/runtime/agents/design-director.json', eventPath: '/tmp/authorized-game/.agent/runtime/events/design-director.jsonl', taskPath: '/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl', taskQueue: runtimeState.taskQueue, recentEvents: [], recentTasks: [runtimeTask], }, ]; } if (command === 'read_local_project_file') { throw new Error( '读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)', ); } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [] }; } throw new Error(`unexpected invoke ${command}`); }, ); let runtimeUpdateHandler: | ((event: { payload: Record }) => void) | null = null; const listen = vi.fn( async ( eventName: string, handler: (event: { payload: Record }) => void, ) => { if (eventName === 'game-creator-agent-progress') { return () => {}; } if (eventName === 'game-creator-agent-runtime-update') { runtimeUpdateHandler = handler; return () => { if (runtimeUpdateHandler === handler) { runtimeUpdateHandler = null; } }; } throw new Error(`unexpected listen ${eventName}`); }, ); window.__TAURI__ = { core: { invoke }, event: { listen } }; renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('想做什么游戏?'); const agentStatusList = screen.getByLabelText('Agent 状态列表'); await waitFor(() => { const designCard = within(agentStatusList).getByRole('button', { name: /拆解创作方向/, }); expect(designCard.textContent).toContain( 'Runtime:running / planning · Loop 2/3 · 整理目标和约束 · 等待 Agent 输出计划或回复 · 下一步 等待 Agent 输出计划或回复 · run runtime-design-director-1', ); expect(designCard.textContent).toContain('当前目标:补齐第一关节奏目标'); expect(designCard.textContent).toContain('当前任务:拆解关卡节奏'); expect(designCard.textContent).toContain( '当前计划步骤:#1 active · 读取项目上下文 · 正在整理目标和约束', ); expect(designCard.textContent).toContain( '任务队列:pending 1 · running 1 · waiting 0 · needsInput 0 · cancelled 0 · completed 0 · failed 0 · total 2 · latest runtime-design-director-1', ); expect(designCard.textContent).toContain( '最近任务:pending / queued · 排队补齐世界观拆解', ); }); await waitFor(() => { expect( invoke.mock.calls.filter( ([command]) => command === 'resume_game_creator_agent_runtime_tasks', ), ).toHaveLength(1); }); await act(async () => { runtimeUpdateHandler?.({ payload: { projectPath: '/tmp/authorized-game', agentId: 'design-director', runId: 'runtime-design-director-1', status: 'idle', phase: 'completed', runtime: { state: completedRuntimeState, sessionPath: '/tmp/authorized-game/.agent/runtime/agents/design-director.json', eventPath: '/tmp/authorized-game/.agent/runtime/events/design-director.jsonl', taskPath: '/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl', taskQueue: completedRuntimeState.taskQueue, recentEvents: [], recentTasks: [completedRuntimeTask], }, }, }); }); await waitFor(() => { const designCard = within(agentStatusList).getByRole('button', { name: /拆解创作方向/, }); expect(designCard.textContent).toContain( 'Runtime:idle / completed · Loop 2/3 · 等待下一轮输入 · 等待 开发者下一轮输入 · 下一步 等待下一轮输入 · run runtime-design-director-1', ); expect(designCard.textContent).toContain( '任务队列:pending 1 · running 0 · waiting 0 · needsInput 0 · cancelled 0 · completed 1 · failed 0 · total 2 · latest runtime-design-director-1', ); expect(designCard.textContent).toContain( '最近任务:completed / completed · 排队补齐世界观拆解', ); }); }); it('lets the developer schedule ready manifest tasks into agent runtimes', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const runtimeTask = { schemaVersion: 'game-creator-agent-runtime-task.v1', agentId: 'design-director', taskId: 'design-director', sessionId: 'agent-session-design-director', runId: 'ready-design-director-1', source: 'agent-ready-task-scheduler', task: '执行 Ready 任务:拆解创作方向', status: 'running', phase: 'planning', currentAction: '准备读取项目上下文', error: null, updatedAt: 20, }; const runtimeState = { schemaVersion: 'game-creator-agent-runtime.v1', agentId: 'design-director', taskId: 'design-director', sessionId: 'agent-session-design-director', runId: 'ready-design-director-1', source: 'agent-ready-task-scheduler', status: 'running', phase: 'planning', currentTask: '执行 Ready 任务:拆解创作方向', currentGoal: '把 ready 任务投递给对应 Agent', currentAction: '准备读取项目上下文', waitingOn: 'Agent 输出计划或回复', loopIteration: 1, maxLoopIterations: 3, toolActionBudget: 3, plan: ['读取项目上下文'], planSteps: [], activePlanStepIndex: null, observations: [], taskQueue: { total: 1, pending: 0, running: 1, completed: 0, failed: 0, latestRunId: 'ready-design-director-1', updatedAt: 20, }, allowedTools: ['conversation.read'], lastResponse: null, error: null, updatedAt: 20, }; const runtimeResult = { state: runtimeState, sessionPath: '/tmp/authorized-game/.agent/runtime/agents/design-director.json', eventPath: '/tmp/authorized-game/.agent/runtime/events/design-director.jsonl', taskPath: '/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl', taskQueue: runtimeState.taskQueue, recentEvents: [], recentTasks: [runtimeTask], }; let scheduled = false; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'append_local_permission_log') { return {}; } if (command === 'init_local_game_project') { const projectPath = String(args?.projectPath ?? ''); return { projectPath, manifestPath: `${projectPath}/.agent/manifest.json`, manifest, }; } if (command === 'read_project_permission_policy') { return emptyProjectPolicy(); } if (command === 'resume_game_creator_agent_runtime_tasks') { return []; } if (command === 'read_game_creator_agent_runtimes') { return scheduled ? [runtimeResult] : []; } if (command === 'schedule_game_creator_agent_ready_tasks') { scheduled = true; return [runtimeResult]; } if (command === 'append_local_conversation_message') { return { path: '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: null, messages: [], }; } if (command === 'get_local_game_manifest') { return manifest; } if (command === 'read_local_project_file') { throw new Error( '读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)', ); } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [] }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('想做什么游戏?'); const scheduleButton = await screen.findByRole('button', { name: '调度 Ready', }); await waitFor(() => { expect((scheduleButton as HTMLButtonElement).disabled).toBe(false); }); fireEvent.click(scheduleButton); await waitFor(() => { expect( invoke.mock.calls.some( ([command, args]) => command === 'schedule_game_creator_agent_ready_tasks' && args?.projectPath === '/tmp/authorized-game' && args?.limit === 16, ), ).toBe(true); }); expect(await screen.findAllByText('已调度 1 个 Ready 任务。')).toHaveLength( 2, ); const agentStatusList = screen.getByLabelText('Agent 状态列表'); await waitFor(() => { const designCard = within(agentStatusList).getByRole('button', { name: /拆解创作方向/, }); expect(designCard.textContent).toContain('Runtime:running / planning'); expect(designCard.textContent).toContain( '当前目标:把 ready 任务投递给对应 Agent', ); expect(designCard.textContent).toContain( '最近任务:running / planning · 执行 Ready 任务:拆解创作方向', ); }); }); it('keeps ready task scheduling out of the normal user window', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'append_local_permission_log') { return {}; } if (command === 'init_local_game_project') { const projectPath = String(args?.projectPath ?? ''); return { projectPath, manifestPath: `${projectPath}/.agent/manifest.json`, manifest, }; } if (command === 'resume_game_creator_agent_runtime_tasks') { return []; } if (command === 'read_game_creator_agent_runtimes') { return []; } if (command === 'read_local_project_file') { throw new Error( '读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)', ); } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [] }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('想做什么游戏?'); expect(screen.queryByRole('button', { name: '调度 Ready' })).toBeNull(); expect( invoke.mock.calls.some( ([command]) => command === 'schedule_game_creator_agent_ready_tasks', ), ).toBe(false); }); it('ignores stale agent conversation reads after switching agents', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); let releaseOldConversation: (() => void) | null = null; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'append_local_permission_log') { return {}; } if (command === 'init_local_game_project') { const projectPath = String(args?.projectPath ?? ''); return { projectPath, manifestPath: `${projectPath}/.agent/manifest.json`, manifest, }; } if (command === 'read_project_permission_policy') { return emptyProjectPolicy(); } if (command === 'resume_game_creator_agent_runtime_tasks') { return []; } if (command === 'read_game_creator_agent_runtimes') { return []; } if (command === 'read_local_conversation') { if (args?.agentId === null) { return { path: '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: null, messages: [], }; } if (args?.agentId === 'design-director') { return await new Promise((resolve) => { releaseOldConversation = () => resolve({ path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: 'design-director', messages: [ { schemaVersion: 'game-creator-conversation.v1', role: 'user', content: '旧 Agent 慢速消息', agentId: 'design-director', updatedAt: 1, }, ], }); }); } return { path: '/tmp/authorized-game/.agent/conversations/agents/art-director.jsonl', agentId: args?.agentId, messages: [ { schemaVersion: 'game-creator-conversation.v1', role: 'user', content: '新 Agent 历史消息', agentId: String(args?.agentId ?? ''), updatedAt: 2, }, ], }; } if (command === 'read_local_agent_memory') { return { taskId: args?.taskId, path: `/tmp/authorized-game/memory/agents/${String( args?.taskId ?? '', )}.md`, content: args?.taskId === 'art-director' ? '新 Agent 私有记忆' : '旧 Agent 私有记忆', exists: true, }; } if (command === 'read_local_project_file') { throw new Error( '读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)', ); } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [] }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('想做什么游戏?'); const agentStatusList = screen.getByLabelText('Agent 状态列表'); const designAgentButton = within(agentStatusList).getByRole('button', { name: /拆解创作方向/, }); await waitFor(() => { expect((designAgentButton as HTMLButtonElement).disabled).toBe(false); }); fireEvent.click(designAgentButton); await waitFor(() => { expect(releaseOldConversation).not.toBeNull(); }); fireEvent.click( within(agentStatusList).getByRole('button', { name: /确定视觉方向/ }), ); expect(await screen.findByText('新 Agent 历史消息')).not.toBeNull(); expect(screen.getByLabelText('Agent 私有记忆').textContent).toContain( '新 Agent 私有记忆', ); await act(async () => { releaseOldConversation?.(); }); expect(screen.queryByText('旧 Agent 慢速消息')).toBeNull(); expect(screen.getByText('新 Agent 历史消息')).not.toBeNull(); expect(screen.getByLabelText('Agent 私有记忆').textContent).not.toContain( '旧 Agent 私有记忆', ); }); it('ignores stale agent conversation saves after switching agents', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); let releaseOldSave: (() => void) | null = null; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'append_local_permission_log') { return {}; } if (command === 'init_local_game_project') { const projectPath = String(args?.projectPath ?? ''); return { projectPath, manifestPath: `${projectPath}/.agent/manifest.json`, manifest, }; } if (command === 'read_project_permission_policy') { return emptyProjectPolicy(); } if (command === 'resume_game_creator_agent_runtime_tasks') { return []; } if (command === 'read_game_creator_agent_runtimes') { return []; } if (command === 'read_local_conversation') { if (args?.agentId === null) { return { path: '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: null, messages: [], }; } if (args?.agentId === 'art-director') { return { path: '/tmp/authorized-game/.agent/conversations/agents/art-director.jsonl', agentId: 'art-director', messages: [ { schemaVersion: 'game-creator-conversation.v1', role: 'user', content: '新 Agent 留存消息', agentId: 'art-director', updatedAt: 2, }, ], }; } return { path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: 'design-director', messages: [], }; } if (command === 'read_local_agent_memory') { return { taskId: args?.taskId, path: `/tmp/authorized-game/memory/agents/${String( args?.taskId ?? '', )}.md`, content: '', exists: false, }; } if (command === 'append_local_conversation_message') { const message = args?.message as { role: 'user' | 'assistant'; content: string; agentId: string | null; }; return await new Promise((resolve) => { releaseOldSave = () => resolve({ path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: 'design-director', messages: [ { schemaVersion: 'game-creator-conversation.v1', role: message.role, content: message.content, agentId: message.agentId, updatedAt: 1, }, ], }); }); } if (command === 'read_local_project_file') { throw new Error( '读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)', ); } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [] }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('想做什么游戏?'); const agentStatusList = screen.getByLabelText('Agent 状态列表'); const designAgentButton = within(agentStatusList).getByRole('button', { name: /拆解创作方向/, }); await waitFor(() => { expect((designAgentButton as HTMLButtonElement).disabled).toBe(false); }); fireEvent.click(designAgentButton); const input = await screen.findByLabelText('Agent 对话内容'); fireEvent.change(input, { target: { value: '旧 Agent 保存回包' } }); fireEvent.submit(input.closest('form') as HTMLFormElement); await screen.findByText('正在保存用户消息'); await waitFor(() => { expect(releaseOldSave).not.toBeNull(); }); fireEvent.click( within(agentStatusList).getByRole('button', { name: /确定视觉方向/ }), ); expect(await screen.findByText('新 Agent 留存消息')).not.toBeNull(); await act(async () => { releaseOldSave?.(); }); expect(screen.getByText('新 Agent 留存消息')).not.toBeNull(); expect(screen.queryByText('旧 Agent 保存回包')).toBeNull(); expect(screen.queryByText(/已保存 1 条/)).toBeNull(); }); it('ignores stale agent reads after closing the agent dialog', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); let releaseConversation: (() => void) | null = null; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'append_local_permission_log') { return {}; } if (command === 'init_local_game_project') { const projectPath = String(args?.projectPath ?? ''); return { projectPath, manifestPath: `${projectPath}/.agent/manifest.json`, manifest, }; } if (command === 'read_project_permission_policy') { return emptyProjectPolicy(); } if (command === 'resume_game_creator_agent_runtime_tasks') { return []; } if (command === 'read_game_creator_agent_runtimes') { return []; } if (command === 'read_local_conversation') { if (args?.agentId === null) { return { path: '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: null, messages: [], }; } return await new Promise((resolve) => { releaseConversation = () => resolve({ path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl', agentId: 'design-director', messages: [ { schemaVersion: 'game-creator-conversation.v1', role: 'user', content: '关闭后不该写入界面状态', agentId: 'design-director', updatedAt: 1, }, ], }); }); } if (command === 'read_local_agent_memory') { return { taskId: args?.taskId, path: '/tmp/authorized-game/memory/agents/design/director.md', content: '关闭后不该读取私有记忆', exists: true, }; } if (command === 'read_local_project_file') { throw new Error( '读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)', ); } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [] }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?dev&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('想做什么游戏?'); await waitFor(() => { expect( ( screen.getByRole('button', { name: '刷新 Agent', }) as HTMLButtonElement ).disabled, ).toBe(false); }); fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ })); await waitFor(() => { expect(releaseConversation).not.toBeNull(); }); fireEvent.click(screen.getByRole('button', { name: '关闭' })); expect(screen.queryByLabelText('Agent 对话')).toBeNull(); await act(async () => { releaseConversation?.(); }); expect(screen.queryByText('关闭后不该写入界面状态')).toBeNull(); expect(screen.queryByText('conversation.read')).toBeNull(); expect(screen.queryByText('memory.agent.read')).toBeNull(); }); it('updates the open agent dialog when agent status is refreshed', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); let runReadCount = 0; const makeTrace = (withAgentStep: boolean) => ({ schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, runId: 'run-open-agent-refresh', commandId: 'game.generate_draft', status: 'running', passes: 1, maxPasses: 3, toolCallCount: 1, maxToolCalls: 128, stopReason: 'running', goal: '做一个厨房弹幕游戏', coordination: 'Planner', steps: withAgentStep ? [ { pass: 1, agent: 'Planner', phase: 'plan', taskId: 'design-director', group: 'design', role: 'Director', status: 'running', inputPaths: ['memory/session.md'], outputPaths: ['.agent/spec.md'], summary: '刷新后的拆解方向', toolCalls: [ { toolId: 'llm.planner.refresh', status: 'ok', inputPaths: ['memory/session.md'], outputPaths: ['.agent/spec.md'], summary: '刷新后工具调用', }, ...Array.from({ length: 5 }, (_, index) => ({ toolId: `llm.extra.${index + 1}`, status: 'ok', inputPaths: [], outputPaths: [], summary: `额外工具调用 ${index + 1}`, })), ], }, ] : [], artifacts: [], taskGraph: { goal: '做一个厨房弹幕游戏', readyTaskIds: [], activeTaskIds: withAgentStep ? ['design-director'] : [], carriedTaskIds: [], repairFocus: [], repairRoutes: [], tasks: createGameCreationAppSeedTasks(), }, passPlans: [], nextStep: 'continue', error: null, updatedAt: runReadCount, }) satisfies GameCreationAgentRunTrace; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'append_local_permission_log') { return {}; } if (command === 'init_local_game_project') { const projectPath = String(args?.projectPath ?? ''); return { projectPath, manifestPath: `${projectPath}/.agent/manifest.json`, manifest, }; } if (command === 'read_local_conversation') { return { path: args?.agentId ? '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl' : '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: args?.agentId ?? null, messages: [], }; } if (command === 'read_local_agent_memory') { return { taskId: args?.taskId, path: '/tmp/authorized-game/memory/agents/design/director.md', content: '', exists: false, }; } if (command === 'read_project_permission_policy') { return emptyProjectPolicy(); } if (command === 'resume_game_creator_agent_runtime_tasks') { return []; } if (command === 'read_game_creator_agent_runtimes') { return []; } if (command === 'read_local_project_file') { runReadCount += 1; return { path: String(args?.relativePath ?? ''), absolutePath: `${String(args?.projectPath ?? '')}/${String( args?.relativePath ?? '', )}`, content: JSON.stringify(makeTrace(runReadCount > 1)), }; } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [] }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); await screen.findByText('已打开:/tmp/authorized-game'); fireEvent.click(screen.getByRole('button', { name: /拆解创作方向/ })); expect(await screen.findByLabelText('Agent 对话')).not.toBeNull(); expect(screen.getByLabelText('Agent 最近证据').textContent).toContain( '暂无最近运行证据', ); fireEvent.click(screen.getByRole('button', { name: '刷新 Agent' })); expect(await screen.findByText('刷新后的拆解方向')).not.toBeNull(); await waitFor(() => { expect(screen.getByLabelText('Agent 对话').textContent).toContain( '刷新后的拆解方向', ); }); expect(screen.getByLabelText('Agent 最近证据').textContent).toContain( 'in: memory/session.md', ); expect(screen.getByLabelText('Agent 最近证据').textContent).toContain( 'out: .agent/spec.md', ); expect(screen.getByLabelText('Agent 最近证据').textContent).toContain( 'tool: llm.planner.refresh · ok · 刷新后工具调用', ); expect(screen.getByLabelText('Agent 最近证据').textContent).toContain( 'tool: llm.planner.refresh · ok · 刷新后工具调用 · in memory/session.md · out .agent/spec.md', ); expect(screen.getByLabelText('Agent 最近证据').textContent).toContain( '还有 1 个工具调用', ); }); it('confirms before refreshing agents when trace read policy requires it', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const trace: GameCreationAgentRunTrace = { schemaVersion: GAME_CREATION_AGENT_RUN_SCHEMA_VERSION, runId: 'run-agent-refresh-confirm', commandId: 'game.generate_draft', status: 'running', passes: 1, maxPasses: 3, toolCallCount: 1, maxToolCalls: 128, stopReason: 'running', goal: '做一个厨房弹幕游戏', coordination: 'Planner', steps: [], artifacts: [], taskGraph: { goal: '做一个厨房弹幕游戏', readyTaskIds: [], activeTaskIds: [], carriedTaskIds: [], repairFocus: [], repairRoutes: [], tasks: createGameCreationAppSeedTasks(), }, passPlans: [], nextStep: 'continue', error: null, updatedAt: 1, }; const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'append_local_permission_log') { return {}; } if (command === 'init_local_game_project') { const projectPath = String(args?.projectPath ?? ''); return { projectPath, manifestPath: `${projectPath}/.agent/manifest.json`, manifest, }; } if (command === 'read_local_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: null, messages: [], }; } if (command === 'read_project_permission_policy') { return { path: '.agent/policy.json', policy: { deniedCommands: [], confirmCommands: ['agent.trace_read'], }, }; } if (command === 'read_local_project_file') { return { path: '.agent/run.latest.json', absolutePath: `${String(args?.projectPath ?? '')}/.agent/run.latest.json`, content: JSON.stringify(trace), }; } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [] }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); expect(await screen.findByText('想做什么游戏?')).not.toBeNull(); invoke.mockClear(); fireEvent.click(screen.getByRole('button', { name: '刷新 Agent' })); expect(await screen.findByText('agent.trace_read')).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'read_local_project_file', expect.anything(), ); fireEvent.click(screen.getByRole('button', { name: '确认' })); await waitFor(() => { expect(invoke).toHaveBeenCalledWith('read_local_project_file', { projectPath: '/tmp/authorized-game', relativePath: '.agent/run.latest.json', commandId: 'agent.trace_read', }); }); }); it('cancels agent run trace refresh policy confirmation from the panel', async () => { const manifest = createGameCreationAppManifest( 'local-project-draft', '未命名游戏原型', ); const invoke = vi.fn( async (command: string, args?: Record) => { if (command === 'append_local_permission_log') { return {}; } if (command === 'init_local_game_project') { const projectPath = String(args?.projectPath ?? ''); return { projectPath, manifestPath: `${projectPath}/.agent/manifest.json`, manifest, }; } if (command === 'read_local_conversation') { return { path: '/tmp/authorized-game/.agent/conversations/project.jsonl', agentId: null, messages: [], }; } if (command === 'read_project_permission_policy') { return { path: '.agent/policy.json', policy: { deniedCommands: [], confirmCommands: ['agent.trace_read'], }, }; } if (command === 'read_local_project_file') { throw new Error('should wait for trace confirmation'); } if (command === 'list_local_project_files') { return { projectPath: String(args?.projectPath ?? ''), files: [] }; } throw new Error(`unexpected invoke ${command}`); }, ); window.__TAURI__ = { core: { invoke } }; renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); expect(await screen.findByText('想做什么游戏?')).not.toBeNull(); invoke.mockClear(); fireEvent.click(screen.getByRole('button', { name: '刷新 Agent' })); const traceReadCommand = await screen.findByText('agent.trace_read'); fireEvent.click( within( traceReadCommand.closest('.pending-command') as HTMLElement, ).getByRole('button', { name: '取消' }), ); expect( await screen.findByText('run: 已取消读取 Agent trace'), ).not.toBeNull(); expect(invoke).not.toHaveBeenCalledWith( 'read_local_project_file', expect.anything(), ); }); }