From b0cfbe4f63c2bf85ec01c1fa989a2861560d3a46 Mon Sep 17 00:00:00 2001 From: Linghong Date: Sun, 20 Sep 2026 18:04:35 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=88=86=E9=A2=84=E8=A7=88=E5=BF=AB?= =?UTF-8?q?=E6=8D=B7=E6=93=8D=E4=BD=9C=E6=B5=8B=E8=AF=95=E5=B9=B6=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E5=88=9D=E5=A7=8B=E5=8C=96=E6=97=B6=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将单条长流程拆为 79 个独立用例,每例重新初始化并使用默认超时 保留有效行为断言并删除无实际操作的空断言块 补齐编辑器插件 mock,移除重复的项目打开状态等待 同步测试拆分边界与定向验证文档 --- .../preview-shortcuts.suite.ts | 25 +- .../preview-shortcuts/actions.ts | 8 +- .../assert-planning-and-status-shortcuts.ts | 3387 ++++++++-------- .../assert-playtest-and-release-shortcuts.ts | 3522 ++++++++--------- .../assert-project-and-design-shortcuts.ts | 3116 ++++++++------- .../assert-project-tools-and-preview.ts | 1653 ++++---- .../preview-shortcuts/invoke-mock.ts | 7 +- .../shared-memory/development-workflow.md | 2 + ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 + 9 files changed, 5972 insertions(+), 5750 deletions(-) diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts.suite.ts index c354e7c1c..5581854ca 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts.suite.ts @@ -1,20 +1,11 @@ -import { it } from '../harness'; -import { openPreviewShortcutProject } from './preview-shortcuts/actions'; -import { arrangePreviewShortcutTest } from './preview-shortcuts/arrange'; -import { assertPlanningAndStatusShortcutFlow } from './preview-shortcuts/assert-planning-and-status-shortcuts'; -import { assertPlaytestAndReleaseShortcutFlow } from './preview-shortcuts/assert-playtest-and-release-shortcuts'; -import { assertProjectAndDesignShortcutFlow } from './preview-shortcuts/assert-project-and-design-shortcuts'; -import { assertProjectToolsAndPreviewFlow } from './preview-shortcuts/assert-project-tools-and-preview'; +import { registerPlanningAndStatusShortcutTests } from './preview-shortcuts/assert-planning-and-status-shortcuts'; +import { registerPlaytestAndReleaseShortcutTests } from './preview-shortcuts/assert-playtest-and-release-shortcuts'; +import { registerProjectAndDesignShortcutTests } from './preview-shortcuts/assert-project-and-design-shortcuts'; +import { registerProjectToolsAndPreviewTests } from './preview-shortcuts/assert-project-tools-and-preview'; export function registerProjectPreviewShortcutTests() { - it('runs preview shortcuts from the main project window', async () => { - const { invoke } = arrangePreviewShortcutTest(); - - openPreviewShortcutProject(); - - await assertProjectAndDesignShortcutFlow(invoke); - await assertPlanningAndStatusShortcutFlow(invoke); - await assertPlaytestAndReleaseShortcutFlow(invoke); - await assertProjectToolsAndPreviewFlow(invoke); - }, 60000); + registerProjectAndDesignShortcutTests(); + registerPlanningAndStatusShortcutTests(); + registerPlaytestAndReleaseShortcutTests(); + registerProjectToolsAndPreviewTests(); } diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/actions.ts b/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/actions.ts index 5cda70075..9f1e816c2 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/actions.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/actions.ts @@ -1,5 +1,9 @@ -import { renderAppAt } from '../../harness'; +import { renderAppAt, screen } from '../../harness'; +import { arrangePreviewShortcutTest } from './arrange'; -export function openPreviewShortcutProject() { +export async function openPreviewShortcutProject() { + const { invoke } = arrangePreviewShortcutTest(); renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game'); + await screen.findByText('已打开:authorized-game'); + return invoke; } diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/assert-planning-and-status-shortcuts.ts b/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/assert-planning-and-status-shortcuts.ts index 3788df509..c51c344c0 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/assert-planning-and-status-shortcuts.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/assert-planning-and-status-shortcuts.ts @@ -2,1704 +2,1781 @@ import { composerText, expect, fireEvent, + it, screen, submitChat, within, } from '../../harness'; -import type { PreviewShortcutInvoke } from './invoke-mock'; +import { openPreviewShortcutProject } from './actions'; import { messageBubble } from './message-bubble'; -export async function assertPlanningAndStatusShortcutFlow( - invoke: PreviewShortcutInvoke, -) { - const manifestReadCountBeforeRisks = invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length; - const fileReadCountBeforeRisks = invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length; - const runLocalCountBeforeRisks = invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length; - await submitChat('/risks'); - expect(await screen.findByText(/项目风险:/)).not.toBeNull(); - expect( - screen.getByText(/最近 run 已通过,但当前本地预览未运行。 建议:\/run/), - ).not.toBeNull(); - expect( - screen.getByText(/还有 3 个 ready 任务等待处理。 建议:\/tasks/), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '处理首个风险' })); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( +export function registerPlanningAndStatusShortcutTests() { + it('预览快捷操作:/risks', async () => { + const invoke = await openPreviewShortcutProject(); + const manifestReadCountBeforeRisks = invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(manifestReadCountBeforeRisks); - expect( - invoke.mock.calls.filter( + ).length; + const fileReadCountBeforeRisks = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(fileReadCountBeforeRisks); - expect( - invoke.mock.calls.filter( + ).length; + const runLocalCountBeforeRisks = invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(runLocalCountBeforeRisks); - - const commandCountsBeforeBlockers = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/blockers'); - const blockerMessages = await screen.findAllByText(/当前阻塞项:/); - const blockerMessage = messageBubble( - blockerMessages[blockerMessages.length - 1], - ); - expect(blockerMessage.textContent).toContain('项目:未命名游戏原型'); - expect(blockerMessage.textContent).toContain( - 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', - ); - expect(blockerMessage.textContent).toContain( - '原型已通过,但本地预览未运行。 建议:/run', - ); - expect(blockerMessage.textContent).toContain( - '原型已通过,但本地试玩包尚未导出。 建议:/export', - ); - expect(blockerMessage.textContent).toContain( - 'ready 任务 1 个:美术组 / Asset 生成首版美术素材(art-asset-plan)。 建议:/todo', - ); - expect(blockerMessage.textContent).toContain( - '边界:只整理阻塞项;不读取文件;不启动预览;不导出试玩包;不写项目', - ); - expect(blockerMessage.textContent).toContain('建议:/run'); - const blockerMessageList = document.querySelector('.message-list'); - expect(blockerMessageList).not.toBeNull(); - const blockerDraftButtons = within( - blockerMessageList as HTMLElement, - ).getAllByRole('button', { name: '处理首个阻塞' }); - fireEvent.click(blockerDraftButtons[blockerDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeBlockers.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeBlockers.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeBlockers.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeBlockers.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeBlockers.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeBlockers.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeBlockers.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeBlockers.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeBlockers.controlAgentRun); - - const commandCountsBeforeReady = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/ready'); - const readyMessages = await screen.findAllByText(/试玩就绪度:/); - const readyMessage = messageBubble(readyMessages[readyMessages.length - 1]); - expect(readyMessage.textContent).toContain('项目:未命名游戏原型'); - expect(readyMessage.textContent).toContain( - 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', - ); - expect(readyMessage.textContent).toContain('原型:最近 run 已通过'); - expect(readyMessage.textContent).toContain('预览:未启动'); - expect(readyMessage.textContent).toContain('自检:已通过'); - expect(readyMessage.textContent).toContain('试玩包:未导出'); - expect(readyMessage.textContent).toContain('任务:失败 0 / ready 1'); - expect(readyMessage.textContent).toContain('素材:美术 已有 / 音频 可后补'); - expect(readyMessage.textContent).toContain( - '结论:接近可测,先补齐预览 / 自检 / 试玩包', - ); - expect(readyMessage.textContent).toContain( - '边界:只判断就绪度;不读取文件;不启动预览;不导出试玩包;不写项目', - ); - expect(readyMessage.textContent).toContain('建议:/run'); - const readyMessageList = document.querySelector('.message-list'); - expect(readyMessageList).not.toBeNull(); - const readyDraftButtons = within( - readyMessageList as HTMLElement, - ).getAllByRole('button', { name: '启动试玩' }); - fireEvent.click(readyDraftButtons[readyDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeReady.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeReady.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeReady.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeReady.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeReady.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeReady.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeReady.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeReady.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeReady.controlAgentRun); - - const commandCountsBeforeEvidence = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/evidence'); - const evidenceMessages = await screen.findAllByText(/验证证据台账:/); - const evidenceMessage = messageBubble( - evidenceMessages[evidenceMessages.length - 1], - ); - expect(evidenceMessage.textContent).toContain('项目:未命名游戏原型'); - expect(evidenceMessage.textContent).toContain( - 'Run trace:已有 run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', - ); - expect(evidenceMessage.textContent).toContain('Evaluator:通过'); - expect(evidenceMessage.textContent).toContain('静态自检:已有通过证据'); - expect(evidenceMessage.textContent).toContain('预览:未启动'); - expect(evidenceMessage.textContent).toContain('试玩包:缺失'); - expect(evidenceMessage.textContent).toContain( - '入口产物:缺失 trace 产物证据', - ); - expect(evidenceMessage.textContent).toContain( - '资产:2 个 · 美术 有 · 音频 可后补', - ); - expect(evidenceMessage.textContent).toContain('最近失败命令:暂无'); - expect(evidenceMessage.textContent).toContain('缺口:'); - expect(evidenceMessage.textContent).toContain('本地预览未运行 · 建议 /run'); - expect(evidenceMessage.textContent).toContain( - '缺少本地试玩包导出记录 · 建议 /export', - ); - expect(evidenceMessage.textContent).toContain( - '边界:只整理当前已加载证据;不读取文件;不启动或打开预览;不导出试玩包;不写项目', - ); - expect(evidenceMessage.textContent).toContain('建议:/run'); - const evidenceMessageList = document.querySelector('.message-list'); - expect(evidenceMessageList).not.toBeNull(); - const evidenceDraftButtons = within( - evidenceMessageList as HTMLElement, - ).getAllByRole('button', { name: '补齐首个证据缺口' }); - fireEvent.click(evidenceDraftButtons[evidenceDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeEvidence.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeEvidence.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeEvidence.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeEvidence.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeEvidence.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeEvidence.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeEvidence.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeEvidence.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeEvidence.controlAgentRun); - - const commandCountsBeforeDeps = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/deps'); - const dependencyMessages = await screen.findAllByText(/任务依赖链:/); - const dependencyMessage = messageBubble( - dependencyMessages[dependencyMessages.length - 1], - ); - expect(dependencyMessage.textContent).toContain( - 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', - ); - expect(dependencyMessage.textContent).toContain( - '状态:active 0 / carry 0 / ready 1 / 等待依赖 13', - ); - expect(dependencyMessage.textContent).toContain( - '美术组 / Asset 生成首版美术素材(art-asset-plan) · 等待:美术组 / Director 确定视觉方向与规范图(art-director);设计实现组 / Gameplay 确定玩法规格与界面原型(design-foundation)', - ); - expect(dependencyMessage.textContent).toContain( - '设计实现组 / Gameplay 确定玩法规格与界面原型(design-foundation) · 等待:设计实现组 / Director 拆解创作方向(design-director);美术组 / Director 确定视觉方向与规范图(art-director)', - ); - expect(dependencyMessage.textContent).toContain( - '边界:只整理任务依赖;不读取任务文件;不启动 run;不修改项目', - ); - expect(dependencyMessage.textContent).toContain('建议:/criteria'); - const dependencyMessageList = document.querySelector('.message-list'); - expect(dependencyMessageList).not.toBeNull(); - const dependencyDraftButtons = within( - dependencyMessageList as HTMLElement, - ).getAllByRole('button', { name: '查看验收标准' }); - fireEvent.click(dependencyDraftButtons[dependencyDraftButtons.length - 1]); - expect(await composerText()).toBe('/criteria'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeDeps.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeDeps.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeDeps.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeDeps.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeDeps.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeDeps.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeDeps.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeDeps.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeDeps.controlAgentRun); - - const commandCountsBeforeRevise = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/revise'); - const revisionMessages = await screen.findAllByText(/改版草稿:/); - const revisionMessage = messageBubble( - revisionMessages[revisionMessages.length - 1], - ); - expect(revisionMessage.textContent).toContain('项目:未命名游戏原型'); - expect(revisionMessage.textContent).toContain( - 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', - ); - expect(revisionMessage.textContent).toContain( - '最近评审:Evaluator #1 · passed · 质量评审通过', - ); - expect(revisionMessage.textContent).toContain('最近试玩:暂无'); - expect(revisionMessage.textContent).toContain( - '推进 ready 任务:美术组 / Asset 生成首版美术素材(art-asset-plan)', - ); - expect(revisionMessage.textContent).toContain( - '补齐本地试玩:启动预览并验证首屏', - ); - expect(revisionMessage.textContent).toContain('交付:导出本地试玩包'); - expect(revisionMessage.textContent).toContain( - '保留项:保留当前已通过的核心玩法和可运行入口', - ); - expect(revisionMessage.textContent).toContain( - '调整项:推进 ready 任务:美术组 / Asset 生成首版美术素材(art-asset-plan);补齐本地试玩:启动预览并验证首屏', - ); - expect(revisionMessage.textContent).toContain( - '新增项:补一次本地预览验证;补导出本地试玩包', - ); - expect(revisionMessage.textContent).toContain( - '验收口径:通过 /ready、/qa 和 /changes 复查', - ); - expect(revisionMessage.textContent).toContain( - '参考命令:/ready;/deps;/qa;/changes', - ); - expect(revisionMessage.textContent).toContain( - '草稿:/agent-resume 改版说明:', - ); - expect(revisionMessage.textContent).toContain( - '边界:只准备改版说明;不继续 run;不读取文件;不启动预览;不导出试玩包;不写项目', - ); - expect(revisionMessage.textContent).toContain( - '建议:/agent-resume 改版说明:', - ); - const revisionMessageList = document.querySelector('.message-list'); - expect(revisionMessageList).not.toBeNull(); - const revisionDraftButtons = within( - revisionMessageList as HTMLElement, - ).getAllByRole('button', { name: '填入改版说明' }); - fireEvent.click(revisionDraftButtons[revisionDraftButtons.length - 1]); - expect(await composerText()).toEqual( - expect.stringContaining('/agent-resume 改版说明:'), - ); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeRevise.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeRevise.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeRevise.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeRevise.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeRevise.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeRevise.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeRevise.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeRevise.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeRevise.controlAgentRun); - - const commandCountsBeforePrivacy = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/privacy'); - const privacyMessages = await screen.findAllByText(/隐私与导出边界:/); - const privacyMessage = messageBubble( - privacyMessages[privacyMessages.length - 1], - ); - expect(privacyMessage.textContent).toContain('项目:未命名游戏原型'); - expect(privacyMessage.textContent).toContain( - '本地目录:/tmp/authorized-game', - ); - expect(privacyMessage.textContent).toContain( - '智能服务凭据由服务端按登录账号管理;不进入客户端、manifest、trace、聊天、导出包或项目文件', - ); - expect(privacyMessage.textContent).toContain( - '本地预览:未启动;仅限 127.0.0.1 本机访问', - ); - expect(privacyMessage.textContent).toContain( - '试玩包:尚未导出;只应包含 game/、assets/ 和 exports/README.md', - ); - expect(privacyMessage.textContent).toContain( - '内部文件:.agent/、memory/、日志、trace、配置和密钥不得进入试玩包', - ); - expect(privacyMessage.textContent).toContain( - '素材来源:2 个;上传 1 / 画板 1', - ); - expect(privacyMessage.textContent).toContain( - 'Trace:run-main-shortcut-trace · 3 个内部产物记录;只通过 /trace 或 /internals 查看,不作为交付内容', - ); - expect(privacyMessage.textContent).toContain( - '交付前建议:/credits;/ready;/export;/exports', - ); - expect(privacyMessage.textContent).toContain( - '边界:只整理隐私与交付口径;不读取文件;不导出;不启动预览;不写项目', - ); - expect(privacyMessage.textContent).toContain('建议:/credits'); - const privacyMessageList = document.querySelector('.message-list'); - expect(privacyMessageList).not.toBeNull(); - const privacyDraftButtons = within( - privacyMessageList as HTMLElement, - ).getAllByRole('button', { name: '查看素材来源' }); - fireEvent.click(privacyDraftButtons[privacyDraftButtons.length - 1]); - expect(await composerText()).toBe('/credits'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforePrivacy.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforePrivacy.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforePrivacy.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforePrivacy.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforePrivacy.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforePrivacy.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforePrivacy.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforePrivacy.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforePrivacy.controlAgentRun); - - const fileReadCountBeforeCriteria = invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length; - const manifestReadCountBeforeCriteria = invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length; - const runLocalCountBeforeCriteria = invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length; - await submitChat('/criteria'); - expect(await screen.findByText(/当前验收标准:/)).not.toBeNull(); - const criteriaMessages = screen.getAllByText(/当前验收标准:/); - const criteriaMessage = messageBubble( - criteriaMessages[criteriaMessages.length - 1], - ); - expect(criteriaMessage.textContent).toContain( - 'ready:美术组 / Asset 生成首版美术素材(art-asset-plan)', - ); - expect(criteriaMessage.textContent).toContain( - '验收:角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记', - ); - expect(criteriaMessage.textContent).toContain( - '产物:assets/manifest.art.json, assets/art-spritesheet.png', - ); - const criteriaDraftButtons = screen.getAllByRole('button', { - name: '查看任务', + ).length; + await submitChat('/risks'); + expect(await screen.findByText(/项目风险:/)).not.toBeNull(); + expect( + screen.getByText(/最近 run 已通过,但当前本地预览未运行。 建议:\/run/), + ).not.toBeNull(); + expect( + screen.getByText(/还有 3 个 ready 任务等待处理。 建议:\/tasks/), + ).not.toBeNull(); + fireEvent.click(screen.getByRole('button', { name: '处理首个风险' })); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(manifestReadCountBeforeRisks); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(fileReadCountBeforeRisks); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(runLocalCountBeforeRisks); }); - fireEvent.click(criteriaDraftButtons[criteriaDraftButtons.length - 1]); - expect(await composerText()).toBe('/tasks'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(fileReadCountBeforeCriteria); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(manifestReadCountBeforeCriteria); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(runLocalCountBeforeCriteria); - const fileReadCountBeforeGroups = invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length; - const manifestReadCountBeforeGroups = invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length; - const runLocalCountBeforeGroups = invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length; - await submitChat('/groups'); - expect(await screen.findByText(/专业组进度:/)).not.toBeNull(); - const groupMessages = screen.getAllByText(/专业组进度:/); - const groupMessage = messageBubble(groupMessages[groupMessages.length - 1]); - expect(groupMessage.textContent).toContain( - '美术组:完成 0/3 · active 0 · carry 0 · ready 1', - ); - expect(groupMessage.textContent).toContain('下一步 Asset 生成首版美术素材'); - expect(groupMessage.textContent).toContain('程序组:完成 0/5'); - const groupDraftButtons = screen.getAllByRole('button', { - name: '查看任务', + it('预览快捷操作:/blockers', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeBlockers = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/blockers'); + const blockerMessages = await screen.findAllByText(/当前阻塞项:/); + const blockerMessage = messageBubble( + blockerMessages[blockerMessages.length - 1], + ); + expect(blockerMessage.textContent).toContain('项目:未命名游戏原型'); + expect(blockerMessage.textContent).toContain( + 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', + ); + expect(blockerMessage.textContent).toContain( + '原型已通过,但本地预览未运行。 建议:/run', + ); + expect(blockerMessage.textContent).toContain( + '原型已通过,但本地试玩包尚未导出。 建议:/export', + ); + expect(blockerMessage.textContent).toContain( + 'ready 任务 1 个:美术组 / Asset 生成首版美术素材(art-asset-plan)。 建议:/todo', + ); + expect(blockerMessage.textContent).toContain( + '边界:只整理阻塞项;不读取文件;不启动预览;不导出试玩包;不写项目', + ); + expect(blockerMessage.textContent).toContain('建议:/run'); + const blockerMessageList = document.querySelector('.message-list'); + expect(blockerMessageList).not.toBeNull(); + const blockerDraftButtons = within( + blockerMessageList as HTMLElement, + ).getAllByRole('button', { name: '处理首个阻塞' }); + fireEvent.click(blockerDraftButtons[blockerDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeBlockers.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeBlockers.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeBlockers.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeBlockers.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeBlockers.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeBlockers.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeBlockers.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeBlockers.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforeBlockers.controlAgentRun); }); - fireEvent.click(groupDraftButtons[groupDraftButtons.length - 1]); - expect(await composerText()).toBe('/tasks'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(fileReadCountBeforeGroups); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(manifestReadCountBeforeGroups); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(runLocalCountBeforeGroups); - const commandCountsBeforeBalance = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - }; - await submitChat('/balance'); - expect(await screen.findByText(/数值状态:/)).not.toBeNull(); - const balanceMessages = screen.getAllByText(/数值状态:/); - const balanceMessage = messageBubble( - balanceMessages[balanceMessages.length - 1], - ); - expect(balanceMessage.textContent).toContain('项目:未命名游戏原型'); - expect(balanceMessage.textContent).toContain( - 'balance-director:Director 确定数值口径 · 待处理', - ); - expect(balanceMessage.textContent).toContain( - 'balance-seed:Difficulty 生成初版数值 · 待处理', - ); - expect(balanceMessage.textContent).toContain( - '难度、节奏和得分口径可指导数值表', - ); - expect(balanceMessage.textContent).toContain( - '速度、生命、得分和难度参数可被程序组读取', - ); - expect(balanceMessage.textContent).toContain( - '数值表:game/balance.json · 待生成', - ); - expect(balanceMessage.textContent).toContain( - '试玩关联:/playtest;/feedback', - ); - expect(balanceMessage.textContent).toContain( - '建议:/agent-resume 数值调整:前 30 秒更易上手;得分反馈更明显;失败后重开节奏更快', - ); - const balanceMessageList = document.querySelector('.message-list'); - expect(balanceMessageList).not.toBeNull(); - const balanceDraftButtons = within( - balanceMessageList as HTMLElement, - ).getAllByRole('button', { name: '填写数值反馈' }); - fireEvent.click(balanceDraftButtons[balanceDraftButtons.length - 1]); - expect(await composerText()).toBe( - '/agent-resume 数值调整:前 30 秒更易上手;得分反馈更明显;失败后重开节奏更快', - ); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeBalance.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeBalance.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeBalance.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeBalance.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeBalance.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeBalance.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeBalance.exportPackage); - - const fileReadCountBeforeBudget = invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length; - const manifestReadCountBeforeBudget = invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length; - const runLocalCountBeforeBudget = invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length; - await submitChat('/budget'); - expect(await screen.findByText(/运行预算:/)).not.toBeNull(); - const budgetMessages = screen.getAllByText(/运行预算:/); - const budgetMessage = messageBubble( - budgetMessages[budgetMessages.length - 1], - ); - expect(budgetMessage.textContent).toContain( - 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', - ); - expect(budgetMessage.textContent).toContain('轮次:已用 1/3 · 剩余 2'); - expect(budgetMessage.textContent).toContain( - '工具调用:已用 1/128 · 剩余 127', - ); - expect(budgetMessage.textContent).toContain('下一步:preview'); - expect(budgetMessage.textContent).toContain('建议:/publish'); - const budgetDraftButtons = screen.getAllByRole('button', { - name: '查看发布准备', + it('预览快捷操作:/ready', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeReady = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/ready'); + const readyMessages = await screen.findAllByText(/试玩就绪度:/); + const readyMessage = messageBubble(readyMessages[readyMessages.length - 1]); + expect(readyMessage.textContent).toContain('项目:未命名游戏原型'); + expect(readyMessage.textContent).toContain( + 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', + ); + expect(readyMessage.textContent).toContain('原型:最近 run 已通过'); + expect(readyMessage.textContent).toContain('预览:未启动'); + expect(readyMessage.textContent).toContain('自检:已通过'); + expect(readyMessage.textContent).toContain('试玩包:未导出'); + expect(readyMessage.textContent).toContain('任务:失败 0 / ready 1'); + expect(readyMessage.textContent).toContain('素材:美术 已有 / 音频 可后补'); + expect(readyMessage.textContent).toContain( + '结论:接近可测,先补齐预览 / 自检 / 试玩包', + ); + expect(readyMessage.textContent).toContain( + '边界:只判断就绪度;不读取文件;不启动预览;不导出试玩包;不写项目', + ); + expect(readyMessage.textContent).toContain('建议:/run'); + const readyMessageList = document.querySelector('.message-list'); + expect(readyMessageList).not.toBeNull(); + const readyDraftButtons = within( + readyMessageList as HTMLElement, + ).getAllByRole('button', { name: '启动试玩' }); + fireEvent.click(readyDraftButtons[readyDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeReady.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeReady.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeReady.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeReady.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeReady.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeReady.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeReady.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeReady.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforeReady.controlAgentRun); }); - fireEvent.click(budgetDraftButtons[budgetDraftButtons.length - 1]); - expect(await composerText()).toBe('/publish'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(fileReadCountBeforeBudget); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(manifestReadCountBeforeBudget); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(runLocalCountBeforeBudget); - const fileReadCountBeforeQa = invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length; - const manifestReadCountBeforeQa = invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length; - const runLocalCountBeforeQa = invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length; - const startPreviewCountBeforeQa = invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length; - const openPreviewCountBeforeQa = invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length; - await submitChat('/qa'); - expect(await screen.findByText(/质量检查:/)).not.toBeNull(); - const qaMessages = screen.getAllByText(/质量检查:/); - const qaMessage = messageBubble(qaMessages[qaMessages.length - 1]); - expect(qaMessage.textContent).toContain( - 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', - ); - expect(qaMessage.textContent).toContain('Evaluator:通过 · 质量评审通过'); - expect(qaMessage.textContent).toContain('任务:完成 0/16 · ready 3 · 失败 0'); - expect(qaMessage.textContent).toContain('静态自检:通过'); - expect(qaMessage.textContent).toContain('试玩:待启动预览'); - expect(qaMessage.textContent).toContain('产物:3 个'); - expect(qaMessage.textContent).toContain('建议:/playtest'); - const qaDraftButtons = screen.getAllByRole('button', { - name: '查看试玩状态', + it('预览快捷操作:/evidence', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeEvidence = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/evidence'); + const evidenceMessages = await screen.findAllByText(/验证证据台账:/); + const evidenceMessage = messageBubble( + evidenceMessages[evidenceMessages.length - 1], + ); + expect(evidenceMessage.textContent).toContain('项目:未命名游戏原型'); + expect(evidenceMessage.textContent).toContain( + 'Run trace:已有 run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', + ); + expect(evidenceMessage.textContent).toContain('Evaluator:通过'); + expect(evidenceMessage.textContent).toContain('静态自检:已有通过证据'); + expect(evidenceMessage.textContent).toContain('预览:未启动'); + expect(evidenceMessage.textContent).toContain('试玩包:缺失'); + expect(evidenceMessage.textContent).toContain( + '入口产物:缺失 trace 产物证据', + ); + expect(evidenceMessage.textContent).toContain( + '资产:2 个 · 美术 有 · 音频 可后补', + ); + expect(evidenceMessage.textContent).toContain('最近失败命令:暂无'); + expect(evidenceMessage.textContent).toContain('缺口:'); + expect(evidenceMessage.textContent).toContain('本地预览未运行 · 建议 /run'); + expect(evidenceMessage.textContent).toContain( + '缺少本地试玩包导出记录 · 建议 /export', + ); + expect(evidenceMessage.textContent).toContain( + '边界:只整理当前已加载证据;不读取文件;不启动或打开预览;不导出试玩包;不写项目', + ); + expect(evidenceMessage.textContent).toContain('建议:/run'); + const evidenceMessageList = document.querySelector('.message-list'); + expect(evidenceMessageList).not.toBeNull(); + const evidenceDraftButtons = within( + evidenceMessageList as HTMLElement, + ).getAllByRole('button', { name: '补齐首个证据缺口' }); + fireEvent.click(evidenceDraftButtons[evidenceDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeEvidence.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeEvidence.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeEvidence.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeEvidence.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeEvidence.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeEvidence.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeEvidence.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeEvidence.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforeEvidence.controlAgentRun); }); - fireEvent.click(qaDraftButtons[qaDraftButtons.length - 1]); - expect(await composerText()).toBe('/playtest'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(fileReadCountBeforeQa); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(manifestReadCountBeforeQa); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(runLocalCountBeforeQa); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(startPreviewCountBeforeQa); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(openPreviewCountBeforeQa); - const fileReadCountBeforeChanges = invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length; - const manifestReadCountBeforeChanges = invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length; - const fileListCountBeforeChanges = invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length; - const runLocalCountBeforeChanges = invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length; - const startPreviewCountBeforeChanges = invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length; - const openPreviewCountBeforeChanges = invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length; - await submitChat('/changes'); - expect(await screen.findByText(/最近变更:/)).not.toBeNull(); - const changeMessages = screen.getAllByText(/最近变更:/); - const changeMessage = messageBubble( - changeMessages[changeMessages.length - 1], - ); - expect(changeMessage.textContent).toContain( - 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', - ); - expect(changeMessage.textContent).toContain('可验产物:3 个'); - expect(changeMessage.textContent).toContain('exports/README.md · 128B'); - expect(changeMessage.textContent).toContain( - '.agent/passes/pass-1/agenda.md · 96B', - ); - expect(changeMessage.textContent).toContain( - 'Generator #1 · completed · .agent/passes/pass-1/draft.json', - ); - expect(changeMessage.textContent).toContain( - '最近命令:game.static_smoke · 完成', - ); - expect(changeMessage.textContent).toContain( - '真实差异:/checkpoints 后 /diff checkpoint-id', - ); - expect(changeMessage.textContent).toContain('建议:/read exports/README.md'); - const changeDraftButtons = screen.getAllByRole('button', { - name: '读取首个产物', + it('预览快捷操作:/deps', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeDeps = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/deps'); + const dependencyMessages = await screen.findAllByText(/任务依赖链:/); + const dependencyMessage = messageBubble( + dependencyMessages[dependencyMessages.length - 1], + ); + expect(dependencyMessage.textContent).toContain( + 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', + ); + expect(dependencyMessage.textContent).toContain( + '状态:active 0 / carry 0 / ready 1 / 等待依赖 13', + ); + expect(dependencyMessage.textContent).toContain( + '美术组 / Asset 生成首版美术素材(art-asset-plan) · 等待:美术组 / Director 确定视觉方向与规范图(art-director);设计实现组 / Gameplay 确定玩法规格与界面原型(design-foundation)', + ); + expect(dependencyMessage.textContent).toContain( + '设计实现组 / Gameplay 确定玩法规格与界面原型(design-foundation) · 等待:设计实现组 / Director 拆解创作方向(design-director);美术组 / Director 确定视觉方向与规范图(art-director)', + ); + expect(dependencyMessage.textContent).toContain( + '边界:只整理任务依赖;不读取任务文件;不启动 run;不修改项目', + ); + expect(dependencyMessage.textContent).toContain('建议:/criteria'); + const dependencyMessageList = document.querySelector('.message-list'); + expect(dependencyMessageList).not.toBeNull(); + const dependencyDraftButtons = within( + dependencyMessageList as HTMLElement, + ).getAllByRole('button', { name: '查看验收标准' }); + fireEvent.click(dependencyDraftButtons[dependencyDraftButtons.length - 1]); + expect(await composerText()).toBe('/criteria'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeDeps.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeDeps.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeDeps.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeDeps.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeDeps.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeDeps.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeDeps.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeDeps.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforeDeps.controlAgentRun); }); - fireEvent.click(changeDraftButtons[changeDraftButtons.length - 1]); - expect(await composerText()).toBe('/read exports/README.md'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(fileReadCountBeforeChanges); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(manifestReadCountBeforeChanges); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(fileListCountBeforeChanges); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(runLocalCountBeforeChanges); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(startPreviewCountBeforeChanges); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(openPreviewCountBeforeChanges); - const fileReadCountBeforeReview = invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length; - const manifestReadCountBeforeReview = invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length; - const runLocalCountBeforeReview = invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length; - const startPreviewCountBeforeReview = invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length; - const openPreviewCountBeforeReview = invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length; - await submitChat('/review'); - expect(await screen.findByText(/评审状态:/)).not.toBeNull(); - const reviewMessages = screen.getAllByText(/评审状态:/); - const reviewMessage = messageBubble( - reviewMessages[reviewMessages.length - 1], - ); - expect(reviewMessage.textContent).toContain('Evaluator:通过'); - expect(reviewMessage.textContent).toContain('返工焦点:暂无'); - expect(reviewMessage.textContent).toContain( - '评审记录:/read .agent/findings.md', - ); - expect(reviewMessage.textContent).toContain( - 'Evaluator #1 · passed · 质量评审通过', - ); - fireEvent.click(screen.getByRole('button', { name: '读取评审记录' })); - expect(await composerText()).toBe('/read .agent/findings.md'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(fileReadCountBeforeReview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(manifestReadCountBeforeReview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(runLocalCountBeforeReview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(startPreviewCountBeforeReview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(openPreviewCountBeforeReview); - - const fileReadCountBeforeContext = invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length; - await submitChat('/context'); - expect(await screen.findByText(/上下文来源:/)).not.toBeNull(); - expect(screen.getByText(/项目对话:\/history/)).not.toBeNull(); - expect(screen.getByText(/项目黑板:\/memory blackboard/)).not.toBeNull(); - expect(screen.getByText(/Agent 私有记忆:16 个/)).not.toBeNull(); - expect( - screen.getByText(/最近 Run:run-main-shortcut-trace · \/trace/), - ).not.toBeNull(); - expect( - screen.getByText(/\.agent\/spec\.md:\/read \.agent\/spec\.md/), - ).not.toBeNull(); - const contextDraftButtons = screen.getAllByRole('button', { - name: '读取首个上下文', + it('预览快捷操作:/revise', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeRevise = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/revise'); + const revisionMessages = await screen.findAllByText(/改版草稿:/); + const revisionMessage = messageBubble( + revisionMessages[revisionMessages.length - 1], + ); + expect(revisionMessage.textContent).toContain('项目:未命名游戏原型'); + expect(revisionMessage.textContent).toContain( + 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', + ); + expect(revisionMessage.textContent).toContain( + '最近评审:Evaluator #1 · passed · 质量评审通过', + ); + expect(revisionMessage.textContent).toContain('最近试玩:暂无'); + expect(revisionMessage.textContent).toContain( + '推进 ready 任务:美术组 / Asset 生成首版美术素材(art-asset-plan)', + ); + expect(revisionMessage.textContent).toContain( + '补齐本地试玩:启动预览并验证首屏', + ); + expect(revisionMessage.textContent).toContain('交付:导出本地试玩包'); + expect(revisionMessage.textContent).toContain( + '保留项:保留当前已通过的核心玩法和可运行入口', + ); + expect(revisionMessage.textContent).toContain( + '调整项:推进 ready 任务:美术组 / Asset 生成首版美术素材(art-asset-plan);补齐本地试玩:启动预览并验证首屏', + ); + expect(revisionMessage.textContent).toContain( + '新增项:补一次本地预览验证;补导出本地试玩包', + ); + expect(revisionMessage.textContent).toContain( + '验收口径:通过 /ready、/qa 和 /changes 复查', + ); + expect(revisionMessage.textContent).toContain( + '参考命令:/ready;/deps;/qa;/changes', + ); + expect(revisionMessage.textContent).toContain( + '草稿:/agent-resume 改版说明:', + ); + expect(revisionMessage.textContent).toContain( + '边界:只准备改版说明;不继续 run;不读取文件;不启动预览;不导出试玩包;不写项目', + ); + expect(revisionMessage.textContent).toContain( + '建议:/agent-resume 改版说明:', + ); + const revisionMessageList = document.querySelector('.message-list'); + expect(revisionMessageList).not.toBeNull(); + const revisionDraftButtons = within( + revisionMessageList as HTMLElement, + ).getAllByRole('button', { name: '填入改版说明' }); + fireEvent.click(revisionDraftButtons[revisionDraftButtons.length - 1]); + expect(await composerText()).toEqual( + expect.stringContaining('/agent-resume 改版说明:'), + ); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeRevise.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeRevise.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeRevise.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeRevise.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeRevise.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeRevise.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeRevise.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeRevise.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforeRevise.controlAgentRun); }); - fireEvent.click(contextDraftButtons[contextDraftButtons.length - 1]); - expect(await composerText()).toBe('/read .agent/spec.md'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(fileReadCountBeforeContext); - const fileReadCountBeforeTimeline = invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length; - const manifestReadCountBeforeTimeline = invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length; - const runLocalCountBeforeTimeline = invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length; - const startPreviewCountBeforeTimeline = invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length; - const openPreviewCountBeforeTimeline = invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length; - await submitChat('/timeline'); - expect(await screen.findByText(/项目时间线:/)).not.toBeNull(); - const timelineMessages = screen.getAllByText(/项目时间线:/); - const timelineMessage = messageBubble( - timelineMessages[timelineMessages.length - 1], - ); - expect(timelineMessage.textContent).toContain( - 'Run:run-main-shortcut-trace · passed / done', - ); - expect(timelineMessage.textContent).toContain( - '命令 game.static_smoke · 完成 · 日志 /read .agent/logs/command.log', - ); - expect(timelineMessage.textContent).toContain( - 'Generator #1 / generate · completed · 生成可运行草案 · 输出 .agent/passes/pass-1/draft.json', - ); - expect(timelineMessage.textContent).toContain( - 'Evaluator #1 / evaluation · passed · 质量评审通过', - ); - const timelineDraftButtons = screen.getAllByRole('button', { - name: '读取最近日志', + it('预览快捷操作:/privacy', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforePrivacy = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/privacy'); + const privacyMessages = await screen.findAllByText(/隐私与导出边界:/); + const privacyMessage = messageBubble( + privacyMessages[privacyMessages.length - 1], + ); + expect(privacyMessage.textContent).toContain('项目:未命名游戏原型'); + expect(privacyMessage.textContent).toContain( + '本地目录:/tmp/authorized-game', + ); + expect(privacyMessage.textContent).toContain( + '智能服务凭据由服务端按登录账号管理;不进入客户端、manifest、trace、聊天、导出包或项目文件', + ); + expect(privacyMessage.textContent).toContain( + '本地预览:未启动;仅限 127.0.0.1 本机访问', + ); + expect(privacyMessage.textContent).toContain( + '试玩包:尚未导出;只应包含 game/、assets/ 和 exports/README.md', + ); + expect(privacyMessage.textContent).toContain( + '内部文件:.agent/、memory/、日志、trace、配置和密钥不得进入试玩包', + ); + expect(privacyMessage.textContent).toContain( + '素材来源:2 个;上传 1 / 画板 1', + ); + expect(privacyMessage.textContent).toContain( + 'Trace:run-main-shortcut-trace · 3 个内部产物记录;只通过 /trace 或 /internals 查看,不作为交付内容', + ); + expect(privacyMessage.textContent).toContain( + '交付前建议:/credits;/ready;/export;/exports', + ); + expect(privacyMessage.textContent).toContain( + '边界:只整理隐私与交付口径;不读取文件;不导出;不启动预览;不写项目', + ); + expect(privacyMessage.textContent).toContain('建议:/credits'); + const privacyMessageList = document.querySelector('.message-list'); + expect(privacyMessageList).not.toBeNull(); + const privacyDraftButtons = within( + privacyMessageList as HTMLElement, + ).getAllByRole('button', { name: '查看素材来源' }); + fireEvent.click(privacyDraftButtons[privacyDraftButtons.length - 1]); + expect(await composerText()).toBe('/credits'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforePrivacy.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforePrivacy.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforePrivacy.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforePrivacy.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforePrivacy.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforePrivacy.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforePrivacy.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforePrivacy.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforePrivacy.controlAgentRun); }); - fireEvent.click(timelineDraftButtons[timelineDraftButtons.length - 1]); - expect(await composerText()).toBe('/read .agent/logs/command.log'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(fileReadCountBeforeTimeline); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(manifestReadCountBeforeTimeline); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(runLocalCountBeforeTimeline); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(startPreviewCountBeforeTimeline); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(openPreviewCountBeforeTimeline); - const manifestReadCountBeforeHandoff = invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length; - const fileReadCountBeforeHandoff = invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length; - const runLocalCountBeforeHandoff = invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length; - await submitChat('/handoff'); - expect(await screen.findByText(/项目交接:/)).not.toBeNull(); - const handoffMessages = screen.getAllByText(/项目交接:/); - const handoffMessage = messageBubble( - handoffMessages[handoffMessages.length - 1], - ); - expect(handoffMessage.textContent).toContain('Run:run-main-shortcut-trace'); - expect(handoffMessage.textContent).toContain( - 'Ready:art/Asset 生成首版美术素材', - ); - expect(handoffMessage.textContent).toContain('历史:已加载 0 个 run'); - const nextActionButtons = screen.getAllByRole('button', { - name: '查看下一步', + it('预览快捷操作:/criteria', async () => { + const invoke = await openPreviewShortcutProject(); + const fileReadCountBeforeCriteria = invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length; + const manifestReadCountBeforeCriteria = invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length; + const runLocalCountBeforeCriteria = invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length; + await submitChat('/criteria'); + expect(await screen.findByText(/当前验收标准:/)).not.toBeNull(); + const criteriaMessages = screen.getAllByText(/当前验收标准:/); + const criteriaMessage = messageBubble( + criteriaMessages[criteriaMessages.length - 1], + ); + expect(criteriaMessage.textContent).toContain( + 'ready:美术组 / Asset 生成首版美术素材(art-asset-plan)', + ); + expect(criteriaMessage.textContent).toContain( + '验收:角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记', + ); + expect(criteriaMessage.textContent).toContain( + '产物:assets/manifest.art.json, assets/art-spritesheet.png', + ); + const criteriaDraftButtons = screen.getAllByRole('button', { + name: '查看任务', + }); + fireEvent.click(criteriaDraftButtons[criteriaDraftButtons.length - 1]); + expect(await composerText()).toBe('/tasks'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(fileReadCountBeforeCriteria); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(manifestReadCountBeforeCriteria); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(runLocalCountBeforeCriteria); }); - fireEvent.click(nextActionButtons[nextActionButtons.length - 1]); - expect(await composerText()).toBe('/next'); - expect( - invoke.mock.calls.filter( + + it('预览快捷操作:/groups', async () => { + const invoke = await openPreviewShortcutProject(); + const fileReadCountBeforeGroups = invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length; + const manifestReadCountBeforeGroups = invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(manifestReadCountBeforeHandoff); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(fileReadCountBeforeHandoff); - expect( - invoke.mock.calls.filter( + ).length; + const runLocalCountBeforeGroups = invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(runLocalCountBeforeHandoff); - - const fileReadCountBeforeRuns = invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length; - await submitChat('/runs'); - expect(await screen.findByText(/Run 历史读取命令:/)).not.toBeNull(); - expect( - screen.getByText(/当前指针 run-main-shortcut-trace .*:\/trace/), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '查看最近 Run' })); - expect(await composerText()).toBe('/trace'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(fileReadCountBeforeRuns); - - const fileReadCountBeforeRunFiles = invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length; - await submitChat('/run-files'); - expect( - await screen.findByText(/Agent 运行辅助文件读取命令:/), - ).not.toBeNull(); - expect(screen.getByText(/读输出流 · \.agent\/output\.jsonl/)).not.toBeNull(); - expect( - screen.getByText(/读上下文包 · \.agent\/context\.bundle\.json/), - ).not.toBeNull(); - const runFilesMessageList = document.querySelector('.message-list'); - expect(runFilesMessageList).not.toBeNull(); - fireEvent.click( - within(runFilesMessageList as HTMLElement).getByRole('button', { - name: '读输出流', - }), - ); - expect(await composerText()).toBe('/read .agent/output.jsonl'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(fileReadCountBeforeRunFiles); - - const fileReadCountBeforeInternals = invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length; - await submitChat('/internals'); - expect(await screen.findByText(/项目内部真相源读取命令:/)).not.toBeNull(); - expect( - screen.getByText(/读 manifest · \.agent\/manifest\.json/), - ).not.toBeNull(); - expect( - screen.getByText(/读项目对话 · \.agent\/conversations\/project\.jsonl/), - ).not.toBeNull(); - const internalDraftButtons = screen.getAllByRole('button', { - name: '读 manifest', + ).length; + await submitChat('/groups'); + expect(await screen.findByText(/专业组进度:/)).not.toBeNull(); + const groupMessages = screen.getAllByText(/专业组进度:/); + const groupMessage = messageBubble(groupMessages[groupMessages.length - 1]); + expect(groupMessage.textContent).toContain( + '美术组:完成 0/3 · active 0 · carry 0 · ready 1', + ); + expect(groupMessage.textContent).toContain('下一步 Asset 生成首版美术素材'); + expect(groupMessage.textContent).toContain('程序组:完成 0/5'); + const groupDraftButtons = screen.getAllByRole('button', { + name: '查看任务', + }); + fireEvent.click(groupDraftButtons[groupDraftButtons.length - 1]); + expect(await composerText()).toBe('/tasks'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(fileReadCountBeforeGroups); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(manifestReadCountBeforeGroups); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(runLocalCountBeforeGroups); }); - fireEvent.click(internalDraftButtons[internalDraftButtons.length - 1]); - expect(await composerText()).toBe('/read .agent/manifest.json'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(fileReadCountBeforeInternals); - const commandCountsBeforeTodo = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - }; - await submitChat('/todo'); - const todoMessages = await screen.findAllByText(/下一轮小步:/); - const todoMessage = messageBubble(todoMessages[todoMessages.length - 1]); - expect(todoMessage.textContent).toContain('项目:未命名游戏原型'); - expect(todoMessage.textContent).toContain( - 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', - ); - expect(todoMessage.textContent).toContain('编排下一步:preview'); - expect(todoMessage.textContent).toContain( - 'ready:美术组 / Asset 生成首版美术素材(art-asset-plan) · 待处理 · 验收:角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记', - ); - expect(todoMessage.textContent).toContain( - '边界:只整理下一步;不读取任务文件;不启动 run;不修改项目', - ); - expect(todoMessage.textContent).toContain('建议:/tasks'); - const todoMessageList = document.querySelector('.message-list'); - expect(todoMessageList).not.toBeNull(); - const todoDraftButtons = within(todoMessageList as HTMLElement).getAllByRole( - 'button', - { name: '查看任务' }, - ); - fireEvent.click(todoDraftButtons[todoDraftButtons.length - 1]); - expect(await composerText()).toBe('/tasks'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeTodo.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeTodo.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeTodo.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeTodo.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeTodo.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeTodo.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeTodo.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeTodo.exportList); + it('预览快捷操作:/balance', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeBalance = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + }; + await submitChat('/balance'); + expect(await screen.findByText(/数值状态:/)).not.toBeNull(); + const balanceMessages = screen.getAllByText(/数值状态:/); + const balanceMessage = messageBubble( + balanceMessages[balanceMessages.length - 1], + ); + expect(balanceMessage.textContent).toContain('项目:未命名游戏原型'); + expect(balanceMessage.textContent).toContain( + 'balance-director:Director 确定数值口径 · 待处理', + ); + expect(balanceMessage.textContent).toContain( + 'balance-seed:Difficulty 生成初版数值 · 待处理', + ); + expect(balanceMessage.textContent).toContain( + '难度、节奏和得分口径可指导数值表', + ); + expect(balanceMessage.textContent).toContain( + '速度、生命、得分和难度参数可被程序组读取', + ); + expect(balanceMessage.textContent).toContain( + '数值表:game/balance.json · 待生成', + ); + expect(balanceMessage.textContent).toContain( + '试玩关联:/playtest;/feedback', + ); + expect(balanceMessage.textContent).toContain( + '建议:/agent-resume 数值调整:前 30 秒更易上手;得分反馈更明显;失败后重开节奏更快', + ); + const balanceMessageList = document.querySelector('.message-list'); + expect(balanceMessageList).not.toBeNull(); + const balanceDraftButtons = within( + balanceMessageList as HTMLElement, + ).getAllByRole('button', { name: '填写数值反馈' }); + fireEvent.click(balanceDraftButtons[balanceDraftButtons.length - 1]); + expect(await composerText()).toBe( + '/agent-resume 数值调整:前 30 秒更易上手;得分反馈更明显;失败后重开节奏更快', + ); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeBalance.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeBalance.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeBalance.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeBalance.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeBalance.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeBalance.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeBalance.exportPackage); + }); - const commandCountsBeforePlan = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( + it('预览快捷操作:/budget', async () => { + const invoke = await openPreviewShortcutProject(); + const fileReadCountBeforeBudget = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/plan'); - const planMessages = await screen.findAllByText(/下一轮分工计划:/); - const planMessage = messageBubble(planMessages[planMessages.length - 1]); - expect(planMessage.textContent).toContain('项目:未命名游戏原型'); - expect(planMessage.textContent).toContain( - 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', - ); - expect(planMessage.textContent).toContain('编排焦点:preview'); - expect(planMessage.textContent).toContain('协作顺序:美术组'); - expect(planMessage.textContent).toContain( - '美术组:ready · Asset 生成首版美术素材(art-asset-plan) · 验收:角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记', - ); - expect(planMessage.textContent).toContain( - '空档组:设计实现组、程序组、数值组、音乐组、运营组', - ); - expect(planMessage.textContent).toContain( - '边界:只整理下一轮分工;不读取任务文件;不启动 run;不修改项目', - ); - expect(planMessage.textContent).toContain( - '建议:/agent-resume 下一轮计划:美术组 / Asset 生成首版美术素材', - ); - const planMessageList = document.querySelector('.message-list'); - expect(planMessageList).not.toBeNull(); - const planDraftButtons = within(planMessageList as HTMLElement).getAllByRole( - 'button', - { name: '继续执行计划' }, - ); - fireEvent.click(planDraftButtons[planDraftButtons.length - 1]); - expect(await composerText()).toBe( - '/agent-resume 下一轮计划:美术组 / Asset 生成首版美术素材', - ); - expect( - invoke.mock.calls.filter( + ).length; + const manifestReadCountBeforeBudget = invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforePlan.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforePlan.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforePlan.fileList); - expect( - invoke.mock.calls.filter( + ).length; + const runLocalCountBeforeBudget = invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforePlan.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforePlan.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforePlan.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforePlan.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforePlan.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforePlan.controlAgentRun); + ).length; + await submitChat('/budget'); + expect(await screen.findByText(/运行预算:/)).not.toBeNull(); + const budgetMessages = screen.getAllByText(/运行预算:/); + const budgetMessage = messageBubble( + budgetMessages[budgetMessages.length - 1], + ); + expect(budgetMessage.textContent).toContain( + 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', + ); + expect(budgetMessage.textContent).toContain('轮次:已用 1/3 · 剩余 2'); + expect(budgetMessage.textContent).toContain( + '工具调用:已用 1/128 · 剩余 127', + ); + expect(budgetMessage.textContent).toContain('下一步:preview'); + expect(budgetMessage.textContent).toContain('建议:/publish'); + const budgetDraftButtons = screen.getAllByRole('button', { + name: '查看发布准备', + }); + fireEvent.click(budgetDraftButtons[budgetDraftButtons.length - 1]); + expect(await composerText()).toBe('/publish'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(fileReadCountBeforeBudget); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(manifestReadCountBeforeBudget); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(runLocalCountBeforeBudget); + }); - const commandCountsBeforeGuide = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( + it('预览快捷操作:/qa', async () => { + const invoke = await openPreviewShortcutProject(); + const fileReadCountBeforeQa = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/guide'); - const guideMessages = await screen.findAllByText(/使用导引:/); - const guideMessage = messageBubble(guideMessages[guideMessages.length - 1]); - expect(guideMessage.textContent).toContain('项目:未命名游戏原型'); - expect(guideMessage.textContent).toContain('当前阶段:可导出'); - expect(guideMessage.textContent).toContain( - '现在先做:先运行自检并启动本地预览,通过后再导出试玩包。', - ); - expect(guideMessage.textContent).toContain( - '推荐命令:/run / /test-plan / /share', - ); - expect(guideMessage.textContent).toContain( - '边界:只给操作导引;不读取文件;不启动 run;不启动预览;不写项目', - ); - const guideMessageList = document.querySelector('.message-list'); - expect(guideMessageList).not.toBeNull(); - const guideDraftButtons = within( - guideMessageList as HTMLElement, - ).getAllByRole('button', { name: '执行导引建议' }); - fireEvent.click(guideDraftButtons[guideDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( + ).length; + const manifestReadCountBeforeQa = invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeGuide.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeGuide.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeGuide.fileList); - expect( - invoke.mock.calls.filter( + ).length; + const runLocalCountBeforeQa = invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeGuide.runLocal); - expect( - invoke.mock.calls.filter( + ).length; + const startPreviewCountBeforeQa = invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeGuide.startPreview); - expect( - invoke.mock.calls.filter( + ).length; + const openPreviewCountBeforeQa = invoke.mock.calls.filter( ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeGuide.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeGuide.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeGuide.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeGuide.controlAgentRun); + ).length; + await submitChat('/qa'); + expect(await screen.findByText(/质量检查:/)).not.toBeNull(); + const qaMessages = screen.getAllByText(/质量检查:/); + const qaMessage = messageBubble(qaMessages[qaMessages.length - 1]); + expect(qaMessage.textContent).toContain( + 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', + ); + expect(qaMessage.textContent).toContain('Evaluator:通过 · 质量评审通过'); + expect(qaMessage.textContent).toContain( + '任务:完成 0/16 · ready 3 · 失败 0', + ); + expect(qaMessage.textContent).toContain('静态自检:通过'); + expect(qaMessage.textContent).toContain('试玩:待启动预览'); + expect(qaMessage.textContent).toContain('产物:3 个'); + expect(qaMessage.textContent).toContain('建议:/playtest'); + const qaDraftButtons = screen.getAllByRole('button', { + name: '查看试玩状态', + }); + fireEvent.click(qaDraftButtons[qaDraftButtons.length - 1]); + expect(await composerText()).toBe('/playtest'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(fileReadCountBeforeQa); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(manifestReadCountBeforeQa); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(runLocalCountBeforeQa); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(startPreviewCountBeforeQa); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(openPreviewCountBeforeQa); + }); - const commandCountsBeforeProgress = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( + it('预览快捷操作:/changes', async () => { + const invoke = await openPreviewShortcutProject(); + const fileReadCountBeforeChanges = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/progress'); - const progressMessages = await screen.findAllByText(/项目进度:/); - const progressMessage = messageBubble( - progressMessages[progressMessages.length - 1], - ); - expect(progressMessage.textContent).toContain('项目:未命名游戏原型'); - expect(progressMessage.textContent).toContain('当前阶段:已生成'); - expect(progressMessage.textContent).toMatch( - /任务完成度:0\/\d+ · 0% · ready 1 · 失败 0/, - ); - expect(progressMessage.textContent).toContain( - '最近 Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', - ); - expect(progressMessage.textContent).toContain('预览:未启动'); - expect(progressMessage.textContent).toContain( - '素材:共 2 个 · 美术 1 · 音频 0', - ); - expect(progressMessage.textContent).toContain( - '交付:自检 已通过 · 试玩包 未导出', - ); - expect(progressMessage.textContent).toContain( - '边界:只整理项目进度;不读取文件;不启动 run;不启动预览;不导出试玩包;不写项目', - ); - expect(progressMessage.textContent).toContain('建议:/run'); - const progressMessageList = document.querySelector('.message-list'); - expect(progressMessageList).not.toBeNull(); - const progressDraftButtons = within( - progressMessageList as HTMLElement, - ).getAllByRole('button', { name: '启动预览' }); - fireEvent.click(progressDraftButtons[progressDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( + ).length; + const manifestReadCountBeforeChanges = invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeProgress.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeProgress.fileRead); - expect( - invoke.mock.calls.filter( + ).length; + const fileListCountBeforeChanges = invoke.mock.calls.filter( ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeProgress.fileList); - expect( - invoke.mock.calls.filter( + ).length; + const runLocalCountBeforeChanges = invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeProgress.runLocal); - expect( - invoke.mock.calls.filter( + ).length; + const startPreviewCountBeforeChanges = invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeProgress.startPreview); - expect( - invoke.mock.calls.filter( + ).length; + const openPreviewCountBeforeChanges = invoke.mock.calls.filter( ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeProgress.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeProgress.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeProgress.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeProgress.controlAgentRun); + ).length; + await submitChat('/changes'); + expect(await screen.findByText(/最近变更:/)).not.toBeNull(); + const changeMessages = screen.getAllByText(/最近变更:/); + const changeMessage = messageBubble( + changeMessages[changeMessages.length - 1], + ); + expect(changeMessage.textContent).toContain( + 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', + ); + expect(changeMessage.textContent).toContain('可验产物:3 个'); + expect(changeMessage.textContent).toContain('exports/README.md · 128B'); + expect(changeMessage.textContent).toContain( + '.agent/passes/pass-1/agenda.md · 96B', + ); + expect(changeMessage.textContent).toContain( + 'Generator #1 · completed · .agent/passes/pass-1/draft.json', + ); + expect(changeMessage.textContent).toContain( + '最近命令:game.static_smoke · 完成', + ); + expect(changeMessage.textContent).toContain( + '真实差异:/checkpoints 后 /diff checkpoint-id', + ); + expect(changeMessage.textContent).toContain( + '建议:/read exports/README.md', + ); + const changeDraftButtons = screen.getAllByRole('button', { + name: '读取首个产物', + }); + fireEvent.click(changeDraftButtons[changeDraftButtons.length - 1]); + expect(await composerText()).toBe('/read exports/README.md'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(fileReadCountBeforeChanges); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(manifestReadCountBeforeChanges); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(fileListCountBeforeChanges); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(runLocalCountBeforeChanges); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(startPreviewCountBeforeChanges); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(openPreviewCountBeforeChanges); + }); + + it('预览快捷操作:/review', async () => { + const invoke = await openPreviewShortcutProject(); + const fileReadCountBeforeReview = invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length; + const manifestReadCountBeforeReview = invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length; + const runLocalCountBeforeReview = invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length; + const startPreviewCountBeforeReview = invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length; + const openPreviewCountBeforeReview = invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length; + await submitChat('/review'); + expect(await screen.findByText(/评审状态:/)).not.toBeNull(); + const reviewMessages = screen.getAllByText(/评审状态:/); + const reviewMessage = messageBubble( + reviewMessages[reviewMessages.length - 1], + ); + expect(reviewMessage.textContent).toContain('Evaluator:通过'); + expect(reviewMessage.textContent).toContain('返工焦点:暂无'); + expect(reviewMessage.textContent).toContain( + '评审记录:/read .agent/findings.md', + ); + expect(reviewMessage.textContent).toContain( + 'Evaluator #1 · passed · 质量评审通过', + ); + fireEvent.click(screen.getByRole('button', { name: '读取评审记录' })); + expect(await composerText()).toBe('/read .agent/findings.md'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(fileReadCountBeforeReview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(manifestReadCountBeforeReview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(runLocalCountBeforeReview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(startPreviewCountBeforeReview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(openPreviewCountBeforeReview); + }); + + it('预览快捷操作:/context', async () => { + const invoke = await openPreviewShortcutProject(); + const fileReadCountBeforeContext = invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length; + await submitChat('/context'); + expect(await screen.findByText(/上下文来源:/)).not.toBeNull(); + expect(screen.getByText(/项目对话:\/history/)).not.toBeNull(); + expect(screen.getByText(/项目黑板:\/memory blackboard/)).not.toBeNull(); + expect(screen.getByText(/Agent 私有记忆:16 个/)).not.toBeNull(); + expect( + screen.getByText(/最近 Run:run-main-shortcut-trace · \/trace/), + ).not.toBeNull(); + expect( + screen.getByText(/\.agent\/spec\.md:\/read \.agent\/spec\.md/), + ).not.toBeNull(); + const contextDraftButtons = screen.getAllByRole('button', { + name: '读取首个上下文', + }); + fireEvent.click(contextDraftButtons[contextDraftButtons.length - 1]); + expect(await composerText()).toBe('/read .agent/spec.md'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(fileReadCountBeforeContext); + }); + + it('预览快捷操作:/timeline', async () => { + const invoke = await openPreviewShortcutProject(); + const fileReadCountBeforeTimeline = invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length; + const manifestReadCountBeforeTimeline = invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length; + const runLocalCountBeforeTimeline = invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length; + const startPreviewCountBeforeTimeline = invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length; + const openPreviewCountBeforeTimeline = invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length; + await submitChat('/timeline'); + expect(await screen.findByText(/项目时间线:/)).not.toBeNull(); + const timelineMessages = screen.getAllByText(/项目时间线:/); + const timelineMessage = messageBubble( + timelineMessages[timelineMessages.length - 1], + ); + expect(timelineMessage.textContent).toContain( + 'Run:run-main-shortcut-trace · passed / done', + ); + expect(timelineMessage.textContent).toContain( + '命令 game.static_smoke · 完成 · 日志 /read .agent/logs/command.log', + ); + expect(timelineMessage.textContent).toContain( + 'Generator #1 / generate · completed · 生成可运行草案 · 输出 .agent/passes/pass-1/draft.json', + ); + expect(timelineMessage.textContent).toContain( + 'Evaluator #1 / evaluation · passed · 质量评审通过', + ); + const timelineDraftButtons = screen.getAllByRole('button', { + name: '读取最近日志', + }); + fireEvent.click(timelineDraftButtons[timelineDraftButtons.length - 1]); + expect(await composerText()).toBe('/read .agent/logs/command.log'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(fileReadCountBeforeTimeline); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(manifestReadCountBeforeTimeline); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(runLocalCountBeforeTimeline); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(startPreviewCountBeforeTimeline); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(openPreviewCountBeforeTimeline); + }); + + it('预览快捷操作:/handoff', async () => { + const invoke = await openPreviewShortcutProject(); + const manifestReadCountBeforeHandoff = invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length; + const fileReadCountBeforeHandoff = invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length; + const runLocalCountBeforeHandoff = invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length; + await submitChat('/handoff'); + expect(await screen.findByText(/项目交接:/)).not.toBeNull(); + const handoffMessages = screen.getAllByText(/项目交接:/); + const handoffMessage = messageBubble( + handoffMessages[handoffMessages.length - 1], + ); + expect(handoffMessage.textContent).toContain( + 'Run:run-main-shortcut-trace', + ); + expect(handoffMessage.textContent).toContain( + 'Ready:art/Asset 生成首版美术素材', + ); + expect(handoffMessage.textContent).toContain('历史:已加载 0 个 run'); + const nextActionButtons = screen.getAllByRole('button', { + name: '查看下一步', + }); + fireEvent.click(nextActionButtons[nextActionButtons.length - 1]); + expect(await composerText()).toBe('/next'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(manifestReadCountBeforeHandoff); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(fileReadCountBeforeHandoff); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(runLocalCountBeforeHandoff); + }); + + it('预览快捷操作:/runs', async () => { + const invoke = await openPreviewShortcutProject(); + const fileReadCountBeforeRuns = invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length; + await submitChat('/runs'); + expect(await screen.findByText(/Run 历史读取命令:/)).not.toBeNull(); + expect( + screen.getByText(/当前指针 run-main-shortcut-trace .*:\/trace/), + ).not.toBeNull(); + fireEvent.click(screen.getByRole('button', { name: '查看最近 Run' })); + expect(await composerText()).toBe('/trace'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(fileReadCountBeforeRuns); + }); + + it('预览快捷操作:/run-files', async () => { + const invoke = await openPreviewShortcutProject(); + const fileReadCountBeforeRunFiles = invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length; + await submitChat('/run-files'); + expect( + await screen.findByText(/Agent 运行辅助文件读取命令:/), + ).not.toBeNull(); + expect( + screen.getByText(/读输出流 · \.agent\/output\.jsonl/), + ).not.toBeNull(); + expect( + screen.getByText(/读上下文包 · \.agent\/context\.bundle\.json/), + ).not.toBeNull(); + const runFilesMessageList = document.querySelector('.message-list'); + expect(runFilesMessageList).not.toBeNull(); + fireEvent.click( + within(runFilesMessageList as HTMLElement).getByRole('button', { + name: '读输出流', + }), + ); + expect(await composerText()).toBe('/read .agent/output.jsonl'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(fileReadCountBeforeRunFiles); + }); + + it('预览快捷操作:/internals', async () => { + const invoke = await openPreviewShortcutProject(); + const fileReadCountBeforeInternals = invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length; + await submitChat('/internals'); + expect(await screen.findByText(/项目内部真相源读取命令:/)).not.toBeNull(); + expect( + screen.getByText(/读 manifest · \.agent\/manifest\.json/), + ).not.toBeNull(); + expect( + screen.getByText(/读项目对话 · \.agent\/conversations\/project\.jsonl/), + ).not.toBeNull(); + const internalDraftButtons = screen.getAllByRole('button', { + name: '读 manifest', + }); + fireEvent.click(internalDraftButtons[internalDraftButtons.length - 1]); + expect(await composerText()).toBe('/read .agent/manifest.json'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(fileReadCountBeforeInternals); + }); + + it('预览快捷操作:/todo', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeTodo = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + }; + await submitChat('/todo'); + const todoMessages = await screen.findAllByText(/下一轮小步:/); + const todoMessage = messageBubble(todoMessages[todoMessages.length - 1]); + expect(todoMessage.textContent).toContain('项目:未命名游戏原型'); + expect(todoMessage.textContent).toContain( + 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', + ); + expect(todoMessage.textContent).toContain('编排下一步:preview'); + expect(todoMessage.textContent).toContain( + 'ready:美术组 / Asset 生成首版美术素材(art-asset-plan) · 待处理 · 验收:角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记', + ); + expect(todoMessage.textContent).toContain( + '边界:只整理下一步;不读取任务文件;不启动 run;不修改项目', + ); + expect(todoMessage.textContent).toContain('建议:/tasks'); + const todoMessageList = document.querySelector('.message-list'); + expect(todoMessageList).not.toBeNull(); + const todoDraftButtons = within( + todoMessageList as HTMLElement, + ).getAllByRole('button', { name: '查看任务' }); + fireEvent.click(todoDraftButtons[todoDraftButtons.length - 1]); + expect(await composerText()).toBe('/tasks'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeTodo.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeTodo.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeTodo.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeTodo.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeTodo.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeTodo.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeTodo.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeTodo.exportList); + }); + + it('预览快捷操作:/plan', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforePlan = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/plan'); + const planMessages = await screen.findAllByText(/下一轮分工计划:/); + const planMessage = messageBubble(planMessages[planMessages.length - 1]); + expect(planMessage.textContent).toContain('项目:未命名游戏原型'); + expect(planMessage.textContent).toContain( + 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', + ); + expect(planMessage.textContent).toContain('编排焦点:preview'); + expect(planMessage.textContent).toContain('协作顺序:美术组'); + expect(planMessage.textContent).toContain( + '美术组:ready · Asset 生成首版美术素材(art-asset-plan) · 验收:角色、场景、UI 和动画需求已映射到画板或本地资产,且至少一张首版核心素材图已生成并登记', + ); + expect(planMessage.textContent).toContain( + '空档组:设计实现组、程序组、数值组、音乐组、运营组', + ); + expect(planMessage.textContent).toContain( + '边界:只整理下一轮分工;不读取任务文件;不启动 run;不修改项目', + ); + expect(planMessage.textContent).toContain( + '建议:/agent-resume 下一轮计划:美术组 / Asset 生成首版美术素材', + ); + const planMessageList = document.querySelector('.message-list'); + expect(planMessageList).not.toBeNull(); + const planDraftButtons = within( + planMessageList as HTMLElement, + ).getAllByRole('button', { name: '继续执行计划' }); + fireEvent.click(planDraftButtons[planDraftButtons.length - 1]); + expect(await composerText()).toBe( + '/agent-resume 下一轮计划:美术组 / Asset 生成首版美术素材', + ); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforePlan.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforePlan.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforePlan.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforePlan.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforePlan.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforePlan.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforePlan.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforePlan.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforePlan.controlAgentRun); + }); + + it('预览快捷操作:/guide', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeGuide = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/guide'); + const guideMessages = await screen.findAllByText(/使用导引:/); + const guideMessage = messageBubble(guideMessages[guideMessages.length - 1]); + expect(guideMessage.textContent).toContain('项目:未命名游戏原型'); + expect(guideMessage.textContent).toContain('当前阶段:可导出'); + expect(guideMessage.textContent).toContain( + '现在先做:先运行自检并启动本地预览,通过后再导出试玩包。', + ); + expect(guideMessage.textContent).toContain( + '推荐命令:/run / /test-plan / /share', + ); + expect(guideMessage.textContent).toContain( + '边界:只给操作导引;不读取文件;不启动 run;不启动预览;不写项目', + ); + const guideMessageList = document.querySelector('.message-list'); + expect(guideMessageList).not.toBeNull(); + const guideDraftButtons = within( + guideMessageList as HTMLElement, + ).getAllByRole('button', { name: '执行导引建议' }); + fireEvent.click(guideDraftButtons[guideDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeGuide.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeGuide.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeGuide.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeGuide.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeGuide.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeGuide.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeGuide.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeGuide.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforeGuide.controlAgentRun); + }); + + it('预览快捷操作:/progress', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeProgress = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/progress'); + const progressMessages = await screen.findAllByText(/项目进度:/); + const progressMessage = messageBubble( + progressMessages[progressMessages.length - 1], + ); + expect(progressMessage.textContent).toContain('项目:未命名游戏原型'); + expect(progressMessage.textContent).toContain('当前阶段:已生成'); + expect(progressMessage.textContent).toMatch( + /任务完成度:0\/\d+ · 0% · ready 1 · 失败 0/, + ); + expect(progressMessage.textContent).toContain( + '最近 Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', + ); + expect(progressMessage.textContent).toContain('预览:未启动'); + expect(progressMessage.textContent).toContain( + '素材:共 2 个 · 美术 1 · 音频 0', + ); + expect(progressMessage.textContent).toContain( + '交付:自检 已通过 · 试玩包 未导出', + ); + expect(progressMessage.textContent).toContain( + '边界:只整理项目进度;不读取文件;不启动 run;不启动预览;不导出试玩包;不写项目', + ); + expect(progressMessage.textContent).toContain('建议:/run'); + const progressMessageList = document.querySelector('.message-list'); + expect(progressMessageList).not.toBeNull(); + const progressDraftButtons = within( + progressMessageList as HTMLElement, + ).getAllByRole('button', { name: '启动预览' }); + fireEvent.click(progressDraftButtons[progressDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeProgress.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeProgress.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeProgress.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeProgress.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeProgress.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeProgress.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeProgress.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeProgress.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforeProgress.controlAgentRun); + }); } diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/assert-playtest-and-release-shortcuts.ts b/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/assert-playtest-and-release-shortcuts.ts index 2467320d1..891f0dce9 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/assert-playtest-and-release-shortcuts.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/assert-playtest-and-release-shortcuts.ts @@ -2,1806 +2,1790 @@ import { composerText, expect, fireEvent, + it, screen, submitChat, within, } from '../../harness'; -import type { PreviewShortcutInvoke } from './invoke-mock'; +import { openPreviewShortcutProject } from './actions'; import { messageBubble } from './message-bubble'; -export async function assertPlaytestAndReleaseShortcutFlow( - invoke: PreviewShortcutInvoke, -) { - const runLocalCountBeforeNext = invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length; - await submitChat('/next'); - expect(await screen.findByText(/下一步建议:/)).not.toBeNull(); - expect(screen.getByText(/运行自检并启动本地预览:\/run/)).not.toBeNull(); - expect(screen.getByText(/查看创作目标:\/goal/)).not.toBeNull(); - expect(screen.getByText(/查看普通用户操作导引:\/guide/)).not.toBeNull(); - expect(screen.getByText(/查看项目进度:\/progress/)).not.toBeNull(); - expect(screen.getByText(/查看创作规格包:\/spec/)).not.toBeNull(); - expect(screen.getByText(/查看本轮 MVP 范围:\/mvp/)).not.toBeNull(); - expect(screen.getByText(/查看试玩定位与卖点:\/pitch/)).not.toBeNull(); - expect(screen.getByText(/准备 30 秒试玩讲解稿:\/demo/)).not.toBeNull(); - expect(screen.getByText(/查看玩法操作与规则:\/rules/)).not.toBeNull(); - expect(screen.getByText(/查看新手引导检查:\/tutorial/)).not.toBeNull(); - expect(screen.getByText(/查看移动试玩检查:\/mobile/)).not.toBeNull(); - expect(screen.getByText(/准备兼容性说明:\/compatibility/)).not.toBeNull(); - expect( - screen.getByText(/查看可读性与无障碍检查:\/accessibility/), - ).not.toBeNull(); - expect( - screen.getByText(/查看本地化与文案检查:\/localization/), - ).not.toBeNull(); - expect(screen.getByText(/查看性能与加载检查:\/performance/)).not.toBeNull(); - expect(screen.getByText(/查看试玩前打磨清单:\/polish/)).not.toBeNull(); - expect(screen.getByText(/查看数值与难度口径:\/balance/)).not.toBeNull(); - expect(screen.getByText(/查看 .* 个 ready 任务:\/tasks/)).not.toBeNull(); - expect(screen.getByText(/查看当前任务验收标准:\/criteria/)).not.toBeNull(); - expect(screen.getByText(/查看专业组进度:\/groups/)).not.toBeNull(); - expect(screen.getByText(/查看质量检查清单:\/qa/)).not.toBeNull(); - expect(screen.getByText(/查看最近生成变更:\/changes/)).not.toBeNull(); - expect(screen.getByText(/查看下一轮分工计划:\/plan/)).not.toBeNull(); - expect(screen.getByText(/查看下一轮小步清单:\/todo/)).not.toBeNull(); - expect(screen.getByText(/查看当前阻塞项:\/blockers/)).not.toBeNull(); - expect(screen.getByText(/查看试玩就绪度:\/ready/)).not.toBeNull(); - expect(screen.getByText(/查看验证证据台账:\/evidence/)).not.toBeNull(); - expect( - screen.getByText(/查看 Agent 智能服务状态:\/llm-routes/), - ).not.toBeNull(); - expect(screen.getByText(/查看任务依赖链:\/deps/)).not.toBeNull(); - expect(screen.getByText(/准备下一轮改版说明:\/revise/)).not.toBeNull(); - expect(screen.getByText(/查看隐私与导出边界:\/privacy/)).not.toBeNull(); - expect(screen.getByText(/查看首批试玩对象:\/audience/)).not.toBeNull(); - expect(screen.getByText(/准备试玩邀请文案:\/invite/)).not.toBeNull(); - expect(screen.getByText(/准备试玩问卷问题:\/survey/)).not.toBeNull(); - expect(screen.getByText(/准备封面与缩略图检查:\/cover/)).not.toBeNull(); - expect(screen.getByText(/准备宣传截图清单:\/screenshots/)).not.toBeNull(); - expect(screen.getByText(/准备试玩短视频脚本:\/trailer/)).not.toBeNull(); - expect(screen.getByText(/准备试玩常见问答:\/faq/)).not.toBeNull(); - expect(screen.getByText(/准备社区发布文案:\/post/)).not.toBeNull(); - expect(screen.getByText(/准备上架资料清单:\/store/)).not.toBeNull(); - expect(screen.getByText(/准备媒体资料包清单:\/media-kit/)).not.toBeNull(); - expect(screen.getByText(/准备试玩更新说明:\/release-notes/)).not.toBeNull(); - expect(screen.getByText(/准备已知问题清单:\/known-issues/)).not.toBeNull(); - expect(screen.getByText(/查看素材署名与来源:\/credits/)).not.toBeNull(); - expect(screen.getByText(/查看美术素材:\/art/)).not.toBeNull(); - expect(screen.getByText(/查看发布准备清单:\/publish/)).not.toBeNull(); - expect(screen.getByText(/准备作品页文案清单:\/listing/)).not.toBeNull(); - expect(screen.getByText(/查看试玩状态:\/playtest/)).not.toBeNull(); - expect(screen.getByText(/准备手动测试计划:\/test-plan/)).not.toBeNull(); - expect(screen.getByText(/准备试玩反馈:\/feedback/)).not.toBeNull(); - expect(screen.getByText(/准备复玩观察清单:\/retention/)).not.toBeNull(); - expect(screen.getByText(/准备试玩交付清单:\/share/)).not.toBeNull(); - expect(screen.getByText(/查看最近 run 预算:\/budget/)).not.toBeNull(); - expect(screen.getByText(/查看评审和返工焦点:\/review/)).not.toBeNull(); - expect(screen.getByText(/查看生成上下文来源:\/context/)).not.toBeNull(); - expect(screen.getByText(/查看项目活动时间线:\/timeline/)).not.toBeNull(); - expect( - screen.getByText(/列出内部真相源读取命令:\/internals/), - ).not.toBeNull(); - expect(screen.getByText(/列出 Agent 轮次产物:\/passes/)).not.toBeNull(); - expect( - screen.getByText(/列出 Agent 运行辅助文件:\/run-files/), - ).not.toBeNull(); - const nextMessageList = document.querySelector('.message-list'); - expect(nextMessageList).not.toBeNull(); - fireEvent.click( - within(nextMessageList as HTMLElement).getByRole('button', { - name: '运行自检并启动本地预览', - }), - ); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( +export function registerPlaytestAndReleaseShortcutTests() { + it('预览快捷操作:/next', async () => { + const invoke = await openPreviewShortcutProject(); + const runLocalCountBeforeNext = invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(runLocalCountBeforeNext); - - const manifestReadCountBeforePublish = invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length; - const fileReadCountBeforePublish = invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length; - const runLocalCountBeforePublish = invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length; - const startPreviewCountBeforePublish = invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length; - await submitChat('/publish'); - expect(await screen.findByText(/发布准备:/)).not.toBeNull(); - expect( - screen.getByText(/原型:最近 run 已通过 run-main-shortcut-trace/), - ).not.toBeNull(); - expect(screen.getByText(/预览:未启动 · 建议 \/run/)).not.toBeNull(); - expect(screen.getByText(/音频:暂无 · 建议 \/audio/)).not.toBeNull(); - expect( - screen.getByText( - /包装:最近 Run 包含 exports\/README\.md · \/read exports\/README\.md/, - ), - ).not.toBeNull(); - const publishDraftButtons = screen.getAllByRole('button', { - name: '启动预览', + ).length; + await submitChat('/next'); + expect(await screen.findByText(/下一步建议:/)).not.toBeNull(); + expect(screen.getByText(/运行自检并启动本地预览:\/run/)).not.toBeNull(); + expect(screen.getByText(/查看创作目标:\/goal/)).not.toBeNull(); + expect(screen.getByText(/查看普通用户操作导引:\/guide/)).not.toBeNull(); + expect(screen.getByText(/查看项目进度:\/progress/)).not.toBeNull(); + expect(screen.getByText(/查看创作规格包:\/spec/)).not.toBeNull(); + expect(screen.getByText(/查看本轮 MVP 范围:\/mvp/)).not.toBeNull(); + expect(screen.getByText(/查看试玩定位与卖点:\/pitch/)).not.toBeNull(); + expect(screen.getByText(/准备 30 秒试玩讲解稿:\/demo/)).not.toBeNull(); + expect(screen.getByText(/查看玩法操作与规则:\/rules/)).not.toBeNull(); + expect(screen.getByText(/查看新手引导检查:\/tutorial/)).not.toBeNull(); + expect(screen.getByText(/查看移动试玩检查:\/mobile/)).not.toBeNull(); + expect(screen.getByText(/准备兼容性说明:\/compatibility/)).not.toBeNull(); + expect( + screen.getByText(/查看可读性与无障碍检查:\/accessibility/), + ).not.toBeNull(); + expect( + screen.getByText(/查看本地化与文案检查:\/localization/), + ).not.toBeNull(); + expect( + screen.getByText(/查看性能与加载检查:\/performance/), + ).not.toBeNull(); + expect(screen.getByText(/查看试玩前打磨清单:\/polish/)).not.toBeNull(); + expect(screen.getByText(/查看数值与难度口径:\/balance/)).not.toBeNull(); + expect(screen.getByText(/查看 .* 个 ready 任务:\/tasks/)).not.toBeNull(); + expect(screen.getByText(/查看当前任务验收标准:\/criteria/)).not.toBeNull(); + expect(screen.getByText(/查看专业组进度:\/groups/)).not.toBeNull(); + expect(screen.getByText(/查看质量检查清单:\/qa/)).not.toBeNull(); + expect(screen.getByText(/查看最近生成变更:\/changes/)).not.toBeNull(); + expect(screen.getByText(/查看下一轮分工计划:\/plan/)).not.toBeNull(); + expect(screen.getByText(/查看下一轮小步清单:\/todo/)).not.toBeNull(); + expect(screen.getByText(/查看当前阻塞项:\/blockers/)).not.toBeNull(); + expect(screen.getByText(/查看试玩就绪度:\/ready/)).not.toBeNull(); + expect(screen.getByText(/查看验证证据台账:\/evidence/)).not.toBeNull(); + expect( + screen.getByText(/查看 Agent 智能服务状态:\/llm-routes/), + ).not.toBeNull(); + expect(screen.getByText(/查看任务依赖链:\/deps/)).not.toBeNull(); + expect(screen.getByText(/准备下一轮改版说明:\/revise/)).not.toBeNull(); + expect(screen.getByText(/查看隐私与导出边界:\/privacy/)).not.toBeNull(); + expect(screen.getByText(/查看首批试玩对象:\/audience/)).not.toBeNull(); + expect(screen.getByText(/准备试玩邀请文案:\/invite/)).not.toBeNull(); + expect(screen.getByText(/准备试玩问卷问题:\/survey/)).not.toBeNull(); + expect(screen.getByText(/准备封面与缩略图检查:\/cover/)).not.toBeNull(); + expect(screen.getByText(/准备宣传截图清单:\/screenshots/)).not.toBeNull(); + expect(screen.getByText(/准备试玩短视频脚本:\/trailer/)).not.toBeNull(); + expect(screen.getByText(/准备试玩常见问答:\/faq/)).not.toBeNull(); + expect(screen.getByText(/准备社区发布文案:\/post/)).not.toBeNull(); + expect(screen.getByText(/准备上架资料清单:\/store/)).not.toBeNull(); + expect(screen.getByText(/准备媒体资料包清单:\/media-kit/)).not.toBeNull(); + expect( + screen.getByText(/准备试玩更新说明:\/release-notes/), + ).not.toBeNull(); + expect(screen.getByText(/准备已知问题清单:\/known-issues/)).not.toBeNull(); + expect(screen.getByText(/查看素材署名与来源:\/credits/)).not.toBeNull(); + expect(screen.getByText(/查看美术素材:\/art/)).not.toBeNull(); + expect(screen.getByText(/查看发布准备清单:\/publish/)).not.toBeNull(); + expect(screen.getByText(/准备作品页文案清单:\/listing/)).not.toBeNull(); + expect(screen.getByText(/查看试玩状态:\/playtest/)).not.toBeNull(); + expect(screen.getByText(/准备手动测试计划:\/test-plan/)).not.toBeNull(); + expect(screen.getByText(/准备试玩反馈:\/feedback/)).not.toBeNull(); + expect(screen.getByText(/准备复玩观察清单:\/retention/)).not.toBeNull(); + expect(screen.getByText(/准备试玩交付清单:\/share/)).not.toBeNull(); + expect(screen.getByText(/查看最近 run 预算:\/budget/)).not.toBeNull(); + expect(screen.getByText(/查看评审和返工焦点:\/review/)).not.toBeNull(); + expect(screen.getByText(/查看生成上下文来源:\/context/)).not.toBeNull(); + expect(screen.getByText(/查看项目活动时间线:\/timeline/)).not.toBeNull(); + expect( + screen.getByText(/列出内部真相源读取命令:\/internals/), + ).not.toBeNull(); + expect(screen.getByText(/列出 Agent 轮次产物:\/passes/)).not.toBeNull(); + expect( + screen.getByText(/列出 Agent 运行辅助文件:\/run-files/), + ).not.toBeNull(); + const nextMessageList = document.querySelector('.message-list'); + expect(nextMessageList).not.toBeNull(); + fireEvent.click( + within(nextMessageList as HTMLElement).getByRole('button', { + name: '运行自检并启动本地预览', + }), + ); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(runLocalCountBeforeNext); }); - fireEvent.click(publishDraftButtons[publishDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(manifestReadCountBeforePublish); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(fileReadCountBeforePublish); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(runLocalCountBeforePublish); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(startPreviewCountBeforePublish); - const commandCountsBeforeListing = { - manifestRead: invoke.mock.calls.filter( + it('预览快捷操作:/publish', async () => { + const invoke = await openPreviewShortcutProject(); + const manifestReadCountBeforePublish = invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( + ).length; + const fileReadCountBeforePublish = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( + ).length; + const runLocalCountBeforePublish = invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( + ).length; + const startPreviewCountBeforePublish = invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - }; - await submitChat('/listing'); - const listingMessages = await screen.findAllByText(/作品页草稿:/); - const listingMessage = messageBubble( - listingMessages[listingMessages.length - 1], - ); - expect(listingMessage.textContent).toContain('标题:未命名游戏原型'); - expect(listingMessage.textContent).toContain( - '一句话卖点:做一个厨房弹幕游戏', - ); - expect(listingMessage.textContent).toContain( - 'publish-strategy:Director 整理运营定位 · 待处理', - ); - expect(listingMessage.textContent).toContain( - 'publish-package:Publish 整理发布包装 · 待处理', - ); - expect(listingMessage.textContent).toContain( - '封面素材:1 个视觉素材 · 可用 1 个画板 / 生成来源', - ); - expect(listingMessage.textContent).toContain( - '说明文案:exports/README.md · 已生成', - ); - expect(listingMessage.textContent).toContain( - '标签口径:玩法类型;视觉风格;难度 / 节奏;本地可玩', - ); - expect(listingMessage.textContent).toContain( - '边界:只整理作品页文案和封面需求;不上传云端;不发布作品', - ); - expect(listingMessage.textContent).toContain('建议:/read exports/README.md'); - const listingMessageList = document.querySelector('.message-list'); - expect(listingMessageList).not.toBeNull(); - const listingDraftButtons = within( - listingMessageList as HTMLElement, - ).getAllByRole('button', { name: '读发布说明' }); - fireEvent.click(listingDraftButtons[listingDraftButtons.length - 1]); - expect(await composerText()).toBe('/read exports/README.md'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeListing.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeListing.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeListing.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeListing.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeListing.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeListing.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeListing.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeListing.exportList); - - const fileReadCountBeforePlaytest = invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length; - const manifestReadCountBeforePlaytest = invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length; - const runLocalCountBeforePlaytest = invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length; - const startPreviewCountBeforePlaytest = invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length; - const openPreviewCountBeforePlaytest = invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length; - await submitChat('/playtest'); - const playtestMessages = await screen.findAllByText(/试玩状态:/); - const playtestMessage = messageBubble( - playtestMessages[playtestMessages.length - 1], - ); - expect(playtestMessage.textContent).toContain( - '原型:最近 run 已通过 run-main-shortcut-trace', - ); - expect(playtestMessage.textContent).toContain('预览:未启动 · 建议 /run'); - expect(playtestMessage.textContent).toContain( - 'Playtest 任务:程序组 / Playtest 预览并试玩验收 · 待处理', - ); - expect(playtestMessage.textContent).toContain( - '试玩日志:/read .agent/logs/preview.log', - ); - const playtestDraftButtons = screen.getAllByRole('button', { - name: '启动试玩', + ).length; + await submitChat('/publish'); + expect(await screen.findByText(/发布准备:/)).not.toBeNull(); + expect( + screen.getByText(/原型:最近 run 已通过 run-main-shortcut-trace/), + ).not.toBeNull(); + expect(screen.getByText(/预览:未启动 · 建议 \/run/)).not.toBeNull(); + expect(screen.getByText(/音频:暂无 · 建议 \/audio/)).not.toBeNull(); + expect( + screen.getByText( + /包装:最近 Run 包含 exports\/README\.md · \/read exports\/README\.md/, + ), + ).not.toBeNull(); + const publishDraftButtons = screen.getAllByRole('button', { + name: '启动预览', + }); + fireEvent.click(publishDraftButtons[publishDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(manifestReadCountBeforePublish); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(fileReadCountBeforePublish); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(runLocalCountBeforePublish); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(startPreviewCountBeforePublish); }); - fireEvent.click(playtestDraftButtons[playtestDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(fileReadCountBeforePlaytest); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(manifestReadCountBeforePlaytest); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(runLocalCountBeforePlaytest); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(startPreviewCountBeforePlaytest); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(openPreviewCountBeforePlaytest); - const commandCountsBeforeTestPlan = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - }; - await submitChat('/test-plan'); - const testPlanMessages = await screen.findAllByText(/手动测试计划:/); - const testPlanMessage = messageBubble( - testPlanMessages[testPlanMessages.length - 1], - ); - expect(testPlanMessage.textContent).toContain('项目:未命名游戏原型'); - expect(testPlanMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); - expect(testPlanMessage.textContent).toContain( - '当前证据:最近 run 已通过 run-main-shortcut-trace;预览 未启动;入口 未见 trace 产物', - ); - expect(testPlanMessage.textContent).toContain( - '启动预览:/run 后确认首屏不空白', - ); - expect(testPlanMessage.textContent).toContain( - '30 秒理解:目标、操作、得分/失败和重开可见', - ); - expect(testPlanMessage.textContent).toContain( - '输入验证:键盘/点击/触屏至少一种可完成核心动作', - ); - expect(testPlanMessage.textContent).toContain('结局验证:胜利或失败后可重开'); - expect(testPlanMessage.textContent).toContain( - '回归检查:/mobile;/accessibility;/performance;/audio', - ); - expect(testPlanMessage.textContent).toContain( - 'preview-readiness:程序组 / Preview 执行静态自检 · 待处理', - ); - expect(testPlanMessage.textContent).toContain( - 'preview-playtest:程序组 / Playtest 预览并试玩验收 · 待处理', - ); - expect(testPlanMessage.textContent).toContain('记录反馈:/feedback'); - expect(testPlanMessage.textContent).toContain('建议:/run'); - const testPlanMessageList = document.querySelector('.message-list'); - expect(testPlanMessageList).not.toBeNull(); - const testPlanDraftButtons = within( - testPlanMessageList as HTMLElement, - ).getAllByRole('button', { name: '启动试玩' }); - fireEvent.click(testPlanDraftButtons[testPlanDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeTestPlan.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeTestPlan.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeTestPlan.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeTestPlan.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeTestPlan.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeTestPlan.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeTestPlan.exportPackage); + it('预览快捷操作:/listing', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeListing = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + }; + await submitChat('/listing'); + const listingMessages = await screen.findAllByText(/作品页草稿:/); + const listingMessage = messageBubble( + listingMessages[listingMessages.length - 1], + ); + expect(listingMessage.textContent).toContain('标题:未命名游戏原型'); + expect(listingMessage.textContent).toContain( + '一句话卖点:做一个厨房弹幕游戏', + ); + expect(listingMessage.textContent).toContain( + 'publish-strategy:Director 整理运营定位 · 待处理', + ); + expect(listingMessage.textContent).toContain( + 'publish-package:Publish 整理发布包装 · 待处理', + ); + expect(listingMessage.textContent).toContain( + '封面素材:1 个视觉素材 · 可用 1 个画板 / 生成来源', + ); + expect(listingMessage.textContent).toContain( + '说明文案:exports/README.md · 已生成', + ); + expect(listingMessage.textContent).toContain( + '标签口径:玩法类型;视觉风格;难度 / 节奏;本地可玩', + ); + expect(listingMessage.textContent).toContain( + '边界:只整理作品页文案和封面需求;不上传云端;不发布作品', + ); + expect(listingMessage.textContent).toContain( + '建议:/read exports/README.md', + ); + const listingMessageList = document.querySelector('.message-list'); + expect(listingMessageList).not.toBeNull(); + const listingDraftButtons = within( + listingMessageList as HTMLElement, + ).getAllByRole('button', { name: '读发布说明' }); + fireEvent.click(listingDraftButtons[listingDraftButtons.length - 1]); + expect(await composerText()).toBe('/read exports/README.md'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeListing.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeListing.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeListing.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeListing.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeListing.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeListing.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeListing.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeListing.exportList); + }); - const commandCountsBeforeAudience = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( + it('预览快捷操作:/playtest', async () => { + const invoke = await openPreviewShortcutProject(); + const fileReadCountBeforePlaytest = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/audience'); - const audienceMessages = await screen.findAllByText(/首批试玩对象:/); - const audienceMessage = messageBubble( - audienceMessages[audienceMessages.length - 1], - ); - expect(audienceMessage.textContent).toContain('项目:未命名游戏原型'); - expect(audienceMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); - expect(audienceMessage.textContent).toContain( - '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', - ); - expect(audienceMessage.textContent).toContain( - '先测人群:创作者自测 1 轮;熟悉目标的同事 1-2 人;完全没看过项目的人 3-5 人;至少 1 位移动/触屏用户', - ); - expect(audienceMessage.textContent).toContain( - '第一批测试者:3-5 人;每人 5-10 分钟;先看能否独立理解', - ); - expect(audienceMessage.textContent).toContain( - '观察重点:30 秒能否理解目标;输入是否顺;胜负/重开是否明确;难度是否过早劝退;视觉/音效是否干扰', - ); - expect(audienceMessage.textContent).toContain( - 'preview-readiness:程序组 / Preview 执行静态自检 · 待处理', - ); - expect(audienceMessage.textContent).toContain( - 'preview-playtest:程序组 / Playtest 预览并试玩验收 · 待处理', - ); - expect(audienceMessage.textContent).toContain( - '暂不面向:公开发布、付费用户、大规模投放、儿童/无障碍等强承诺场景', - ); - expect(audienceMessage.textContent).toContain( - '参考:/playtest;/test-plan;/feedback;/share', - ); - expect(audienceMessage.textContent).toContain( - '边界:只整理首批试玩对象;不读取文件;不启动或打开预览;不导出试玩包;不写项目', - ); - expect(audienceMessage.textContent).toContain('建议:/run'); - const audienceMessageList = document.querySelector('.message-list'); - expect(audienceMessageList).not.toBeNull(); - const audienceDraftButtons = within( - audienceMessageList as HTMLElement, - ).getAllByRole('button', { name: '启动试玩' }); - fireEvent.click(audienceDraftButtons[audienceDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( + ).length; + const manifestReadCountBeforePlaytest = invoke.mock.calls.filter( ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeAudience.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeAudience.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeAudience.fileList); - expect( - invoke.mock.calls.filter( + ).length; + const runLocalCountBeforePlaytest = invoke.mock.calls.filter( ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeAudience.runLocal); - expect( - invoke.mock.calls.filter( + ).length; + const startPreviewCountBeforePlaytest = invoke.mock.calls.filter( ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeAudience.startPreview); - expect( - invoke.mock.calls.filter( + ).length; + const openPreviewCountBeforePlaytest = invoke.mock.calls.filter( ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeAudience.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeAudience.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeAudience.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeAudience.controlAgentRun); + ).length; + await submitChat('/playtest'); + const playtestMessages = await screen.findAllByText(/试玩状态:/); + const playtestMessage = messageBubble( + playtestMessages[playtestMessages.length - 1], + ); + expect(playtestMessage.textContent).toContain( + '原型:最近 run 已通过 run-main-shortcut-trace', + ); + expect(playtestMessage.textContent).toContain('预览:未启动 · 建议 /run'); + expect(playtestMessage.textContent).toContain( + 'Playtest 任务:程序组 / Playtest 预览并试玩验收 · 待处理', + ); + expect(playtestMessage.textContent).toContain( + '试玩日志:/read .agent/logs/preview.log', + ); + const playtestDraftButtons = screen.getAllByRole('button', { + name: '启动试玩', + }); + fireEvent.click(playtestDraftButtons[playtestDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(fileReadCountBeforePlaytest); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(manifestReadCountBeforePlaytest); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(runLocalCountBeforePlaytest); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(startPreviewCountBeforePlaytest); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(openPreviewCountBeforePlaytest); + }); - const commandCountsBeforeInvite = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/invite'); - const inviteMessages = await screen.findAllByText(/试玩邀请:/); - const inviteMessage = messageBubble( - inviteMessages[inviteMessages.length - 1], - ); - expect(inviteMessage.textContent).toContain('项目:未命名游戏原型'); - expect(inviteMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); - expect(inviteMessage.textContent).toContain( - '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', - ); - expect(inviteMessage.textContent).toContain( - '邀请对象:先发 3-5 人;优先熟人/同事/没看过项目的人;暂不公开发布或大规模投放', - ); - expect(inviteMessage.textContent).toContain('5-10 分钟试玩'); - expect(inviteMessage.textContent).toContain('30 秒内能否理解目标'); - expect(inviteMessage.textContent).toContain('操作是否顺'); - expect(inviteMessage.textContent).toContain('胜负和重开是否清楚'); - expect(inviteMessage.textContent).toContain( - '发送前:先 /run 启动本地预览,再把本地试玩方式发给测试者', - ); - expect(inviteMessage.textContent).toContain( - '参考:/audience;/test-plan;/feedback;/share', - ); - expect(inviteMessage.textContent).toContain( - '边界:只准备邀请文案;不读取文件;不启动或打开预览;不导出试玩包;不写项目', - ); - expect(inviteMessage.textContent).toContain('建议:/run'); - const inviteMessageList = document.querySelector('.message-list'); - expect(inviteMessageList).not.toBeNull(); - const inviteDraftButtons = within( - inviteMessageList as HTMLElement, - ).getAllByRole('button', { name: '启动试玩' }); - fireEvent.click(inviteDraftButtons[inviteDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeInvite.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeInvite.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeInvite.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeInvite.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeInvite.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeInvite.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeInvite.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeInvite.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeInvite.controlAgentRun); + it('预览快捷操作:/test-plan', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeTestPlan = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + }; + await submitChat('/test-plan'); + const testPlanMessages = await screen.findAllByText(/手动测试计划:/); + const testPlanMessage = messageBubble( + testPlanMessages[testPlanMessages.length - 1], + ); + expect(testPlanMessage.textContent).toContain('项目:未命名游戏原型'); + expect(testPlanMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); + expect(testPlanMessage.textContent).toContain( + '当前证据:最近 run 已通过 run-main-shortcut-trace;预览 未启动;入口 未见 trace 产物', + ); + expect(testPlanMessage.textContent).toContain( + '启动预览:/run 后确认首屏不空白', + ); + expect(testPlanMessage.textContent).toContain( + '30 秒理解:目标、操作、得分/失败和重开可见', + ); + expect(testPlanMessage.textContent).toContain( + '输入验证:键盘/点击/触屏至少一种可完成核心动作', + ); + expect(testPlanMessage.textContent).toContain( + '结局验证:胜利或失败后可重开', + ); + expect(testPlanMessage.textContent).toContain( + '回归检查:/mobile;/accessibility;/performance;/audio', + ); + expect(testPlanMessage.textContent).toContain( + 'preview-readiness:程序组 / Preview 执行静态自检 · 待处理', + ); + expect(testPlanMessage.textContent).toContain( + 'preview-playtest:程序组 / Playtest 预览并试玩验收 · 待处理', + ); + expect(testPlanMessage.textContent).toContain('记录反馈:/feedback'); + expect(testPlanMessage.textContent).toContain('建议:/run'); + const testPlanMessageList = document.querySelector('.message-list'); + expect(testPlanMessageList).not.toBeNull(); + const testPlanDraftButtons = within( + testPlanMessageList as HTMLElement, + ).getAllByRole('button', { name: '启动试玩' }); + fireEvent.click(testPlanDraftButtons[testPlanDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeTestPlan.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeTestPlan.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeTestPlan.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeTestPlan.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeTestPlan.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeTestPlan.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeTestPlan.exportPackage); + }); - const commandCountsBeforeBugReport = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeBugReport.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeBugReport.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeBugReport.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeBugReport.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeBugReport.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeBugReport.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeBugReport.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeBugReport.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeBugReport.controlAgentRun); + it('预览快捷操作:/audience', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeAudience = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/audience'); + const audienceMessages = await screen.findAllByText(/首批试玩对象:/); + const audienceMessage = messageBubble( + audienceMessages[audienceMessages.length - 1], + ); + expect(audienceMessage.textContent).toContain('项目:未命名游戏原型'); + expect(audienceMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); + expect(audienceMessage.textContent).toContain( + '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', + ); + expect(audienceMessage.textContent).toContain( + '先测人群:创作者自测 1 轮;熟悉目标的同事 1-2 人;完全没看过项目的人 3-5 人;至少 1 位移动/触屏用户', + ); + expect(audienceMessage.textContent).toContain( + '第一批测试者:3-5 人;每人 5-10 分钟;先看能否独立理解', + ); + expect(audienceMessage.textContent).toContain( + '观察重点:30 秒能否理解目标;输入是否顺;胜负/重开是否明确;难度是否过早劝退;视觉/音效是否干扰', + ); + expect(audienceMessage.textContent).toContain( + 'preview-readiness:程序组 / Preview 执行静态自检 · 待处理', + ); + expect(audienceMessage.textContent).toContain( + 'preview-playtest:程序组 / Playtest 预览并试玩验收 · 待处理', + ); + expect(audienceMessage.textContent).toContain( + '暂不面向:公开发布、付费用户、大规模投放、儿童/无障碍等强承诺场景', + ); + expect(audienceMessage.textContent).toContain( + '参考:/playtest;/test-plan;/feedback;/share', + ); + expect(audienceMessage.textContent).toContain( + '边界:只整理首批试玩对象;不读取文件;不启动或打开预览;不导出试玩包;不写项目', + ); + expect(audienceMessage.textContent).toContain('建议:/run'); + const audienceMessageList = document.querySelector('.message-list'); + expect(audienceMessageList).not.toBeNull(); + const audienceDraftButtons = within( + audienceMessageList as HTMLElement, + ).getAllByRole('button', { name: '启动试玩' }); + fireEvent.click(audienceDraftButtons[audienceDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeAudience.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeAudience.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeAudience.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeAudience.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeAudience.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeAudience.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeAudience.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeAudience.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforeAudience.controlAgentRun); + }); - const commandCountsBeforeSurvey = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/survey'); - const surveyMessages = await screen.findAllByText(/试玩问卷:/); - const surveyMessage = messageBubble( - surveyMessages[surveyMessages.length - 1], - ); - expect(surveyMessage.textContent).toContain('项目:未命名游戏原型'); - expect(surveyMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); - expect(surveyMessage.textContent).toContain( - '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', - ); - expect(surveyMessage.textContent).toContain( - '使用场景:发给首批 3-5 位测试者;每人 5-10 分钟;先自由玩一局再回答', - ); - expect(surveyMessage.textContent).toContain( - '问题清单:1. 30 秒内你觉得目标是什么?2. 第一次操作哪里最卡?3. 胜负/重开是否清楚?4. 难度/节奏感觉如何?5. 最想保留和最想改的各一项?', - ); - expect(surveyMessage.textContent).toContain( - '记录格式:每题 1-5 分 + 一句话;补充设备、输入方式、是否愿意再玩一局', - ); - expect(surveyMessage.textContent).toContain( - '追踪方式:客户端问题由自动诊断提示;整体反馈走 /feedback;下一轮改动走 /revise', - ); - expect(surveyMessage.textContent).toContain( - '参考:/invite;/audience;/feedback', - ); - expect(surveyMessage.textContent).toContain( - '边界:只准备试玩问卷;不读取文件;不启动或打开预览;不导出试玩包;不写项目', - ); - expect(surveyMessage.textContent).toContain('建议:/run'); - const surveyMessageList = document.querySelector('.message-list'); - expect(surveyMessageList).not.toBeNull(); - const surveyDraftButtons = within( - surveyMessageList as HTMLElement, - ).getAllByRole('button', { name: '启动试玩' }); - fireEvent.click(surveyDraftButtons[surveyDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeSurvey.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeSurvey.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeSurvey.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeSurvey.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeSurvey.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeSurvey.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeSurvey.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeSurvey.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeSurvey.controlAgentRun); + it('预览快捷操作:/invite', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeInvite = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/invite'); + const inviteMessages = await screen.findAllByText(/试玩邀请:/); + const inviteMessage = messageBubble( + inviteMessages[inviteMessages.length - 1], + ); + expect(inviteMessage.textContent).toContain('项目:未命名游戏原型'); + expect(inviteMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); + expect(inviteMessage.textContent).toContain( + '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', + ); + expect(inviteMessage.textContent).toContain( + '邀请对象:先发 3-5 人;优先熟人/同事/没看过项目的人;暂不公开发布或大规模投放', + ); + expect(inviteMessage.textContent).toContain('5-10 分钟试玩'); + expect(inviteMessage.textContent).toContain('30 秒内能否理解目标'); + expect(inviteMessage.textContent).toContain('操作是否顺'); + expect(inviteMessage.textContent).toContain('胜负和重开是否清楚'); + expect(inviteMessage.textContent).toContain( + '发送前:先 /run 启动本地预览,再把本地试玩方式发给测试者', + ); + expect(inviteMessage.textContent).toContain( + '参考:/audience;/test-plan;/feedback;/share', + ); + expect(inviteMessage.textContent).toContain( + '边界:只准备邀请文案;不读取文件;不启动或打开预览;不导出试玩包;不写项目', + ); + expect(inviteMessage.textContent).toContain('建议:/run'); + const inviteMessageList = document.querySelector('.message-list'); + expect(inviteMessageList).not.toBeNull(); + const inviteDraftButtons = within( + inviteMessageList as HTMLElement, + ).getAllByRole('button', { name: '启动试玩' }); + fireEvent.click(inviteDraftButtons[inviteDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeInvite.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeInvite.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeInvite.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeInvite.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeInvite.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeInvite.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeInvite.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeInvite.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforeInvite.controlAgentRun); + }); - const commandCountsBeforeCover = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/cover'); - const coverMessages = await screen.findAllByText(/封面与缩略图:/); - const coverMessage = messageBubble(coverMessages[coverMessages.length - 1]); - expect(coverMessage.textContent).toContain('项目:未命名游戏原型'); - expect(coverMessage.textContent).toContain( - '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', - ); - expect(coverMessage.textContent).toContain( - '可用素材:视觉素材 1 个;画板/生成候选 1 个;总资产 2 个', - ); - expect(coverMessage.textContent).toContain( - '封面候选:assets/canvas-sync/tiles.png · image/png · 画板', - ); - expect(coverMessage.textContent).toContain( - '用途尺寸:作品页封面 16:9;社区缩略图 1:1;移动首屏 9:16', - ); - expect(coverMessage.textContent).toContain( - '选择口径:优先展示核心玩法状态;避免内部路径、调试面板、密钥配置或纯空场景', - ); - expect(coverMessage.textContent).toContain( - '补齐路径:有可试玩时先 /screenshots;缺美术时 /art;作品页文案走 /listing', - ); - expect(coverMessage.textContent).toContain( - '参考:/screenshots;/listing;/media-kit;/credits', - ); - expect(coverMessage.textContent).toContain( - '边界:只准备封面与缩略图检查;不截屏;不裁剪;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', - ); - expect(coverMessage.textContent).toContain('建议:/run'); - const coverMessageList = document.querySelector('.message-list'); - expect(coverMessageList).not.toBeNull(); - const coverDraftButtons = within( - coverMessageList as HTMLElement, - ).getAllByRole('button', { name: '启动试玩' }); - fireEvent.click(coverDraftButtons[coverDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeCover.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeCover.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeCover.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeCover.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeCover.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeCover.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeCover.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeCover.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeCover.controlAgentRun); + it('预览快捷操作:/survey', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeSurvey = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/survey'); + const surveyMessages = await screen.findAllByText(/试玩问卷:/); + const surveyMessage = messageBubble( + surveyMessages[surveyMessages.length - 1], + ); + expect(surveyMessage.textContent).toContain('项目:未命名游戏原型'); + expect(surveyMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); + expect(surveyMessage.textContent).toContain( + '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', + ); + expect(surveyMessage.textContent).toContain( + '使用场景:发给首批 3-5 位测试者;每人 5-10 分钟;先自由玩一局再回答', + ); + expect(surveyMessage.textContent).toContain( + '问题清单:1. 30 秒内你觉得目标是什么?2. 第一次操作哪里最卡?3. 胜负/重开是否清楚?4. 难度/节奏感觉如何?5. 最想保留和最想改的各一项?', + ); + expect(surveyMessage.textContent).toContain( + '记录格式:每题 1-5 分 + 一句话;补充设备、输入方式、是否愿意再玩一局', + ); + expect(surveyMessage.textContent).toContain( + '追踪方式:客户端问题由自动诊断提示;整体反馈走 /feedback;下一轮改动走 /revise', + ); + expect(surveyMessage.textContent).toContain( + '参考:/invite;/audience;/feedback', + ); + expect(surveyMessage.textContent).toContain( + '边界:只准备试玩问卷;不读取文件;不启动或打开预览;不导出试玩包;不写项目', + ); + expect(surveyMessage.textContent).toContain('建议:/run'); + const surveyMessageList = document.querySelector('.message-list'); + expect(surveyMessageList).not.toBeNull(); + const surveyDraftButtons = within( + surveyMessageList as HTMLElement, + ).getAllByRole('button', { name: '启动试玩' }); + fireEvent.click(surveyDraftButtons[surveyDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeSurvey.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeSurvey.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeSurvey.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeSurvey.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeSurvey.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeSurvey.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeSurvey.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeSurvey.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforeSurvey.controlAgentRun); + }); - const commandCountsBeforeScreenshots = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/screenshots'); - const screenshotMessages = await screen.findAllByText(/宣传截图:/); - const screenshotMessage = messageBubble( - screenshotMessages[screenshotMessages.length - 1], - ); - expect(screenshotMessage.textContent).toContain('项目:未命名游戏原型'); - expect(screenshotMessage.textContent).toContain( - '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', - ); - expect(screenshotMessage.textContent).toContain( - '可用素材:视觉素材 1 个;总资产 2 个', - ); - expect(screenshotMessage.textContent).toContain( - '截图目标:封面一张;核心操作一张;胜负/重开一张;移动或窄屏一张;异常/空状态不作为首批宣传图', - ); - expect(screenshotMessage.textContent).toContain( - '拍摄顺序:先确认 /run 可试玩;进入第一局 10-30 秒;截核心交互;再截结算或失败反馈', - ); - expect(screenshotMessage.textContent).toContain( - '命名建议:exports/screenshots/cover.png;gameplay.png;result.png;mobile.png', - ); - expect(screenshotMessage.textContent).toContain( - '文案搭配:每张图只配一句卖点;作品页标题和标签继续走 /listing', - ); - expect(screenshotMessage.textContent).toContain( - '参考:/listing;/publish;/share;/credits', - ); - expect(screenshotMessage.textContent).toContain( - '边界:只准备截图清单;不截屏;不读取文件;不启动或打开预览;不导出试玩包;不写项目', - ); - expect(screenshotMessage.textContent).toContain('建议:/run'); - const screenshotMessageList = document.querySelector('.message-list'); - expect(screenshotMessageList).not.toBeNull(); - const screenshotDraftButtons = within( - screenshotMessageList as HTMLElement, - ).getAllByRole('button', { name: '启动试玩' }); - fireEvent.click(screenshotDraftButtons[screenshotDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeScreenshots.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeScreenshots.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeScreenshots.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeScreenshots.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeScreenshots.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeScreenshots.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeScreenshots.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeScreenshots.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeScreenshots.controlAgentRun); + it('预览快捷操作:/cover', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeCover = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/cover'); + const coverMessages = await screen.findAllByText(/封面与缩略图:/); + const coverMessage = messageBubble(coverMessages[coverMessages.length - 1]); + expect(coverMessage.textContent).toContain('项目:未命名游戏原型'); + expect(coverMessage.textContent).toContain( + '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', + ); + expect(coverMessage.textContent).toContain( + '可用素材:视觉素材 1 个;画板/生成候选 1 个;总资产 2 个', + ); + expect(coverMessage.textContent).toContain( + '封面候选:assets/canvas-sync/tiles.png · image/png · 画板', + ); + expect(coverMessage.textContent).toContain( + '用途尺寸:作品页封面 16:9;社区缩略图 1:1;移动首屏 9:16', + ); + expect(coverMessage.textContent).toContain( + '选择口径:优先展示核心玩法状态;避免内部路径、调试面板、密钥配置或纯空场景', + ); + expect(coverMessage.textContent).toContain( + '补齐路径:有可试玩时先 /screenshots;缺美术时 /art;作品页文案走 /listing', + ); + expect(coverMessage.textContent).toContain( + '参考:/screenshots;/listing;/media-kit;/credits', + ); + expect(coverMessage.textContent).toContain( + '边界:只准备封面与缩略图检查;不截屏;不裁剪;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', + ); + expect(coverMessage.textContent).toContain('建议:/run'); + const coverMessageList = document.querySelector('.message-list'); + expect(coverMessageList).not.toBeNull(); + const coverDraftButtons = within( + coverMessageList as HTMLElement, + ).getAllByRole('button', { name: '启动试玩' }); + fireEvent.click(coverDraftButtons[coverDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeCover.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeCover.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeCover.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeCover.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeCover.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeCover.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeCover.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeCover.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforeCover.controlAgentRun); + }); - const commandCountsBeforeTrailer = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/trailer'); - const trailerMessages = await screen.findAllByText(/试玩短视频:/); - const trailerMessage = messageBubble( - trailerMessages[trailerMessages.length - 1], - ); - expect(trailerMessage.textContent).toContain('项目:未命名游戏原型'); - expect(trailerMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); - expect(trailerMessage.textContent).toContain( - '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', - ); - expect(trailerMessage.textContent).toContain( - '可用素材:视觉素材 1 个;音频素材 0 个;总资产 2 个', - ); - expect(trailerMessage.textContent).toContain( - '15 秒结构:0-3 秒首屏目标;3-8 秒核心操作;8-12 秒胜负 / 重开;12-15 秒结尾 CTA', - ); - expect(trailerMessage.textContent).toContain( - '镜头清单:标题 / 目标提示;玩家第一次操作;得分或失败反馈;重开按钮;结尾试玩邀请', - ); - expect(trailerMessage.textContent).toContain( - '口播节奏:一句玩法目标;一句操作说明;一句邀请试玩和反馈', - ); - expect(trailerMessage.textContent).toContain( - '录制提示:先确认 /run 可试玩;横屏或竖屏只选一种;不露内部路径、调试面板或密钥配置', - ); - expect(trailerMessage.textContent).toContain( - '参考:/screenshots;/listing;/share;/publish', - ); - expect(trailerMessage.textContent).toContain( - '边界:只准备试玩短视频脚本;不录屏;不读取文件;不启动或打开预览;不导出试玩包;不写项目', - ); - expect(trailerMessage.textContent).toContain('建议:/run'); - const trailerMessageList = document.querySelector('.message-list'); - expect(trailerMessageList).not.toBeNull(); - const trailerDraftButtons = within( - trailerMessageList as HTMLElement, - ).getAllByRole('button', { name: '启动试玩' }); - fireEvent.click(trailerDraftButtons[trailerDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeTrailer.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeTrailer.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeTrailer.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeTrailer.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeTrailer.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeTrailer.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeTrailer.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeTrailer.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeTrailer.controlAgentRun); + it('预览快捷操作:/screenshots', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeScreenshots = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/screenshots'); + const screenshotMessages = await screen.findAllByText(/宣传截图:/); + const screenshotMessage = messageBubble( + screenshotMessages[screenshotMessages.length - 1], + ); + expect(screenshotMessage.textContent).toContain('项目:未命名游戏原型'); + expect(screenshotMessage.textContent).toContain( + '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', + ); + expect(screenshotMessage.textContent).toContain( + '可用素材:视觉素材 1 个;总资产 2 个', + ); + expect(screenshotMessage.textContent).toContain( + '截图目标:封面一张;核心操作一张;胜负/重开一张;移动或窄屏一张;异常/空状态不作为首批宣传图', + ); + expect(screenshotMessage.textContent).toContain( + '拍摄顺序:先确认 /run 可试玩;进入第一局 10-30 秒;截核心交互;再截结算或失败反馈', + ); + expect(screenshotMessage.textContent).toContain( + '命名建议:exports/screenshots/cover.png;gameplay.png;result.png;mobile.png', + ); + expect(screenshotMessage.textContent).toContain( + '文案搭配:每张图只配一句卖点;作品页标题和标签继续走 /listing', + ); + expect(screenshotMessage.textContent).toContain( + '参考:/listing;/publish;/share;/credits', + ); + expect(screenshotMessage.textContent).toContain( + '边界:只准备截图清单;不截屏;不读取文件;不启动或打开预览;不导出试玩包;不写项目', + ); + expect(screenshotMessage.textContent).toContain('建议:/run'); + const screenshotMessageList = document.querySelector('.message-list'); + expect(screenshotMessageList).not.toBeNull(); + const screenshotDraftButtons = within( + screenshotMessageList as HTMLElement, + ).getAllByRole('button', { name: '启动试玩' }); + fireEvent.click(screenshotDraftButtons[screenshotDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeScreenshots.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeScreenshots.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeScreenshots.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeScreenshots.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeScreenshots.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeScreenshots.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeScreenshots.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeScreenshots.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforeScreenshots.controlAgentRun); + }); - const commandCountsBeforeFaq = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/faq'); - const faqMessages = await screen.findAllByText(/试玩 FAQ:/); - const faqMessage = messageBubble(faqMessages[faqMessages.length - 1]); - expect(faqMessage.textContent).toContain('项目:未命名游戏原型'); - expect(faqMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); - expect(faqMessage.textContent).toContain( - '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', - ); - expect(faqMessage.textContent).toContain( - '问答清单:1. 这是什么?2. 怎么开始和重开?3. 需要反馈什么?4. 打不开或卡住怎么办?5. 能不能转发或公开?', - ); - expect(faqMessage.textContent).toContain( - '回答口径:早期本地 Web 原型;5-10 分钟试玩;重点反馈目标理解、操作手感、难度、bug 和还想不想再玩', - ); - expect(faqMessage.textContent).toContain( - '测试者提醒:先自由玩一局;不要评价完成度;问卷走 /survey;客户端问题由自动诊断提示', - ); - expect(faqMessage.textContent).toContain( - '交付搭配:/invite;/share;/screenshots;/trailer', - ); - expect(faqMessage.textContent).toContain( - '边界:只准备试玩常见问答;不读取文件;不启动或打开预览;不导出试玩包;不写项目', - ); - expect(faqMessage.textContent).toContain('建议:/run'); - const faqMessageList = document.querySelector('.message-list'); - expect(faqMessageList).not.toBeNull(); - const faqDraftButtons = within(faqMessageList as HTMLElement).getAllByRole( - 'button', - { name: '启动试玩' }, - ); - fireEvent.click(faqDraftButtons[faqDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeFaq.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeFaq.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeFaq.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeFaq.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeFaq.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeFaq.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeFaq.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeFaq.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeFaq.controlAgentRun); + it('预览快捷操作:/trailer', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeTrailer = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/trailer'); + const trailerMessages = await screen.findAllByText(/试玩短视频:/); + const trailerMessage = messageBubble( + trailerMessages[trailerMessages.length - 1], + ); + expect(trailerMessage.textContent).toContain('项目:未命名游戏原型'); + expect(trailerMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); + expect(trailerMessage.textContent).toContain( + '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', + ); + expect(trailerMessage.textContent).toContain( + '可用素材:视觉素材 1 个;音频素材 0 个;总资产 2 个', + ); + expect(trailerMessage.textContent).toContain( + '15 秒结构:0-3 秒首屏目标;3-8 秒核心操作;8-12 秒胜负 / 重开;12-15 秒结尾 CTA', + ); + expect(trailerMessage.textContent).toContain( + '镜头清单:标题 / 目标提示;玩家第一次操作;得分或失败反馈;重开按钮;结尾试玩邀请', + ); + expect(trailerMessage.textContent).toContain( + '口播节奏:一句玩法目标;一句操作说明;一句邀请试玩和反馈', + ); + expect(trailerMessage.textContent).toContain( + '录制提示:先确认 /run 可试玩;横屏或竖屏只选一种;不露内部路径、调试面板或密钥配置', + ); + expect(trailerMessage.textContent).toContain( + '参考:/screenshots;/listing;/share;/publish', + ); + expect(trailerMessage.textContent).toContain( + '边界:只准备试玩短视频脚本;不录屏;不读取文件;不启动或打开预览;不导出试玩包;不写项目', + ); + expect(trailerMessage.textContent).toContain('建议:/run'); + const trailerMessageList = document.querySelector('.message-list'); + expect(trailerMessageList).not.toBeNull(); + const trailerDraftButtons = within( + trailerMessageList as HTMLElement, + ).getAllByRole('button', { name: '启动试玩' }); + fireEvent.click(trailerDraftButtons[trailerDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeTrailer.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeTrailer.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeTrailer.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeTrailer.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeTrailer.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeTrailer.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeTrailer.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeTrailer.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforeTrailer.controlAgentRun); + }); - const commandCountsBeforePost = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/post'); - const postMessages = await screen.findAllByText(/社区发布文案:/); - const postMessage = messageBubble(postMessages[postMessages.length - 1]); - expect(postMessage.textContent).toContain('项目:未命名游戏原型'); - expect(postMessage.textContent).toContain('一句话:做一个厨房弹幕游戏'); - expect(postMessage.textContent).toContain( - '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', - ); - expect(postMessage.textContent).toContain( - '素材准备:视觉素材 1 个;配图走 /screenshots;短视频走 /trailer', - ); - expect(postMessage.textContent).toContain( - '短文案:我做了一个早期 Web 小游戏原型《未命名游戏原型》,核心目标是做一个厨房弹幕游戏。想找 3-5 位朋友试玩 5 分钟,重点看能不能理解目标、操作顺不顺、还想不想再来一局。', - ); - expect(postMessage.textContent).toContain( - '长文案结构:一句玩法目标;一张截图或短视频;试玩方式;希望收到的三类反馈;已知限制', - ); - expect(postMessage.textContent).toContain( - '标签建议:#Web小游戏 #原型试玩 #AI游戏创作 #本地试玩', - ); - expect(postMessage.textContent).toContain( - 'CTA:愿意试玩请回复;遇到问题按 /faq 的口径反馈,客户端问题会自动提示', - ); - expect(postMessage.textContent).toContain( - '参考:/faq;/screenshots;/trailer;/store;/share', - ); - expect(postMessage.textContent).toContain( - '边界:只准备社区发布文案;不上传云端;不发布作品;不读取文件;不启动或打开预览;不导出试玩包;不写项目', - ); - expect(postMessage.textContent).toContain('建议:/run'); - const postMessageList = document.querySelector('.message-list'); - expect(postMessageList).not.toBeNull(); - const postDraftButtons = within(postMessageList as HTMLElement).getAllByRole( - 'button', - { name: '启动试玩' }, - ); - fireEvent.click(postDraftButtons[postDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforePost.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforePost.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforePost.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforePost.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforePost.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforePost.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforePost.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforePost.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforePost.controlAgentRun); + it('预览快捷操作:/faq', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeFaq = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/faq'); + const faqMessages = await screen.findAllByText(/试玩 FAQ:/); + const faqMessage = messageBubble(faqMessages[faqMessages.length - 1]); + expect(faqMessage.textContent).toContain('项目:未命名游戏原型'); + expect(faqMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); + expect(faqMessage.textContent).toContain( + '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', + ); + expect(faqMessage.textContent).toContain( + '问答清单:1. 这是什么?2. 怎么开始和重开?3. 需要反馈什么?4. 打不开或卡住怎么办?5. 能不能转发或公开?', + ); + expect(faqMessage.textContent).toContain( + '回答口径:早期本地 Web 原型;5-10 分钟试玩;重点反馈目标理解、操作手感、难度、bug 和还想不想再玩', + ); + expect(faqMessage.textContent).toContain( + '测试者提醒:先自由玩一局;不要评价完成度;问卷走 /survey;客户端问题由自动诊断提示', + ); + expect(faqMessage.textContent).toContain( + '交付搭配:/invite;/share;/screenshots;/trailer', + ); + expect(faqMessage.textContent).toContain( + '边界:只准备试玩常见问答;不读取文件;不启动或打开预览;不导出试玩包;不写项目', + ); + expect(faqMessage.textContent).toContain('建议:/run'); + const faqMessageList = document.querySelector('.message-list'); + expect(faqMessageList).not.toBeNull(); + const faqDraftButtons = within(faqMessageList as HTMLElement).getAllByRole( + 'button', + { name: '启动试玩' }, + ); + fireEvent.click(faqDraftButtons[faqDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeFaq.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeFaq.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeFaq.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeFaq.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeFaq.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeFaq.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeFaq.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeFaq.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforeFaq.controlAgentRun); + }); - const commandCountsBeforeStore = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/store'); - const storeMessages = await screen.findAllByText(/上架资料:/); - const storeMessage = messageBubble(storeMessages[storeMessages.length - 1]); - expect(storeMessage.textContent).toContain('项目:未命名游戏原型'); - expect(storeMessage.textContent).toContain('一句话:做一个厨房弹幕游戏'); - expect(storeMessage.textContent).toContain( - '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', - ); - expect(storeMessage.textContent).toContain( - '资产概况:视觉 1 个;音频 0 个;总资产 2 个', - ); - expect(storeMessage.textContent).toContain( - '必备资料:作品页文案 /listing;宣传截图 /screenshots;素材署名 /credits;隐私边界 /privacy;试玩包 /export', - ); - expect(storeMessage.textContent).toContain( - '发布说明:exports/README.md · 已生成', - ); - expect(storeMessage.textContent).toContain( - '首发范围:本地 Web 原型;小规模试玩;免费体验;不承诺账号、云存档、排行榜或付费', - ); - expect(storeMessage.textContent).toContain( - '上架前检查:30 秒玩法可懂;首屏不空白;重开清楚;截图不含内部路径;素材来源可说明', - ); - expect(storeMessage.textContent).toContain( - '参考:/publish;/listing;/screenshots;/credits;/privacy;/share', - ); - expect(storeMessage.textContent).toContain( - '边界:只准备上架资料清单;不上传云端;不发布作品;不读取文件;不启动或打开预览;不导出试玩包;不写项目', - ); - expect(storeMessage.textContent).toContain('建议:/run'); - const storeMessageList = document.querySelector('.message-list'); - expect(storeMessageList).not.toBeNull(); - const storeDraftButtons = within( - storeMessageList as HTMLElement, - ).getAllByRole('button', { name: '启动试玩' }); - fireEvent.click(storeDraftButtons[storeDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeStore.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeStore.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeStore.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeStore.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeStore.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeStore.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeStore.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeStore.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeStore.controlAgentRun); + it('预览快捷操作:/post', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforePost = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/post'); + const postMessages = await screen.findAllByText(/社区发布文案:/); + const postMessage = messageBubble(postMessages[postMessages.length - 1]); + expect(postMessage.textContent).toContain('项目:未命名游戏原型'); + expect(postMessage.textContent).toContain('一句话:做一个厨房弹幕游戏'); + expect(postMessage.textContent).toContain( + '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', + ); + expect(postMessage.textContent).toContain( + '素材准备:视觉素材 1 个;配图走 /screenshots;短视频走 /trailer', + ); + expect(postMessage.textContent).toContain( + '短文案:我做了一个早期 Web 小游戏原型《未命名游戏原型》,核心目标是做一个厨房弹幕游戏。想找 3-5 位朋友试玩 5 分钟,重点看能不能理解目标、操作顺不顺、还想不想再来一局。', + ); + expect(postMessage.textContent).toContain( + '长文案结构:一句玩法目标;一张截图或短视频;试玩方式;希望收到的三类反馈;已知限制', + ); + expect(postMessage.textContent).toContain( + '标签建议:#Web小游戏 #原型试玩 #AI游戏创作 #本地试玩', + ); + expect(postMessage.textContent).toContain( + 'CTA:愿意试玩请回复;遇到问题按 /faq 的口径反馈,客户端问题会自动提示', + ); + expect(postMessage.textContent).toContain( + '参考:/faq;/screenshots;/trailer;/store;/share', + ); + expect(postMessage.textContent).toContain( + '边界:只准备社区发布文案;不上传云端;不发布作品;不读取文件;不启动或打开预览;不导出试玩包;不写项目', + ); + expect(postMessage.textContent).toContain('建议:/run'); + const postMessageList = document.querySelector('.message-list'); + expect(postMessageList).not.toBeNull(); + const postDraftButtons = within( + postMessageList as HTMLElement, + ).getAllByRole('button', { name: '启动试玩' }); + fireEvent.click(postDraftButtons[postDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforePost.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforePost.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforePost.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforePost.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforePost.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforePost.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforePost.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforePost.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforePost.controlAgentRun); + }); - const commandCountsBeforeMediaKit = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/media-kit'); - const mediaKitMessages = await screen.findAllByText(/媒体资料包:/); - const mediaKitMessage = messageBubble( - mediaKitMessages[mediaKitMessages.length - 1], - ); - expect(mediaKitMessage.textContent).toContain('项目:未命名游戏原型'); - expect(mediaKitMessage.textContent).toContain('一句话:做一个厨房弹幕游戏'); - expect(mediaKitMessage.textContent).toContain( - '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', - ); - expect(mediaKitMessage.textContent).toContain( - '素材概况:视觉素材 1 个;音频素材 0 个;总资产 2 个;发布说明 exports/README.md · 已生成', - ); - expect(mediaKitMessage.textContent).toContain( - '资料清单:作品页 /listing;宣传截图 /screenshots;短视频 /trailer;FAQ /faq;社区文案 /post;上架清单 /store', - ); - expect(mediaKitMessage.textContent).toContain( - '缺口优先级:先跑 /run 确认可试玩;再补 /screenshots 和 /trailer;最后整理 /post 与 /store', - ); - expect(mediaKitMessage.textContent).toContain( - '打包顺序:1. 确认首屏和核心玩法;2. 准备截图 / 视频 / FAQ;3. 汇总署名、隐私和发布说明', - ); - expect(mediaKitMessage.textContent).toContain( - '参考:/screenshots;/trailer;/listing;/faq;/post;/store;/share', - ); - expect(mediaKitMessage.textContent).toContain( - '边界:只准备媒体资料包清单;不截屏;不录屏;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', - ); - expect(mediaKitMessage.textContent).toContain('建议:/run'); - const mediaKitMessageList = document.querySelector('.message-list'); - expect(mediaKitMessageList).not.toBeNull(); - const mediaKitDraftButtons = within( - mediaKitMessageList as HTMLElement, - ).getAllByRole('button', { name: '启动试玩' }); - fireEvent.click(mediaKitDraftButtons[mediaKitDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeMediaKit.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeMediaKit.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeMediaKit.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeMediaKit.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeMediaKit.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeMediaKit.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeMediaKit.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeMediaKit.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeMediaKit.controlAgentRun); + it('预览快捷操作:/store', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeStore = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/store'); + const storeMessages = await screen.findAllByText(/上架资料:/); + const storeMessage = messageBubble(storeMessages[storeMessages.length - 1]); + expect(storeMessage.textContent).toContain('项目:未命名游戏原型'); + expect(storeMessage.textContent).toContain('一句话:做一个厨房弹幕游戏'); + expect(storeMessage.textContent).toContain( + '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', + ); + expect(storeMessage.textContent).toContain( + '资产概况:视觉 1 个;音频 0 个;总资产 2 个', + ); + expect(storeMessage.textContent).toContain( + '必备资料:作品页文案 /listing;宣传截图 /screenshots;素材署名 /credits;隐私边界 /privacy;试玩包 /export', + ); + expect(storeMessage.textContent).toContain( + '发布说明:exports/README.md · 已生成', + ); + expect(storeMessage.textContent).toContain( + '首发范围:本地 Web 原型;小规模试玩;免费体验;不承诺账号、云存档、排行榜或付费', + ); + expect(storeMessage.textContent).toContain( + '上架前检查:30 秒玩法可懂;首屏不空白;重开清楚;截图不含内部路径;素材来源可说明', + ); + expect(storeMessage.textContent).toContain( + '参考:/publish;/listing;/screenshots;/credits;/privacy;/share', + ); + expect(storeMessage.textContent).toContain( + '边界:只准备上架资料清单;不上传云端;不发布作品;不读取文件;不启动或打开预览;不导出试玩包;不写项目', + ); + expect(storeMessage.textContent).toContain('建议:/run'); + const storeMessageList = document.querySelector('.message-list'); + expect(storeMessageList).not.toBeNull(); + const storeDraftButtons = within( + storeMessageList as HTMLElement, + ).getAllByRole('button', { name: '启动试玩' }); + fireEvent.click(storeDraftButtons[storeDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeStore.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeStore.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeStore.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeStore.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeStore.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeStore.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeStore.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeStore.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforeStore.controlAgentRun); + }); - const commandCountsBeforeReleaseNotes = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/release-notes'); - const releaseNotesMessages = await screen.findAllByText(/试玩更新说明:/); - const releaseNotesMessage = messageBubble( - releaseNotesMessages[releaseNotesMessages.length - 1], - ); - expect(releaseNotesMessage.textContent).toContain('项目:未命名游戏原型'); - expect(releaseNotesMessage.textContent).toContain( - '一句话:做一个厨房弹幕游戏', - ); - expect(releaseNotesMessage.textContent).toContain( - '当前版本:本地 Web 原型 · 小范围试玩 · 预览 未启动', - ); - expect(releaseNotesMessage.textContent).toContain( - 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', - ); - expect(releaseNotesMessage.textContent).toContain( - '本轮变化:可试玩版本已通过 Evaluator', - ); - expect(releaseNotesMessage.textContent).toContain( - '主要产物:exports/README.md;.agent/passes/pass-1/agenda.md', - ); - expect(releaseNotesMessage.textContent).toContain( - '素材变化:视觉素材 1 个;音频素材 0 个', - ); - expect(releaseNotesMessage.textContent).toContain( - '玩家可见说明:玩法目标;操作方式;胜负 / 重开反馈;当前已知限制', - ); - expect(releaseNotesMessage.textContent).toContain( - '已知限制:本地原型;不承诺账号、云存档、排行榜、付费或长期兼容', - ); - expect(releaseNotesMessage.textContent).toContain( - '搭配:/changes;/media-kit;/post;/store;/share', - ); - expect(releaseNotesMessage.textContent).toContain( - '边界:只准备试玩更新说明;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', - ); - expect(releaseNotesMessage.textContent).toContain('建议:/run'); - const releaseNotesMessageList = document.querySelector('.message-list'); - expect(releaseNotesMessageList).not.toBeNull(); - const releaseNotesDraftButtons = within( - releaseNotesMessageList as HTMLElement, - ).getAllByRole('button', { name: '启动试玩' }); - fireEvent.click( - releaseNotesDraftButtons[releaseNotesDraftButtons.length - 1], - ); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeReleaseNotes.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeReleaseNotes.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeReleaseNotes.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeReleaseNotes.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeReleaseNotes.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeReleaseNotes.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeReleaseNotes.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeReleaseNotes.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeReleaseNotes.controlAgentRun); + it('预览快捷操作:/media-kit', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeMediaKit = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/media-kit'); + const mediaKitMessages = await screen.findAllByText(/媒体资料包:/); + const mediaKitMessage = messageBubble( + mediaKitMessages[mediaKitMessages.length - 1], + ); + expect(mediaKitMessage.textContent).toContain('项目:未命名游戏原型'); + expect(mediaKitMessage.textContent).toContain('一句话:做一个厨房弹幕游戏'); + expect(mediaKitMessage.textContent).toContain( + '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', + ); + expect(mediaKitMessage.textContent).toContain( + '素材概况:视觉素材 1 个;音频素材 0 个;总资产 2 个;发布说明 exports/README.md · 已生成', + ); + expect(mediaKitMessage.textContent).toContain( + '资料清单:作品页 /listing;宣传截图 /screenshots;短视频 /trailer;FAQ /faq;社区文案 /post;上架清单 /store', + ); + expect(mediaKitMessage.textContent).toContain( + '缺口优先级:先跑 /run 确认可试玩;再补 /screenshots 和 /trailer;最后整理 /post 与 /store', + ); + expect(mediaKitMessage.textContent).toContain( + '打包顺序:1. 确认首屏和核心玩法;2. 准备截图 / 视频 / FAQ;3. 汇总署名、隐私和发布说明', + ); + expect(mediaKitMessage.textContent).toContain( + '参考:/screenshots;/trailer;/listing;/faq;/post;/store;/share', + ); + expect(mediaKitMessage.textContent).toContain( + '边界:只准备媒体资料包清单;不截屏;不录屏;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', + ); + expect(mediaKitMessage.textContent).toContain('建议:/run'); + const mediaKitMessageList = document.querySelector('.message-list'); + expect(mediaKitMessageList).not.toBeNull(); + const mediaKitDraftButtons = within( + mediaKitMessageList as HTMLElement, + ).getAllByRole('button', { name: '启动试玩' }); + fireEvent.click(mediaKitDraftButtons[mediaKitDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeMediaKit.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeMediaKit.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeMediaKit.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeMediaKit.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeMediaKit.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeMediaKit.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeMediaKit.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeMediaKit.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforeMediaKit.controlAgentRun); + }); - const commandCountsBeforeKnownIssues = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/known-issues'); - const knownIssueMessages = await screen.findAllByText(/已知问题清单:/); - const knownIssueMessage = messageBubble( - knownIssueMessages[knownIssueMessages.length - 1], - ); - expect(knownIssueMessage.textContent).toContain('项目:未命名游戏原型'); - expect(knownIssueMessage.textContent).toContain( - '当前状态:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed;预览 未启动', - ); - expect(knownIssueMessage.textContent).toContain( - '已知问题:暂无明确失败任务;仍按早期原型标注限制', - ); - expect(knownIssueMessage.textContent).toContain( - '试玩限制:本地 Web 原型;小范围 5-10 分钟试玩;不承诺账号、云存档、排行榜、付费或长期兼容', - ); - expect(knownIssueMessage.textContent).toContain( - '反馈入口:客户端问题由自动诊断提示;整体体验走 /feedback;版本变化走 /release-notes', - ); - expect(knownIssueMessage.textContent).toContain( - '发送前检查:可试玩状态先 /run;交付口径看 /share;对外资料看 /media-kit', - ); - expect(knownIssueMessage.textContent).toContain( - '边界:只准备已知问题清单;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', - ); - expect(knownIssueMessage.textContent).toContain('建议:/run'); - const knownIssueMessageList = document.querySelector('.message-list'); - expect(knownIssueMessageList).not.toBeNull(); - const knownIssueDraftButtons = within( - knownIssueMessageList as HTMLElement, - ).getAllByRole('button', { name: '启动试玩' }); - fireEvent.click(knownIssueDraftButtons[knownIssueDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeKnownIssues.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeKnownIssues.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeKnownIssues.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeKnownIssues.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeKnownIssues.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeKnownIssues.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeKnownIssues.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeKnownIssues.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeKnownIssues.controlAgentRun); + it('预览快捷操作:/release-notes', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeReleaseNotes = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/release-notes'); + const releaseNotesMessages = await screen.findAllByText(/试玩更新说明:/); + const releaseNotesMessage = messageBubble( + releaseNotesMessages[releaseNotesMessages.length - 1], + ); + expect(releaseNotesMessage.textContent).toContain('项目:未命名游戏原型'); + expect(releaseNotesMessage.textContent).toContain( + '一句话:做一个厨房弹幕游戏', + ); + expect(releaseNotesMessage.textContent).toContain( + '当前版本:本地 Web 原型 · 小范围试玩 · 预览 未启动', + ); + expect(releaseNotesMessage.textContent).toContain( + 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', + ); + expect(releaseNotesMessage.textContent).toContain( + '本轮变化:可试玩版本已通过 Evaluator', + ); + expect(releaseNotesMessage.textContent).toContain( + '主要产物:exports/README.md;.agent/passes/pass-1/agenda.md', + ); + expect(releaseNotesMessage.textContent).toContain( + '素材变化:视觉素材 1 个;音频素材 0 个', + ); + expect(releaseNotesMessage.textContent).toContain( + '玩家可见说明:玩法目标;操作方式;胜负 / 重开反馈;当前已知限制', + ); + expect(releaseNotesMessage.textContent).toContain( + '已知限制:本地原型;不承诺账号、云存档、排行榜、付费或长期兼容', + ); + expect(releaseNotesMessage.textContent).toContain( + '搭配:/changes;/media-kit;/post;/store;/share', + ); + expect(releaseNotesMessage.textContent).toContain( + '边界:只准备试玩更新说明;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', + ); + expect(releaseNotesMessage.textContent).toContain('建议:/run'); + const releaseNotesMessageList = document.querySelector('.message-list'); + expect(releaseNotesMessageList).not.toBeNull(); + const releaseNotesDraftButtons = within( + releaseNotesMessageList as HTMLElement, + ).getAllByRole('button', { name: '启动试玩' }); + fireEvent.click( + releaseNotesDraftButtons[releaseNotesDraftButtons.length - 1], + ); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeReleaseNotes.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeReleaseNotes.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeReleaseNotes.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeReleaseNotes.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeReleaseNotes.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeReleaseNotes.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeReleaseNotes.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeReleaseNotes.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforeReleaseNotes.controlAgentRun); + }); + + it('预览快捷操作:/known-issues', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeKnownIssues = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/known-issues'); + const knownIssueMessages = await screen.findAllByText(/已知问题清单:/); + const knownIssueMessage = messageBubble( + knownIssueMessages[knownIssueMessages.length - 1], + ); + expect(knownIssueMessage.textContent).toContain('项目:未命名游戏原型'); + expect(knownIssueMessage.textContent).toContain( + '当前状态:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed;预览 未启动', + ); + expect(knownIssueMessage.textContent).toContain( + '已知问题:暂无明确失败任务;仍按早期原型标注限制', + ); + expect(knownIssueMessage.textContent).toContain( + '试玩限制:本地 Web 原型;小范围 5-10 分钟试玩;不承诺账号、云存档、排行榜、付费或长期兼容', + ); + expect(knownIssueMessage.textContent).toContain( + '反馈入口:客户端问题由自动诊断提示;整体体验走 /feedback;版本变化走 /release-notes', + ); + expect(knownIssueMessage.textContent).toContain( + '发送前检查:可试玩状态先 /run;交付口径看 /share;对外资料看 /media-kit', + ); + expect(knownIssueMessage.textContent).toContain( + '边界:只准备已知问题清单;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', + ); + expect(knownIssueMessage.textContent).toContain('建议:/run'); + const knownIssueMessageList = document.querySelector('.message-list'); + expect(knownIssueMessageList).not.toBeNull(); + const knownIssueDraftButtons = within( + knownIssueMessageList as HTMLElement, + ).getAllByRole('button', { name: '启动试玩' }); + fireEvent.click(knownIssueDraftButtons[knownIssueDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeKnownIssues.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeKnownIssues.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeKnownIssues.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeKnownIssues.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeKnownIssues.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeKnownIssues.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeKnownIssues.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeKnownIssues.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforeKnownIssues.controlAgentRun); + }); } diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/assert-project-and-design-shortcuts.ts b/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/assert-project-and-design-shortcuts.ts index 82cadb2be..35c8a62f7 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/assert-project-and-design-shortcuts.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/assert-project-and-design-shortcuts.ts @@ -2,1556 +2,1618 @@ import { composerText, expect, fireEvent, + it, screen, submitChat, within, } from '../../harness'; -import type { PreviewShortcutInvoke } from './invoke-mock'; +import { openPreviewShortcutProject } from './actions'; import { messageBubble } from './message-bubble'; -export async function assertProjectAndDesignShortcutFlow( - invoke: PreviewShortcutInvoke, -) { - await screen.findByText('已打开:authorized-game'); - expect(screen.getByText('未命名游戏原型')).not.toBeNull(); - expect(screen.queryByText('/tmp/authorized-game')).toBeNull(); - expect(screen.getByText('preview: 未启动')).not.toBeNull(); - const projectSummary = screen.getByLabelText('项目摘要'); - expect(projectSummary.textContent).toContain('任务:已完成 0/'); - expect(projectSummary.textContent).toContain('ready'); - expect(projectSummary.textContent).toContain('资产:2 个 · 上传 1 / 画板 1'); - expect(projectSummary.textContent).toContain( - '最近命令:game.static_smoke 完成', - ); - expect(invoke).not.toHaveBeenCalledWith( - 'get_local_game_manifest', - expect.anything(), - ); - expect(screen.queryByLabelText('开发环境')).toBeNull(); - expect(screen.queryByText('编排 Trace')).toBeNull(); - expect(screen.queryByText(/LLM:已配置 · art-model/)).toBeNull(); +export function registerProjectAndDesignShortcutTests() { + it('预览快捷操作:项目概况与 Agent 状态', async () => { + const invoke = await openPreviewShortcutProject(); + expect(screen.getByText('未命名游戏原型')).not.toBeNull(); + expect(screen.queryByText('/tmp/authorized-game')).toBeNull(); + expect(screen.getByText('preview: 未启动')).not.toBeNull(); + const projectSummary = screen.getByLabelText('项目摘要'); + expect(projectSummary.textContent).toContain('任务:已完成 0/'); + expect(projectSummary.textContent).toContain('ready'); + expect(projectSummary.textContent).toContain( + '资产:2 个 · 上传 1 / 画板 1', + ); + expect(projectSummary.textContent).toContain( + '最近命令:game.static_smoke 完成', + ); + expect(invoke).not.toHaveBeenCalledWith( + 'get_local_game_manifest', + expect.anything(), + ); + expect(screen.queryByLabelText('开发环境')).toBeNull(); + expect(screen.queryByText('编排 Trace')).toBeNull(); + expect(screen.queryByText(/LLM:已配置 · art-model/)).toBeNull(); - fireEvent.click(screen.getByRole('button', { name: 'LLM状态' })); - expect(await screen.findByText(/官方智能服务:已连接/)).not.toBeNull(); - const agentStatusPane = screen.getByLabelText('Agent 状态'); - expect(within(agentStatusPane).getAllByText(/智能服务:已连接/)).toHaveLength( - 2, - ); - expect(screen.queryByText(/router\.genarrative\.world/)).toBeNull(); - expect(screen.queryByText(/gpt-5\.6-sol/)).toBeNull(); - expect(screen.queryByText(/Codex App Server/)).toBeNull(); - expect(screen.queryByText(/planner-secret/)).toBeNull(); - expect(screen.queryByText(/art-secret/)).toBeNull(); + fireEvent.click(screen.getByRole('button', { name: 'LLM状态' })); + expect(await screen.findByText(/官方智能服务:已连接/)).not.toBeNull(); + const agentStatusPane = screen.getByLabelText('Agent 状态'); + expect( + within(agentStatusPane).getAllByText(/智能服务:已连接/), + ).toHaveLength(2); + expect(screen.queryByText(/router\.genarrative\.world/)).toBeNull(); + expect(screen.queryByText(/gpt-5\.6-sol/)).toBeNull(); + expect(screen.queryByText(/Codex App Server/)).toBeNull(); + expect(screen.queryByText(/planner-secret/)).toBeNull(); + expect(screen.queryByText(/art-secret/)).toBeNull(); - await submitChat('/agents'); - await screen.findByText(/Agent 状态:/); - expect( - /Agent 状态:[\s\S]*美术组 \/ Asset · 生成首版美术素材 · 待处理[\s\S]*智能服务:已连接/.test( - screen.getByLabelText('聊天').textContent ?? '', - ), - ).toBe(true); - await submitChat('/agent-conversations'); - const agentConversationMessage = - await screen.findByText('Agent 对话读取命令:'); - const agentConversationBubble = messageBubble(agentConversationMessage); - expect(agentConversationBubble.textContent).toContain( - '美术组 / Asset · 生成首版美术素材:/read .agent/conversations/agents/art-asset-plan.jsonl', - ); - fireEvent.click(screen.getByRole('button', { name: '读取拆解创作方向对话' })); - expect(await composerText()).toBe( - '/read .agent/conversations/agents/design-director.jsonl', - ); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_project_file', - expect.objectContaining({ - relativePath: '.agent/conversations/agents/design-director.jsonl', - }), - ); - await submitChat('/agent-memories'); - const agentMemoryMessage = - await screen.findByText('Agent 私有记忆读取命令:'); - const agentMemoryBubble = messageBubble(agentMemoryMessage); - expect(agentMemoryBubble.textContent).toContain( - '美术组 / Asset · 生成首版美术素材:/read memory/agents/art/asset.md', - ); - fireEvent.click(screen.getByRole('button', { name: '读取拆解创作方向记忆' })); - expect(await composerText()).toBe('/read memory/agents/design/director.md'); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_project_file', - expect.objectContaining({ - relativePath: 'memory/agents/design/director.md', - }), - ); + await submitChat('/agents'); + await screen.findByText(/Agent 状态:/); + expect( + /Agent 状态:[\s\S]*美术组 \/ Asset · 生成首版美术素材 · 待处理[\s\S]*智能服务:已连接/.test( + screen.getByLabelText('聊天').textContent ?? '', + ), + ).toBe(true); + await submitChat('/agent-conversations'); + const agentConversationMessage = + await screen.findByText('Agent 对话读取命令:'); + const agentConversationBubble = messageBubble(agentConversationMessage); + expect(agentConversationBubble.textContent).toContain( + '美术组 / Asset · 生成首版美术素材:/read .agent/conversations/agents/art-asset-plan.jsonl', + ); + fireEvent.click( + screen.getByRole('button', { name: '读取拆解创作方向对话' }), + ); + expect(await composerText()).toBe( + '/read .agent/conversations/agents/design-director.jsonl', + ); + expect(invoke).not.toHaveBeenCalledWith( + 'read_local_project_file', + expect.objectContaining({ + relativePath: '.agent/conversations/agents/design-director.jsonl', + }), + ); + await submitChat('/agent-memories'); + const agentMemoryMessage = + await screen.findByText('Agent 私有记忆读取命令:'); + const agentMemoryBubble = messageBubble(agentMemoryMessage); + expect(agentMemoryBubble.textContent).toContain( + '美术组 / Asset · 生成首版美术素材:/read memory/agents/art/asset.md', + ); + fireEvent.click( + screen.getByRole('button', { name: '读取拆解创作方向记忆' }), + ); + expect(await composerText()).toBe('/read memory/agents/design/director.md'); + expect(invoke).not.toHaveBeenCalledWith( + 'read_local_project_file', + expect.objectContaining({ + relativePath: 'memory/agents/design/director.md', + }), + ); - fireEvent.click( - within(agentStatusPane).getByRole('button', { - name: /生成首版美术素材/, - }), - ); - const agentDialog = await screen.findByRole('dialog', { - name: 'Agent 对话', + fireEvent.click( + within(agentStatusPane).getByRole('button', { + name: /生成首版美术素材/, + }), + ); + const agentDialog = await screen.findByRole('dialog', { + name: 'Agent 对话', + }); + expect( + within(agentDialog).getByText( + /官方智能服务:已连接,流式 开启,联网检索 开启,账号状态 已就绪/, + ), + ).not.toBeNull(); + fireEvent.click(within(agentDialog).getByRole('button', { name: '关闭' })); + + fireEvent.click(screen.getByRole('button', { name: '能力' })); + expect(await screen.findByText(/Native Runtime 能力/)).not.toBeNull(); + + fireEvent.click(screen.getByRole('button', { name: '项目状态' })); + expect(await screen.findByText(/项目:未命名游戏原型/)).not.toBeNull(); + expect(screen.getByText(/目录:\/tmp\/authorized-game/)).not.toBeNull(); + expect(invoke).toHaveBeenCalledWith('get_game_creation_agent_capabilities'); }); - expect( - within(agentDialog).getByText( - /官方智能服务:已连接,流式 开启,联网检索 开启,账号状态 已就绪/, - ), - ).not.toBeNull(); - fireEvent.click(within(agentDialog).getByRole('button', { name: '关闭' })); - fireEvent.click(screen.getByRole('button', { name: '能力' })); - expect(await screen.findByText(/Native Runtime 能力/)).not.toBeNull(); + it('预览快捷操作:/brief', async () => { + const invoke = await openPreviewShortcutProject(); + const manifestReadCountBeforeBrief = invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length; + await submitChat('/brief'); + expect(await screen.findByText(/项目简报:/)).not.toBeNull(); + expect(screen.getByText(/任务:完成 0\/16 · ready 3/)).not.toBeNull(); + expect( + screen.getByText(/最近 Run:run-main-shortcut-trace/), + ).not.toBeNull(); + fireEvent.click(screen.getByRole('button', { name: '查看下一步' })); + expect(await composerText()).toBe('/next'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(manifestReadCountBeforeBrief); + }); - fireEvent.click(screen.getByRole('button', { name: '项目状态' })); - expect(await screen.findByText(/项目:未命名游戏原型/)).not.toBeNull(); - expect(screen.getByText(/目录:\/tmp\/authorized-game/)).not.toBeNull(); + it('预览快捷操作:/goal', async () => { + const invoke = await openPreviewShortcutProject(); + const manifestReadCountBeforeGoal = invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length; + const fileReadCountBeforeGoal = invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length; + const runLocalCountBeforeGoal = invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length; + await submitChat('/goal'); + expect(await screen.findByText(/创作目标:/)).not.toBeNull(); + const goalMessages = screen.getAllByText(/创作目标:/); + const goalMessage = messageBubble(goalMessages[goalMessages.length - 1]); + expect(goalMessage.textContent).toContain('Manifest:做一个厨房弹幕游戏'); + expect(goalMessage.textContent).toContain( + 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', + ); + expect(goalMessage.textContent).toContain('Run 目标:做一个厨房弹幕游戏'); + expect(goalMessage.textContent).toContain('任务图目标:做一个厨房弹幕游戏'); + expect(goalMessage.textContent).toContain('上下文:/context'); + expect(goalMessage.textContent).toContain('建议:/agent-resume 细化目标:'); + fireEvent.click(screen.getByRole('button', { name: '补充目标' })); + expect(await composerText()).toBe('/agent-resume 细化目标:'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(manifestReadCountBeforeGoal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(fileReadCountBeforeGoal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(runLocalCountBeforeGoal); + }); - const manifestReadCountBeforeBrief = invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length; - await submitChat('/brief'); - expect(await screen.findByText(/项目简报:/)).not.toBeNull(); - expect(screen.getByText(/任务:完成 0\/16 · ready 3/)).not.toBeNull(); - expect(screen.getByText(/最近 Run:run-main-shortcut-trace/)).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '查看下一步' })); - expect(await composerText()).toBe('/next'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(manifestReadCountBeforeBrief); + it('预览快捷操作:/spec', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeSpec = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + }; + await submitChat('/spec'); + const specMessages = await screen.findAllByText(/创作规格包:/); + const specMessage = messageBubble(specMessages[specMessages.length - 1]); + expect(specMessage.textContent).toContain('项目:未命名游戏原型'); + expect(specMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); + expect(specMessage.textContent).toContain( + 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', + ); + expect(specMessage.textContent).toContain( + 'Planner 规格:.agent/spec.md · 已出现在最近 run', + ); + expect(specMessage.textContent).toContain( + '玩法设计:game/game_design.md · 任务声明', + ); + expect(specMessage.textContent).toContain( + '发布说明:exports/README.md · 已出现在最近 run', + ); + expect(specMessage.textContent).toContain( + '边界:只整理规格产物状态;不读取规格文件;不启动预览;不写项目', + ); + expect(specMessage.textContent).toContain('建议:/read .agent/spec.md'); + const specMessageList = document.querySelector('.message-list'); + expect(specMessageList).not.toBeNull(); + const specDraftButtons = within( + specMessageList as HTMLElement, + ).getAllByRole('button', { name: '读取规格' }); + fireEvent.click(specDraftButtons[specDraftButtons.length - 1]); + expect(await composerText()).toBe('/read .agent/spec.md'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeSpec.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeSpec.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeSpec.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeSpec.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeSpec.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeSpec.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeSpec.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeSpec.exportList); + }); - const manifestReadCountBeforeGoal = invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length; - const fileReadCountBeforeGoal = invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length; - const runLocalCountBeforeGoal = invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length; - await submitChat('/goal'); - expect(await screen.findByText(/创作目标:/)).not.toBeNull(); - const goalMessages = screen.getAllByText(/创作目标:/); - const goalMessage = messageBubble(goalMessages[goalMessages.length - 1]); - expect(goalMessage.textContent).toContain('Manifest:做一个厨房弹幕游戏'); - expect(goalMessage.textContent).toContain( - 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', - ); - expect(goalMessage.textContent).toContain('Run 目标:做一个厨房弹幕游戏'); - expect(goalMessage.textContent).toContain('任务图目标:做一个厨房弹幕游戏'); - expect(goalMessage.textContent).toContain('上下文:/context'); - expect(goalMessage.textContent).toContain('建议:/agent-resume 细化目标:'); - fireEvent.click(screen.getByRole('button', { name: '补充目标' })); - expect(await composerText()).toBe('/agent-resume 细化目标:'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(manifestReadCountBeforeGoal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(fileReadCountBeforeGoal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(runLocalCountBeforeGoal); + it('预览快捷操作:/mvp', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeMvp = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + }; + await submitChat('/mvp'); + expect(await screen.findByText(/MVP 范围:/)).not.toBeNull(); + const mvpMessages = screen.getAllByText(/MVP 范围:/); + const mvpMessage = messageBubble(mvpMessages[mvpMessages.length - 1]); + expect(mvpMessage.textContent).toContain('项目:未命名游戏原型'); + expect(mvpMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); + expect(mvpMessage.textContent).toContain( + 'MVP 内:可运行 Web 原型;基础输入 / 胜负 / 重开;本地预览;本地试玩包', + ); + expect(mvpMessage.textContent).toContain( + '当前状态:最近 run run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', + ); + expect(mvpMessage.textContent).toContain( + '任务:完成 0/16 · ready 3 · 失败 0', + ); + expect(mvpMessage.textContent).toContain('预览:未启动'); + expect(mvpMessage.textContent).toContain('资产:2 个'); + expect(mvpMessage.textContent).toContain('试玩包:待导出'); + expect(mvpMessage.textContent).toContain( + '先不做:云同步;Unity/Godot;插件市场;任意 shell;深度资产精修', + ); + expect(mvpMessage.textContent).toContain('建议:/run'); + const mvpMessageList = document.querySelector('.message-list'); + expect(mvpMessageList).not.toBeNull(); + const mvpDraftButtons = within(mvpMessageList as HTMLElement).getAllByRole( + 'button', + { name: '启动预览' }, + ); + fireEvent.click(mvpDraftButtons[mvpDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeMvp.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeMvp.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeMvp.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeMvp.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeMvp.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeMvp.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeMvp.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeMvp.exportList); + }); - const commandCountsBeforeSpec = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - }; - await submitChat('/spec'); - const specMessages = await screen.findAllByText(/创作规格包:/); - const specMessage = messageBubble(specMessages[specMessages.length - 1]); - expect(specMessage.textContent).toContain('项目:未命名游戏原型'); - expect(specMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); - expect(specMessage.textContent).toContain( - 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', - ); - expect(specMessage.textContent).toContain( - 'Planner 规格:.agent/spec.md · 已出现在最近 run', - ); - expect(specMessage.textContent).toContain( - '玩法设计:game/game_design.md · 任务声明', - ); - expect(specMessage.textContent).toContain( - '发布说明:exports/README.md · 已出现在最近 run', - ); - expect(specMessage.textContent).toContain( - '边界:只整理规格产物状态;不读取规格文件;不启动预览;不写项目', - ); - expect(specMessage.textContent).toContain('建议:/read .agent/spec.md'); - const specMessageList = document.querySelector('.message-list'); - expect(specMessageList).not.toBeNull(); - const specDraftButtons = within(specMessageList as HTMLElement).getAllByRole( - 'button', - { name: '读取规格' }, - ); - fireEvent.click(specDraftButtons[specDraftButtons.length - 1]); - expect(await composerText()).toBe('/read .agent/spec.md'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeSpec.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeSpec.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeSpec.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeSpec.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeSpec.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeSpec.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeSpec.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeSpec.exportList); + it('预览快捷操作:/pitch', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforePitch = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + }; + await submitChat('/pitch'); + const pitchMessages = await screen.findAllByText(/试玩定位:/); + const pitchMessage = messageBubble(pitchMessages[pitchMessages.length - 1]); + expect(pitchMessage.textContent).toContain('项目:未命名游戏原型'); + expect(pitchMessage.textContent).toContain('一句话:做一个厨房弹幕游戏'); + expect(pitchMessage.textContent).toContain( + '核心乐趣:快速验证目标、操作反馈、胜负结果和重开节奏', + ); + expect(pitchMessage.textContent).toContain( + '当前可演示:最近 run 已通过 run-main-shortcut-trace;预览 未启动', + ); + expect(pitchMessage.textContent).toContain( + '讲给测试者:先说明目标,再说明操作,然后看 30 秒内是否能理解胜负和重开', + ); + expect(pitchMessage.textContent).toContain( + '不承诺:云发布;深度美术精修;账号体系;排行榜;长期运营包装', + ); + expect(pitchMessage.textContent).toContain( + '参考:/mvp;/rules;/playtest;/listing', + ); + expect(pitchMessage.textContent).toContain('建议:/run'); + const pitchMessageList = document.querySelector('.message-list'); + expect(pitchMessageList).not.toBeNull(); + const pitchDraftButtons = within( + pitchMessageList as HTMLElement, + ).getAllByRole('button', { name: '启动预览' }); + fireEvent.click(pitchDraftButtons[pitchDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforePitch.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforePitch.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforePitch.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforePitch.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforePitch.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforePitch.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforePitch.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforePitch.exportList); + }); - const commandCountsBeforeMvp = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - }; - await submitChat('/mvp'); - expect(await screen.findByText(/MVP 范围:/)).not.toBeNull(); - const mvpMessages = screen.getAllByText(/MVP 范围:/); - const mvpMessage = messageBubble(mvpMessages[mvpMessages.length - 1]); - expect(mvpMessage.textContent).toContain('项目:未命名游戏原型'); - expect(mvpMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); - expect(mvpMessage.textContent).toContain( - 'MVP 内:可运行 Web 原型;基础输入 / 胜负 / 重开;本地预览;本地试玩包', - ); - expect(mvpMessage.textContent).toContain( - '当前状态:最近 run run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', - ); - expect(mvpMessage.textContent).toContain( - '任务:完成 0/16 · ready 3 · 失败 0', - ); - expect(mvpMessage.textContent).toContain('预览:未启动'); - expect(mvpMessage.textContent).toContain('资产:2 个'); - expect(mvpMessage.textContent).toContain('试玩包:待导出'); - expect(mvpMessage.textContent).toContain( - '先不做:云同步;Unity/Godot;插件市场;任意 shell;深度资产精修', - ); - expect(mvpMessage.textContent).toContain('建议:/run'); - const mvpMessageList = document.querySelector('.message-list'); - expect(mvpMessageList).not.toBeNull(); - const mvpDraftButtons = within(mvpMessageList as HTMLElement).getAllByRole( - 'button', - { name: '启动预览' }, - ); - fireEvent.click(mvpDraftButtons[mvpDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeMvp.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeMvp.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeMvp.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeMvp.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeMvp.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeMvp.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeMvp.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeMvp.exportList); + it('预览快捷操作:/demo', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeDemo = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + }; + await submitChat('/demo'); + const demoMessages = await screen.findAllByText(/试玩讲解稿:/); + const demoMessage = messageBubble(demoMessages[demoMessages.length - 1]); + expect(demoMessage.textContent).toContain('项目:未命名游戏原型'); + expect(demoMessage.textContent).toContain( + '30 秒开场:这是《未命名游戏原型》,目标是做一个厨房弹幕游戏', + ); + expect(demoMessage.textContent).toContain( + '讲解顺序:目标 -> 操作 -> 反馈 -> 胜负 -> 重开', + ); + expect(demoMessage.textContent).toContain( + '口播稿:先看目标提示,尝试移动/点击完成核心动作;看到得分、受击或状态反馈后,继续到胜利或失败;结束后确认能否一键重开', + ); + expect(demoMessage.textContent).toContain( + '当前演示状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', + ); + expect(demoMessage.textContent).toContain('最近试玩证据:暂无'); + expect(demoMessage.textContent).toContain( + '收反馈:操作是否明白;节奏是否太快;胜负是否清楚;视觉 / 音效是否帮助理解', + ); + expect(demoMessage.textContent).toContain( + '边界:只准备试玩讲解;不启动预览;不导出试玩包;不发布作品', + ); + expect(demoMessage.textContent).toContain( + '参考:/rules;/test-plan;/feedback;/share', + ); + expect(demoMessage.textContent).toContain('建议:/run'); + const demoMessageList = document.querySelector('.message-list'); + expect(demoMessageList).not.toBeNull(); + const demoDraftButtons = within( + demoMessageList as HTMLElement, + ).getAllByRole('button', { name: '启动预览' }); + fireEvent.click(demoDraftButtons[demoDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeDemo.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeDemo.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeDemo.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeDemo.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeDemo.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeDemo.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeDemo.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeDemo.exportList); + }); - const commandCountsBeforePitch = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - }; - await submitChat('/pitch'); - const pitchMessages = await screen.findAllByText(/试玩定位:/); - const pitchMessage = messageBubble(pitchMessages[pitchMessages.length - 1]); - expect(pitchMessage.textContent).toContain('项目:未命名游戏原型'); - expect(pitchMessage.textContent).toContain('一句话:做一个厨房弹幕游戏'); - expect(pitchMessage.textContent).toContain( - '核心乐趣:快速验证目标、操作反馈、胜负结果和重开节奏', - ); - expect(pitchMessage.textContent).toContain( - '当前可演示:最近 run 已通过 run-main-shortcut-trace;预览 未启动', - ); - expect(pitchMessage.textContent).toContain( - '讲给测试者:先说明目标,再说明操作,然后看 30 秒内是否能理解胜负和重开', - ); - expect(pitchMessage.textContent).toContain( - '不承诺:云发布;深度美术精修;账号体系;排行榜;长期运营包装', - ); - expect(pitchMessage.textContent).toContain( - '参考:/mvp;/rules;/playtest;/listing', - ); - expect(pitchMessage.textContent).toContain('建议:/run'); - const pitchMessageList = document.querySelector('.message-list'); - expect(pitchMessageList).not.toBeNull(); - const pitchDraftButtons = within( - pitchMessageList as HTMLElement, - ).getAllByRole('button', { name: '启动预览' }); - fireEvent.click(pitchDraftButtons[pitchDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforePitch.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforePitch.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforePitch.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforePitch.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforePitch.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforePitch.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforePitch.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforePitch.exportList); + it('预览快捷操作:/rules', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeControls = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + }; + await submitChat('/rules'); + expect(await screen.findByText(/玩法操作:/)).not.toBeNull(); + const controlMessages = screen.getAllByText(/玩法操作:/); + const controlMessage = messageBubble( + controlMessages[controlMessages.length - 1], + ); + expect(controlMessage.textContent).toContain('项目:未命名游戏原型'); + expect(controlMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); + expect(controlMessage.textContent).toContain( + '核心口径:目标;操作;胜负;重开;本地预览', + ); + expect(controlMessage.textContent).toContain( + '规则来源:game/game_design.md · 未见 trace 产物', + ); + expect(controlMessage.textContent).toContain( + '原型入口:game/index.html · 未见 trace 产物', + ); + expect(controlMessage.textContent).toContain( + 'design-foundation:设计实现组 / Gameplay 确定玩法规格与界面原型 · 待处理', + ); + expect(controlMessage.textContent).toContain( + 'code-prototype:程序组 / Code 生成可运行原型 · 待处理', + ); + expect(controlMessage.textContent).toContain( + 'preview-playtest:程序组 / Playtest 预览并试玩验收 · 待处理', + ); + expect(controlMessage.textContent).toContain( + '最近程序/试玩步骤:Generator #1 · completed · 生成可运行草案', + ); + expect(controlMessage.textContent).toContain( + '相关命令:/mvp;/playtest;/feedback', + ); + expect(controlMessage.textContent).toContain( + '建议:/agent-resume 操作说明:在首屏明确移动/点击操作、胜负目标、失败后重开方式', + ); + const controlMessageList = document.querySelector('.message-list'); + expect(controlMessageList).not.toBeNull(); + const controlDraftButtons = within( + controlMessageList as HTMLElement, + ).getAllByRole('button', { name: '补充操作说明' }); + fireEvent.click(controlDraftButtons[controlDraftButtons.length - 1]); + expect(await composerText()).toBe( + '/agent-resume 操作说明:在首屏明确移动/点击操作、胜负目标、失败后重开方式', + ); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeControls.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeControls.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeControls.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeControls.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeControls.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeControls.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeControls.exportPackage); + }); - const commandCountsBeforeDemo = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - }; - await submitChat('/demo'); - const demoMessages = await screen.findAllByText(/试玩讲解稿:/); - const demoMessage = messageBubble(demoMessages[demoMessages.length - 1]); - expect(demoMessage.textContent).toContain('项目:未命名游戏原型'); - expect(demoMessage.textContent).toContain( - '30 秒开场:这是《未命名游戏原型》,目标是做一个厨房弹幕游戏', - ); - expect(demoMessage.textContent).toContain( - '讲解顺序:目标 -> 操作 -> 反馈 -> 胜负 -> 重开', - ); - expect(demoMessage.textContent).toContain( - '口播稿:先看目标提示,尝试移动/点击完成核心动作;看到得分、受击或状态反馈后,继续到胜利或失败;结束后确认能否一键重开', - ); - expect(demoMessage.textContent).toContain( - '当前演示状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', - ); - expect(demoMessage.textContent).toContain('最近试玩证据:暂无'); - expect(demoMessage.textContent).toContain( - '收反馈:操作是否明白;节奏是否太快;胜负是否清楚;视觉 / 音效是否帮助理解', - ); - expect(demoMessage.textContent).toContain( - '边界:只准备试玩讲解;不启动预览;不导出试玩包;不发布作品', - ); - expect(demoMessage.textContent).toContain( - '参考:/rules;/test-plan;/feedback;/share', - ); - expect(demoMessage.textContent).toContain('建议:/run'); - const demoMessageList = document.querySelector('.message-list'); - expect(demoMessageList).not.toBeNull(); - const demoDraftButtons = within(demoMessageList as HTMLElement).getAllByRole( - 'button', - { name: '启动预览' }, - ); - fireEvent.click(demoDraftButtons[demoDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeDemo.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeDemo.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeDemo.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeDemo.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeDemo.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeDemo.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeDemo.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeDemo.exportList); + it('预览快捷操作:/tutorial', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeTutorial = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + }; + await submitChat('/tutorial'); + const tutorialMessages = await screen.findAllByText(/新手引导:/); + const tutorialMessage = messageBubble( + tutorialMessages[tutorialMessages.length - 1], + ); + expect(tutorialMessage.textContent).toContain('项目:未命名游戏原型'); + expect(tutorialMessage.textContent).toContain( + '首屏目标:做一个厨房弹幕游戏', + ); + expect(tutorialMessage.textContent).toContain( + '首局 30 秒:看到目标;尝试操作;收到反馈;理解失败/胜利;能重开', + ); + expect(tutorialMessage.textContent).toContain( + '当前证据:原型入口 未见 trace 产物;最近 run 已通过 run-main-shortcut-trace;预览 未启动', + ); + expect(tutorialMessage.textContent).toContain( + '试玩任务:程序组 / Playtest 预览并试玩验收 · 待处理', + ); + expect(tutorialMessage.textContent).toContain( + '最近引导证据:Generator #1 · completed · 生成可运行草案', + ); + expect(tutorialMessage.textContent).toContain( + '需要补齐:首屏目标提示;操作提示;碰撞/得分反馈;失败或胜利提示;重开按钮', + ); + expect(tutorialMessage.textContent).toContain( + '参考:/rules;/playtest;/feedback;/pitch', + ); + expect(tutorialMessage.textContent).toContain( + '建议:/agent-resume 新手引导:在首屏加入目标、操作、反馈、失败重开提示', + ); + const tutorialMessageList = document.querySelector('.message-list'); + expect(tutorialMessageList).not.toBeNull(); + const tutorialDraftButtons = within( + tutorialMessageList as HTMLElement, + ).getAllByRole('button', { name: '补充新手引导' }); + fireEvent.click(tutorialDraftButtons[tutorialDraftButtons.length - 1]); + expect(await composerText()).toBe( + '/agent-resume 新手引导:在首屏加入目标、操作、反馈、失败重开提示', + ); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeTutorial.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeTutorial.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeTutorial.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeTutorial.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeTutorial.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeTutorial.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeTutorial.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeTutorial.exportList); + }); - const commandCountsBeforeControls = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - }; - await submitChat('/rules'); - expect(await screen.findByText(/玩法操作:/)).not.toBeNull(); - const controlMessages = screen.getAllByText(/玩法操作:/); - const controlMessage = messageBubble( - controlMessages[controlMessages.length - 1], - ); - expect(controlMessage.textContent).toContain('项目:未命名游戏原型'); - expect(controlMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); - expect(controlMessage.textContent).toContain( - '核心口径:目标;操作;胜负;重开;本地预览', - ); - expect(controlMessage.textContent).toContain( - '规则来源:game/game_design.md · 未见 trace 产物', - ); - expect(controlMessage.textContent).toContain( - '原型入口:game/index.html · 未见 trace 产物', - ); - expect(controlMessage.textContent).toContain( - 'design-foundation:设计实现组 / Gameplay 确定玩法规格与界面原型 · 待处理', - ); - expect(controlMessage.textContent).toContain( - 'code-prototype:程序组 / Code 生成可运行原型 · 待处理', - ); - expect(controlMessage.textContent).toContain( - 'preview-playtest:程序组 / Playtest 预览并试玩验收 · 待处理', - ); - expect(controlMessage.textContent).toContain( - '最近程序/试玩步骤:Generator #1 · completed · 生成可运行草案', - ); - expect(controlMessage.textContent).toContain( - '相关命令:/mvp;/playtest;/feedback', - ); - expect(controlMessage.textContent).toContain( - '建议:/agent-resume 操作说明:在首屏明确移动/点击操作、胜负目标、失败后重开方式', - ); - const controlMessageList = document.querySelector('.message-list'); - expect(controlMessageList).not.toBeNull(); - const controlDraftButtons = within( - controlMessageList as HTMLElement, - ).getAllByRole('button', { name: '补充操作说明' }); - fireEvent.click(controlDraftButtons[controlDraftButtons.length - 1]); - expect(await composerText()).toBe( - '/agent-resume 操作说明:在首屏明确移动/点击操作、胜负目标、失败后重开方式', - ); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeControls.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeControls.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeControls.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeControls.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeControls.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeControls.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeControls.exportPackage); + it('预览快捷操作:/mobile', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeMobile = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + }; + await submitChat('/mobile'); + const mobileMessages = await screen.findAllByText(/移动试玩:/); + const mobileMessage = messageBubble( + mobileMessages[mobileMessages.length - 1], + ); + expect(mobileMessage.textContent).toContain('项目:未命名游戏原型'); + expect(mobileMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); + expect(mobileMessage.textContent).toContain( + '输入方式:键盘 / 触屏都应能完成核心循环', + ); + expect(mobileMessage.textContent).toContain( + '当前证据:原型入口 未见 trace 产物;最近 run 已通过 run-main-shortcut-trace;预览 未启动', + ); + expect(mobileMessage.textContent).toContain( + '移动检查:触屏操作;响应式画布;横竖屏提示;按钮尺寸;失败/胜利重开', + ); + expect(mobileMessage.textContent).toContain( + 'code-prototype:程序组 / Code 生成可运行原型 · 待处理', + ); + expect(mobileMessage.textContent).toContain( + 'preview-playtest:程序组 / Playtest 预览并试玩验收 · 待处理', + ); + expect(mobileMessage.textContent).toContain( + '最近移动相关步骤:Generator #1 · completed · 生成可运行草案', + ); + expect(mobileMessage.textContent).toContain( + '参考:/rules;/tutorial;/playtest;/feedback', + ); + expect(mobileMessage.textContent).toContain( + '建议:/agent-resume 移动试玩:补充触屏操作、响应式画布、横竖屏提示、重开按钮', + ); + const mobileMessageList = document.querySelector('.message-list'); + expect(mobileMessageList).not.toBeNull(); + const mobileDraftButtons = within( + mobileMessageList as HTMLElement, + ).getAllByRole('button', { name: '补充移动试玩' }); + fireEvent.click(mobileDraftButtons[mobileDraftButtons.length - 1]); + expect(await composerText()).toBe( + '/agent-resume 移动试玩:补充触屏操作、响应式画布、横竖屏提示、重开按钮', + ); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeMobile.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeMobile.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeMobile.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeMobile.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeMobile.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeMobile.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeMobile.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeMobile.exportList); + }); - const commandCountsBeforeTutorial = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - }; - await submitChat('/tutorial'); - const tutorialMessages = await screen.findAllByText(/新手引导:/); - const tutorialMessage = messageBubble( - tutorialMessages[tutorialMessages.length - 1], - ); - expect(tutorialMessage.textContent).toContain('项目:未命名游戏原型'); - expect(tutorialMessage.textContent).toContain('首屏目标:做一个厨房弹幕游戏'); - expect(tutorialMessage.textContent).toContain( - '首局 30 秒:看到目标;尝试操作;收到反馈;理解失败/胜利;能重开', - ); - expect(tutorialMessage.textContent).toContain( - '当前证据:原型入口 未见 trace 产物;最近 run 已通过 run-main-shortcut-trace;预览 未启动', - ); - expect(tutorialMessage.textContent).toContain( - '试玩任务:程序组 / Playtest 预览并试玩验收 · 待处理', - ); - expect(tutorialMessage.textContent).toContain( - '最近引导证据:Generator #1 · completed · 生成可运行草案', - ); - expect(tutorialMessage.textContent).toContain( - '需要补齐:首屏目标提示;操作提示;碰撞/得分反馈;失败或胜利提示;重开按钮', - ); - expect(tutorialMessage.textContent).toContain( - '参考:/rules;/playtest;/feedback;/pitch', - ); - expect(tutorialMessage.textContent).toContain( - '建议:/agent-resume 新手引导:在首屏加入目标、操作、反馈、失败重开提示', - ); - const tutorialMessageList = document.querySelector('.message-list'); - expect(tutorialMessageList).not.toBeNull(); - const tutorialDraftButtons = within( - tutorialMessageList as HTMLElement, - ).getAllByRole('button', { name: '补充新手引导' }); - fireEvent.click(tutorialDraftButtons[tutorialDraftButtons.length - 1]); - expect(await composerText()).toBe( - '/agent-resume 新手引导:在首屏加入目标、操作、反馈、失败重开提示', - ); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeTutorial.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeTutorial.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeTutorial.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeTutorial.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeTutorial.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeTutorial.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeTutorial.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeTutorial.exportList); + it('预览快捷操作:/compatibility', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeCompatibility = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/compatibility'); + const compatibilityMessages = await screen.findAllByText(/兼容性说明:/); + const compatibilityMessage = messageBubble( + compatibilityMessages[compatibilityMessages.length - 1], + ); + expect(compatibilityMessage.textContent).toContain('项目:未命名游戏原型'); + expect(compatibilityMessage.textContent).toContain( + '目标:做一个厨房弹幕游戏', + ); + expect(compatibilityMessage.textContent).toContain( + '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', + ); + expect(compatibilityMessage.textContent).toContain( + '自检:game.static_smoke 已通过', + ); + expect(compatibilityMessage.textContent).toContain( + '输入兼容:键盘优先;触屏按 /mobile 复查', + ); + expect(compatibilityMessage.textContent).toContain( + '推荐环境:桌面 Chrome / Edge 最新版;本机 127.0.0.1 预览;移动浏览器只做早期体验', + ); + expect(compatibilityMessage.textContent).toContain( + '不承诺:旧浏览器、低端设备、离线模式、云存档、账号同步、手柄或多端数据一致', + ); + expect(compatibilityMessage.textContent).toContain( + '反馈口径:设备 / 浏览器 / 输入方式 / 截图或录屏;客户端检测到问题时会自动提示', + ); + expect(compatibilityMessage.textContent).toContain( + '参考:/mobile;/accessibility;/performance;/known-issues', + ); + expect(compatibilityMessage.textContent).toContain( + '边界:只准备兼容性说明;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', + ); + expect(compatibilityMessage.textContent).toContain('建议:/run'); + const compatibilityMessageList = document.querySelector('.message-list'); + expect(compatibilityMessageList).not.toBeNull(); + const compatibilityDraftButtons = within( + compatibilityMessageList as HTMLElement, + ).getAllByRole('button', { name: '启动试玩' }); + fireEvent.click( + compatibilityDraftButtons[compatibilityDraftButtons.length - 1], + ); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeCompatibility.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeCompatibility.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeCompatibility.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeCompatibility.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeCompatibility.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeCompatibility.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeCompatibility.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeCompatibility.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforeCompatibility.controlAgentRun); + }); - const commandCountsBeforeMobile = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - }; - await submitChat('/mobile'); - const mobileMessages = await screen.findAllByText(/移动试玩:/); - const mobileMessage = messageBubble( - mobileMessages[mobileMessages.length - 1], - ); - expect(mobileMessage.textContent).toContain('项目:未命名游戏原型'); - expect(mobileMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); - expect(mobileMessage.textContent).toContain( - '输入方式:键盘 / 触屏都应能完成核心循环', - ); - expect(mobileMessage.textContent).toContain( - '当前证据:原型入口 未见 trace 产物;最近 run 已通过 run-main-shortcut-trace;预览 未启动', - ); - expect(mobileMessage.textContent).toContain( - '移动检查:触屏操作;响应式画布;横竖屏提示;按钮尺寸;失败/胜利重开', - ); - expect(mobileMessage.textContent).toContain( - 'code-prototype:程序组 / Code 生成可运行原型 · 待处理', - ); - expect(mobileMessage.textContent).toContain( - 'preview-playtest:程序组 / Playtest 预览并试玩验收 · 待处理', - ); - expect(mobileMessage.textContent).toContain( - '最近移动相关步骤:Generator #1 · completed · 生成可运行草案', - ); - expect(mobileMessage.textContent).toContain( - '参考:/rules;/tutorial;/playtest;/feedback', - ); - expect(mobileMessage.textContent).toContain( - '建议:/agent-resume 移动试玩:补充触屏操作、响应式画布、横竖屏提示、重开按钮', - ); - const mobileMessageList = document.querySelector('.message-list'); - expect(mobileMessageList).not.toBeNull(); - const mobileDraftButtons = within( - mobileMessageList as HTMLElement, - ).getAllByRole('button', { name: '补充移动试玩' }); - fireEvent.click(mobileDraftButtons[mobileDraftButtons.length - 1]); - expect(await composerText()).toBe( - '/agent-resume 移动试玩:补充触屏操作、响应式画布、横竖屏提示、重开按钮', - ); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeMobile.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeMobile.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeMobile.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeMobile.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeMobile.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeMobile.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeMobile.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeMobile.exportList); + it('预览快捷操作:/accessibility', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeAccessibility = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/accessibility'); + const accessibilityMessages = + await screen.findAllByText(/可读性与无障碍:/); + const accessibilityMessage = messageBubble( + accessibilityMessages[accessibilityMessages.length - 1], + ); + expect(accessibilityMessage.textContent).toContain('项目:未命名游戏原型'); + expect(accessibilityMessage.textContent).toContain( + '目标:做一个厨房弹幕游戏', + ); + expect(accessibilityMessage.textContent).toContain( + '检查范围:文字可读;颜色对比;按钮/状态命名;键盘等价操作;可见焦点;非颜色唯一反馈;静音可玩', + ); + expect(accessibilityMessage.textContent).toContain( + '当前证据:原型入口 未见 trace 产物;最近 run 已通过 run-main-shortcut-trace;预览 未启动', + ); + expect(accessibilityMessage.textContent).toContain( + '补齐项:文字对比;清晰按钮标签;键盘等价操作;非颜色唯一反馈;静音可玩', + ); + expect(accessibilityMessage.textContent).toContain( + 'code-prototype:程序组 / Code 生成可运行原型 · 待处理', + ); + expect(accessibilityMessage.textContent).toContain( + 'quality-review:程序组 / Review 执行质量评审 · 待处理', + ); + expect(accessibilityMessage.textContent).toContain( + 'preview-readiness:程序组 / Preview 执行静态自检 · 待处理', + ); + expect(accessibilityMessage.textContent).toContain( + 'preview-playtest:程序组 / Playtest 预览并试玩验收 · 待处理', + ); + expect(accessibilityMessage.textContent).toContain( + '最近无障碍相关步骤:Evaluator #1 · passed · 质量评审通过', + ); + expect(accessibilityMessage.textContent).toContain( + '参考:/rules;/mobile;/tutorial;/qa;/playtest', + ); + expect(accessibilityMessage.textContent).toContain( + '建议:/agent-resume 可读性与无障碍:补充文字对比、清晰按钮标签、键盘等价操作、非颜色唯一反馈、静音可玩', + ); + const accessibilityMessageList = document.querySelector('.message-list'); + expect(accessibilityMessageList).not.toBeNull(); + const accessibilityDraftButtons = within( + accessibilityMessageList as HTMLElement, + ).getAllByRole('button', { name: '补充无障碍' }); + fireEvent.click( + accessibilityDraftButtons[accessibilityDraftButtons.length - 1], + ); + expect(await composerText()).toBe( + '/agent-resume 可读性与无障碍:补充文字对比、清晰按钮标签、键盘等价操作、非颜色唯一反馈、静音可玩', + ); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeAccessibility.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeAccessibility.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeAccessibility.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeAccessibility.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeAccessibility.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeAccessibility.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeAccessibility.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeAccessibility.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforeAccessibility.controlAgentRun); + }); - const commandCountsBeforeCompatibility = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/compatibility'); - const compatibilityMessages = await screen.findAllByText(/兼容性说明:/); - const compatibilityMessage = messageBubble( - compatibilityMessages[compatibilityMessages.length - 1], - ); - expect(compatibilityMessage.textContent).toContain('项目:未命名游戏原型'); - expect(compatibilityMessage.textContent).toContain( - '目标:做一个厨房弹幕游戏', - ); - expect(compatibilityMessage.textContent).toContain( - '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动', - ); - expect(compatibilityMessage.textContent).toContain( - '自检:game.static_smoke 已通过', - ); - expect(compatibilityMessage.textContent).toContain( - '输入兼容:键盘优先;触屏按 /mobile 复查', - ); - expect(compatibilityMessage.textContent).toContain( - '推荐环境:桌面 Chrome / Edge 最新版;本机 127.0.0.1 预览;移动浏览器只做早期体验', - ); - expect(compatibilityMessage.textContent).toContain( - '不承诺:旧浏览器、低端设备、离线模式、云存档、账号同步、手柄或多端数据一致', - ); - expect(compatibilityMessage.textContent).toContain( - '反馈口径:设备 / 浏览器 / 输入方式 / 截图或录屏;客户端检测到问题时会自动提示', - ); - expect(compatibilityMessage.textContent).toContain( - '参考:/mobile;/accessibility;/performance;/known-issues', - ); - expect(compatibilityMessage.textContent).toContain( - '边界:只准备兼容性说明;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', - ); - expect(compatibilityMessage.textContent).toContain('建议:/run'); - const compatibilityMessageList = document.querySelector('.message-list'); - expect(compatibilityMessageList).not.toBeNull(); - const compatibilityDraftButtons = within( - compatibilityMessageList as HTMLElement, - ).getAllByRole('button', { name: '启动试玩' }); - fireEvent.click( - compatibilityDraftButtons[compatibilityDraftButtons.length - 1], - ); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeCompatibility.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeCompatibility.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeCompatibility.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeCompatibility.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeCompatibility.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeCompatibility.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeCompatibility.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeCompatibility.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeCompatibility.controlAgentRun); + it('预览快捷操作:/localization', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeLocalization = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/localization'); + const localizationMessages = await screen.findAllByText(/本地化与文案:/); + const localizationMessage = messageBubble( + localizationMessages[localizationMessages.length - 1], + ); + expect(localizationMessage.textContent).toContain('项目:未命名游戏原型'); + expect(localizationMessage.textContent).toContain( + '目标:做一个厨房弹幕游戏', + ); + expect(localizationMessage.textContent).toContain( + '默认语言:简体中文;首版不承诺多语言', + ); + expect(localizationMessage.textContent).toContain( + '文案范围:标题;目标提示;操作按钮;状态反馈;失败/胜利;重开;发布简介', + ); + expect(localizationMessage.textContent).toContain( + '当前证据:最近 run 已通过 run-main-shortcut-trace;预览 未启动;发布说明 已生成', + ); + expect(localizationMessage.textContent).toContain( + 'design-foundation:设计实现组 / Gameplay 确定玩法规格与界面原型 · 待处理', + ); + expect(localizationMessage.textContent).toContain( + 'code-prototype:程序组 / Code 生成可运行原型 · 待处理', + ); + expect(localizationMessage.textContent).toContain( + 'quality-review:程序组 / Review 执行质量评审 · 待处理', + ); + expect(localizationMessage.textContent).toContain( + 'publish-package:运营组 / Publish 整理发布包装 · 待处理', + ); + expect(localizationMessage.textContent).toContain( + '检查口径:短句优先;动词一致;玩家术语统一;错误提示可复现;UI 文案避免开发解释', + ); + expect(localizationMessage.textContent).toContain( + '暂不做:英日韩等多语言包;自动翻译;地区化素材;语音本地化;商店长文案 A/B', + ); + expect(localizationMessage.textContent).toContain( + '最近文案相关步骤:Evaluator #1 · passed · 质量评审通过', + ); + expect(localizationMessage.textContent).toContain( + '参考:/rules;/tutorial;/listing;/faq;/known-issues', + ); + expect(localizationMessage.textContent).toContain( + '边界:只整理本地化与文案检查;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', + ); + expect(localizationMessage.textContent).toContain( + '建议:/read exports/README.md', + ); + const localizationMessageList = document.querySelector('.message-list'); + expect(localizationMessageList).not.toBeNull(); + const localizationDraftButtons = within( + localizationMessageList as HTMLElement, + ).getAllByRole('button', { name: '读发布说明' }); + fireEvent.click( + localizationDraftButtons[localizationDraftButtons.length - 1], + ); + expect(await composerText()).toBe('/read exports/README.md'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeLocalization.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeLocalization.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeLocalization.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeLocalization.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeLocalization.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeLocalization.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeLocalization.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeLocalization.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforeLocalization.controlAgentRun); + }); - const commandCountsBeforeAccessibility = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/accessibility'); - const accessibilityMessages = await screen.findAllByText(/可读性与无障碍:/); - const accessibilityMessage = messageBubble( - accessibilityMessages[accessibilityMessages.length - 1], - ); - expect(accessibilityMessage.textContent).toContain('项目:未命名游戏原型'); - expect(accessibilityMessage.textContent).toContain( - '目标:做一个厨房弹幕游戏', - ); - expect(accessibilityMessage.textContent).toContain( - '检查范围:文字可读;颜色对比;按钮/状态命名;键盘等价操作;可见焦点;非颜色唯一反馈;静音可玩', - ); - expect(accessibilityMessage.textContent).toContain( - '当前证据:原型入口 未见 trace 产物;最近 run 已通过 run-main-shortcut-trace;预览 未启动', - ); - expect(accessibilityMessage.textContent).toContain( - '补齐项:文字对比;清晰按钮标签;键盘等价操作;非颜色唯一反馈;静音可玩', - ); - expect(accessibilityMessage.textContent).toContain( - 'code-prototype:程序组 / Code 生成可运行原型 · 待处理', - ); - expect(accessibilityMessage.textContent).toContain( - 'quality-review:程序组 / Review 执行质量评审 · 待处理', - ); - expect(accessibilityMessage.textContent).toContain( - 'preview-readiness:程序组 / Preview 执行静态自检 · 待处理', - ); - expect(accessibilityMessage.textContent).toContain( - 'preview-playtest:程序组 / Playtest 预览并试玩验收 · 待处理', - ); - expect(accessibilityMessage.textContent).toContain( - '最近无障碍相关步骤:Evaluator #1 · passed · 质量评审通过', - ); - expect(accessibilityMessage.textContent).toContain( - '参考:/rules;/mobile;/tutorial;/qa;/playtest', - ); - expect(accessibilityMessage.textContent).toContain( - '建议:/agent-resume 可读性与无障碍:补充文字对比、清晰按钮标签、键盘等价操作、非颜色唯一反馈、静音可玩', - ); - const accessibilityMessageList = document.querySelector('.message-list'); - expect(accessibilityMessageList).not.toBeNull(); - const accessibilityDraftButtons = within( - accessibilityMessageList as HTMLElement, - ).getAllByRole('button', { name: '补充无障碍' }); - fireEvent.click( - accessibilityDraftButtons[accessibilityDraftButtons.length - 1], - ); - expect(await composerText()).toBe( - '/agent-resume 可读性与无障碍:补充文字对比、清晰按钮标签、键盘等价操作、非颜色唯一反馈、静音可玩', - ); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeAccessibility.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeAccessibility.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeAccessibility.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeAccessibility.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeAccessibility.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeAccessibility.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeAccessibility.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeAccessibility.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeAccessibility.controlAgentRun); + it('预览快捷操作:/performance', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforePerformance = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/performance'); + const performanceMessages = await screen.findAllByText(/性能与加载:/); + const performanceMessage = messageBubble( + performanceMessages[performanceMessages.length - 1], + ); + expect(performanceMessage.textContent).toContain('项目:未命名游戏原型'); + expect(performanceMessage.textContent).toContain( + '目标:做一个厨房弹幕游戏', + ); + expect(performanceMessage.textContent).toContain( + '当前证据:最近 run 已通过 run-main-shortcut-trace;预览 未启动;入口 未见 trace 产物;产物 3 个 / 416B;资产 2 个', + ); + expect(performanceMessage.textContent).toContain( + '检查范围:入口 HTML 自包含;首屏不空白;素材体积;主循环稳定;无远程依赖;预览启动', + ); + expect(performanceMessage.textContent).toContain( + 'exports/README.md · 128B · fnv1a64:exports', + ); + expect(performanceMessage.textContent).toContain( + '.agent/passes/pass-1/agenda.md · 96B · fnv1a64:agenda', + ); + expect(performanceMessage.textContent).toContain( + 'code-prototype:程序组 / Code 生成可运行原型 · 待处理', + ); + expect(performanceMessage.textContent).toContain( + 'preview-readiness:程序组 / Preview 执行静态自检 · 待处理', + ); + expect(performanceMessage.textContent).toContain( + 'preview-playtest:程序组 / Playtest 预览并试玩验收 · 待处理', + ); + expect(performanceMessage.textContent).toContain( + '最近性能相关步骤:Generator #1 · completed · 生成可运行草案', + ); + expect(performanceMessage.textContent).toContain( + '参考:/run-artifacts;/playtest;/qa;/export', + ); + expect(performanceMessage.textContent).toContain('建议:/run-artifacts'); + const performanceMessageList = document.querySelector('.message-list'); + expect(performanceMessageList).not.toBeNull(); + const performanceDraftButtons = within( + performanceMessageList as HTMLElement, + ).getAllByRole('button', { name: '列出 Run 产物' }); + fireEvent.click( + performanceDraftButtons[performanceDraftButtons.length - 1], + ); + expect(await composerText()).toBe('/run-artifacts'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforePerformance.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforePerformance.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforePerformance.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforePerformance.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforePerformance.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforePerformance.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforePerformance.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforePerformance.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforePerformance.controlAgentRun); + }); - const commandCountsBeforeLocalization = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/localization'); - const localizationMessages = await screen.findAllByText(/本地化与文案:/); - const localizationMessage = messageBubble( - localizationMessages[localizationMessages.length - 1], - ); - expect(localizationMessage.textContent).toContain('项目:未命名游戏原型'); - expect(localizationMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); - expect(localizationMessage.textContent).toContain( - '默认语言:简体中文;首版不承诺多语言', - ); - expect(localizationMessage.textContent).toContain( - '文案范围:标题;目标提示;操作按钮;状态反馈;失败/胜利;重开;发布简介', - ); - expect(localizationMessage.textContent).toContain( - '当前证据:最近 run 已通过 run-main-shortcut-trace;预览 未启动;发布说明 已生成', - ); - expect(localizationMessage.textContent).toContain( - 'design-foundation:设计实现组 / Gameplay 确定玩法规格与界面原型 · 待处理', - ); - expect(localizationMessage.textContent).toContain( - 'code-prototype:程序组 / Code 生成可运行原型 · 待处理', - ); - expect(localizationMessage.textContent).toContain( - 'quality-review:程序组 / Review 执行质量评审 · 待处理', - ); - expect(localizationMessage.textContent).toContain( - 'publish-package:运营组 / Publish 整理发布包装 · 待处理', - ); - expect(localizationMessage.textContent).toContain( - '检查口径:短句优先;动词一致;玩家术语统一;错误提示可复现;UI 文案避免开发解释', - ); - expect(localizationMessage.textContent).toContain( - '暂不做:英日韩等多语言包;自动翻译;地区化素材;语音本地化;商店长文案 A/B', - ); - expect(localizationMessage.textContent).toContain( - '最近文案相关步骤:Evaluator #1 · passed · 质量评审通过', - ); - expect(localizationMessage.textContent).toContain( - '参考:/rules;/tutorial;/listing;/faq;/known-issues', - ); - expect(localizationMessage.textContent).toContain( - '边界:只整理本地化与文案检查;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', - ); - expect(localizationMessage.textContent).toContain( - '建议:/read exports/README.md', - ); - const localizationMessageList = document.querySelector('.message-list'); - expect(localizationMessageList).not.toBeNull(); - const localizationDraftButtons = within( - localizationMessageList as HTMLElement, - ).getAllByRole('button', { name: '读发布说明' }); - fireEvent.click( - localizationDraftButtons[localizationDraftButtons.length - 1], - ); - expect(await composerText()).toBe('/read exports/README.md'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeLocalization.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeLocalization.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeLocalization.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeLocalization.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeLocalization.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeLocalization.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeLocalization.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeLocalization.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeLocalization.controlAgentRun); + it('预览快捷操作:/polish', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforePolish = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/polish'); + const polishMessages = await screen.findAllByText(/试玩前打磨:/); + const polishMessage = messageBubble( + polishMessages[polishMessages.length - 1], + ); + expect(polishMessage.textContent).toContain('项目:未命名游戏原型'); + expect(polishMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); + expect(polishMessage.textContent).toContain( + '当前证据:最近 run 已通过 run-main-shortcut-trace;预览 未启动;自检 已通过;资产 2 个(美术 1 / 音频 0)', + ); + expect(polishMessage.textContent).toContain( + '打磨范围:新手引导;移动试玩;可读性与无障碍;性能与加载;美术 / 音频素材;试玩反馈', + ); + expect(polishMessage.textContent).toContain( + '推荐顺序:/tutorial -> /mobile -> /accessibility -> /performance -> /credits -> /feedback', + ); + expect(polishMessage.textContent).toContain( + 'art-polish:美术组 / Polish 检查美术可用性 · 待处理', + ); + expect(polishMessage.textContent).toContain( + 'quality-review:程序组 / Review 执行质量评审 · 待处理', + ); + expect(polishMessage.textContent).toContain( + 'publish-package:运营组 / Publish 整理发布包装 · 待处理', + ); + expect(polishMessage.textContent).toContain( + '最近打磨相关步骤:Evaluator #1 · passed · 质量评审通过', + ); + expect(polishMessage.textContent).toContain( + '边界:只整理试玩前打磨清单;不读取文件;不启动预览;不导出试玩包;不写项目', + ); + expect(polishMessage.textContent).toContain( + '建议:/agent-resume 打磨:补齐新手引导、触屏操作、可读性、性能、素材署名和试玩反馈', + ); + const polishMessageList = document.querySelector('.message-list'); + expect(polishMessageList).not.toBeNull(); + const polishDraftButtons = within( + polishMessageList as HTMLElement, + ).getAllByRole('button', { name: '补充打磨' }); + fireEvent.click(polishDraftButtons[polishDraftButtons.length - 1]); + expect(await composerText()).toBe( + '/agent-resume 打磨:补齐新手引导、触屏操作、可读性、性能、素材署名和试玩反馈', + ); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforePolish.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforePolish.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforePolish.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforePolish.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforePolish.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforePolish.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforePolish.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforePolish.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforePolish.controlAgentRun); + }); - const commandCountsBeforePerformance = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/performance'); - const performanceMessages = await screen.findAllByText(/性能与加载:/); - const performanceMessage = messageBubble( - performanceMessages[performanceMessages.length - 1], - ); - expect(performanceMessage.textContent).toContain('项目:未命名游戏原型'); - expect(performanceMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); - expect(performanceMessage.textContent).toContain( - '当前证据:最近 run 已通过 run-main-shortcut-trace;预览 未启动;入口 未见 trace 产物;产物 3 个 / 416B;资产 2 个', - ); - expect(performanceMessage.textContent).toContain( - '检查范围:入口 HTML 自包含;首屏不空白;素材体积;主循环稳定;无远程依赖;预览启动', - ); - expect(performanceMessage.textContent).toContain( - 'exports/README.md · 128B · fnv1a64:exports', - ); - expect(performanceMessage.textContent).toContain( - '.agent/passes/pass-1/agenda.md · 96B · fnv1a64:agenda', - ); - expect(performanceMessage.textContent).toContain( - 'code-prototype:程序组 / Code 生成可运行原型 · 待处理', - ); - expect(performanceMessage.textContent).toContain( - 'preview-readiness:程序组 / Preview 执行静态自检 · 待处理', - ); - expect(performanceMessage.textContent).toContain( - 'preview-playtest:程序组 / Playtest 预览并试玩验收 · 待处理', - ); - expect(performanceMessage.textContent).toContain( - '最近性能相关步骤:Generator #1 · completed · 生成可运行草案', - ); - expect(performanceMessage.textContent).toContain( - '参考:/run-artifacts;/playtest;/qa;/export', - ); - expect(performanceMessage.textContent).toContain('建议:/run-artifacts'); - const performanceMessageList = document.querySelector('.message-list'); - expect(performanceMessageList).not.toBeNull(); - const performanceDraftButtons = within( - performanceMessageList as HTMLElement, - ).getAllByRole('button', { name: '列出 Run 产物' }); - fireEvent.click(performanceDraftButtons[performanceDraftButtons.length - 1]); - expect(await composerText()).toBe('/run-artifacts'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforePerformance.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforePerformance.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforePerformance.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforePerformance.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforePerformance.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforePerformance.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforePerformance.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforePerformance.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforePerformance.controlAgentRun); - - const commandCountsBeforePolish = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/polish'); - const polishMessages = await screen.findAllByText(/试玩前打磨:/); - const polishMessage = messageBubble( - polishMessages[polishMessages.length - 1], - ); - expect(polishMessage.textContent).toContain('项目:未命名游戏原型'); - expect(polishMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); - expect(polishMessage.textContent).toContain( - '当前证据:最近 run 已通过 run-main-shortcut-trace;预览 未启动;自检 已通过;资产 2 个(美术 1 / 音频 0)', - ); - expect(polishMessage.textContent).toContain( - '打磨范围:新手引导;移动试玩;可读性与无障碍;性能与加载;美术 / 音频素材;试玩反馈', - ); - expect(polishMessage.textContent).toContain( - '推荐顺序:/tutorial -> /mobile -> /accessibility -> /performance -> /credits -> /feedback', - ); - expect(polishMessage.textContent).toContain( - 'art-polish:美术组 / Polish 检查美术可用性 · 待处理', - ); - expect(polishMessage.textContent).toContain( - 'quality-review:程序组 / Review 执行质量评审 · 待处理', - ); - expect(polishMessage.textContent).toContain( - 'publish-package:运营组 / Publish 整理发布包装 · 待处理', - ); - expect(polishMessage.textContent).toContain( - '最近打磨相关步骤:Evaluator #1 · passed · 质量评审通过', - ); - expect(polishMessage.textContent).toContain( - '边界:只整理试玩前打磨清单;不读取文件;不启动预览;不导出试玩包;不写项目', - ); - expect(polishMessage.textContent).toContain( - '建议:/agent-resume 打磨:补齐新手引导、触屏操作、可读性、性能、素材署名和试玩反馈', - ); - const polishMessageList = document.querySelector('.message-list'); - expect(polishMessageList).not.toBeNull(); - const polishDraftButtons = within( - polishMessageList as HTMLElement, - ).getAllByRole('button', { name: '补充打磨' }); - fireEvent.click(polishDraftButtons[polishDraftButtons.length - 1]); - expect(await composerText()).toBe( - '/agent-resume 打磨:补齐新手引导、触屏操作、可读性、性能、素材署名和试玩反馈', - ); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforePolish.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforePolish.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforePolish.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforePolish.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforePolish.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforePolish.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforePolish.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforePolish.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforePolish.controlAgentRun); - - const commandCountsBeforeCredits = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - }; - await submitChat('/credits'); - const creditMessages = await screen.findAllByText(/素材署名:/); - const creditMessage = messageBubble( - creditMessages[creditMessages.length - 1], - ); - expect(creditMessage.textContent).toContain('当前资产:2 个'); - expect(creditMessage.textContent).toContain('来源分布:上传 1 / 画板 1'); - expect(creditMessage.textContent).toContain( - 'assets/uploads/hero.txt · text/plain · 上传 · 用户上传', - ); - expect(creditMessage.textContent).toContain( - 'assets/canvas-sync/tiles.png · image/png · 画板 · 画板 canvas-1', - ); - expect(creditMessage.textContent).toContain( - '需要确认:上传素材授权;生成素材模型;画板资源来源;本地试玩包保留来源口径', - ); - expect(creditMessage.textContent).toContain( - '参考:/assets;/art;/audio;/listing', - ); - expect(creditMessage.textContent).toContain('建议:/assets'); - const creditMessageList = document.querySelector('.message-list'); - expect(creditMessageList).not.toBeNull(); - const creditDraftButtons = within( - creditMessageList as HTMLElement, - ).getAllByRole('button', { name: '查看资产' }); - fireEvent.click(creditDraftButtons[creditDraftButtons.length - 1]); - expect(await composerText()).toBe('/assets'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeCredits.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeCredits.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeCredits.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeCredits.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeCredits.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeCredits.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeCredits.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeCredits.exportList); + it('预览快捷操作:/credits', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeCredits = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + }; + await submitChat('/credits'); + const creditMessages = await screen.findAllByText(/素材署名:/); + const creditMessage = messageBubble( + creditMessages[creditMessages.length - 1], + ); + expect(creditMessage.textContent).toContain('当前资产:2 个'); + expect(creditMessage.textContent).toContain('来源分布:上传 1 / 画板 1'); + expect(creditMessage.textContent).toContain( + 'assets/uploads/hero.txt · text/plain · 上传 · 用户上传', + ); + expect(creditMessage.textContent).toContain( + 'assets/canvas-sync/tiles.png · image/png · 画板 · 画板 canvas-1', + ); + expect(creditMessage.textContent).toContain( + '需要确认:上传素材授权;生成素材模型;画板资源来源;本地试玩包保留来源口径', + ); + expect(creditMessage.textContent).toContain( + '参考:/assets;/art;/audio;/listing', + ); + expect(creditMessage.textContent).toContain('建议:/assets'); + const creditMessageList = document.querySelector('.message-list'); + expect(creditMessageList).not.toBeNull(); + const creditDraftButtons = within( + creditMessageList as HTMLElement, + ).getAllByRole('button', { name: '查看资产' }); + fireEvent.click(creditDraftButtons[creditDraftButtons.length - 1]); + expect(await composerText()).toBe('/assets'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeCredits.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeCredits.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeCredits.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeCredits.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeCredits.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeCredits.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeCredits.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeCredits.exportList); + }); } diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/assert-project-tools-and-preview.ts b/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/assert-project-tools-and-preview.ts index 6f9d5a52e..22fe7f8e6 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/assert-project-tools-and-preview.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/assert-project-tools-and-preview.ts @@ -3,830 +3,931 @@ import { composerValue, expect, fireEvent, + it, screen, submitChat, waitFor, within, } from '../../harness'; -import type { PreviewShortcutInvoke } from './invoke-mock'; +import { openPreviewShortcutProject } from './actions'; import { messageBubble } from './message-bubble'; -export async function assertProjectToolsAndPreviewFlow( - invoke: PreviewShortcutInvoke, -) { - const commandCountsBeforeFeedback = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - }; - await submitChat('/feedback'); - const feedbackMessages = await screen.findAllByText(/试玩反馈:/); - const feedbackMessage = messageBubble( - feedbackMessages[feedbackMessages.length - 1], - ); - expect(feedbackMessage.textContent).toContain( - 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', - ); - expect(feedbackMessage.textContent).toContain('预览:未启动 · 建议 /run'); - expect(feedbackMessage.textContent).toContain( - '反馈方向:操作手感;胜负目标;难度;视觉 / 音效;重开路径', - ); - expect(feedbackMessage.textContent).toContain( - '反馈模板:/agent-resume 试玩反馈:保留…;调整…;新增…', - ); - expect(feedbackMessage.textContent).toContain( - '参考:/playtest;/qa;/changes', - ); - expect(feedbackMessage.textContent).toContain('建议:/run'); - const feedbackMessageList = document.querySelector('.message-list'); - expect(feedbackMessageList).not.toBeNull(); - const feedbackDraftButtons = within( - feedbackMessageList as HTMLElement, - ).getAllByRole('button', { name: '启动预览' }); - fireEvent.click(feedbackDraftButtons[feedbackDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeFeedback.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeFeedback.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeFeedback.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeFeedback.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeFeedback.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeFeedback.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeFeedback.exportPackage); - - const commandCountsBeforeRetention = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - controlAgentRun: invoke.mock.calls.filter( - ([command]) => command === 'control_agent_run', - ).length, - }; - await submitChat('/retention'); - const retentionMessages = await screen.findAllByText(/复玩观察:/); - const retentionMessage = messageBubble( - retentionMessages[retentionMessages.length - 1], - ); - expect(retentionMessage.textContent).toContain('项目:未命名游戏原型'); - expect(retentionMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); - expect(retentionMessage.textContent).toContain( - '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动;自检 已通过;试玩包 待导出', - ); - expect(retentionMessage.textContent).toContain( - '首轮样本:3-5 名测试者;每人 5-10 分钟;先不解释玩法,观察是否能自己完成首局', - ); - expect(retentionMessage.textContent).toContain( - '复玩信号:是否主动重开;失败后是否理解原因;第二局是否更快进入目标;是否愿意换难度/角色/关卡;是否能说出想保留的一点', - ); - expect(retentionMessage.textContent).toContain( - '资产与包装:素材 2 个;发布说明 已生成', - ); - expect(retentionMessage.textContent).toContain('最近试玩证据:暂无'); - expect(retentionMessage.textContent).toContain( - '记录模板:保留 1 项;调弱/调强 1 项;新增 1 项;必须修 1 项;是否愿意再玩一局', - ); - expect(retentionMessage.textContent).toContain( - '暂不做:真实埋点;留存报表;用户画像;A/B 实验;排行榜或账号留存', - ); - expect(retentionMessage.textContent).toContain( - '参考:/playtest;/feedback;/survey;/share;/known-issues', - ); - expect(retentionMessage.textContent).toContain( - '边界:只准备复玩观察清单;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', - ); - expect(retentionMessage.textContent).toContain('建议:/run'); - const retentionMessageList = document.querySelector('.message-list'); - expect(retentionMessageList).not.toBeNull(); - const retentionDraftButtons = within( - retentionMessageList as HTMLElement, - ).getAllByRole('button', { name: '启动试玩' }); - fireEvent.click(retentionDraftButtons[retentionDraftButtons.length - 1]); - expect(await composerText()).toBe('/run'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeRetention.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeRetention.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeRetention.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeRetention.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeRetention.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeRetention.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeRetention.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeRetention.exportList); - expect( - invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), - ).toHaveLength(commandCountsBeforeRetention.controlAgentRun); - - const commandCountsBeforeShare = { - manifestRead: invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ).length, - fileRead: invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length, - fileList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ).length, - runLocal: invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ).length, - startPreview: invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ).length, - openPreview: invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ).length, - exportPackage: invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ).length, - exportList: invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ).length, - }; - await submitChat('/share'); - const shareMessages = await screen.findAllByText(/试玩交付:/); - const shareMessage = messageBubble(shareMessages[shareMessages.length - 1]); - expect(shareMessage.textContent).toContain('项目:未命名游戏原型'); - expect(shareMessage.textContent).toContain('目录:/tmp/authorized-game'); - expect(shareMessage.textContent).toContain( - '原型:最近 run 已通过 run-main-shortcut-trace', - ); - expect(shareMessage.textContent).toContain('本地预览:未启动 · /run'); - expect(shareMessage.textContent).toContain('本地试玩包:待导出 · /export'); - expect(shareMessage.textContent).toContain( - '给测试者:玩法目标 / 操作 / 胜负 / 重开口径见 /rules', - ); - expect(shareMessage.textContent).toContain('反馈收集:/feedback'); - expect(shareMessage.textContent).toContain( - '交付边界:本地 ZIP 和本地预览;不上传云端;不生成公开分享链接', - ); - expect(shareMessage.textContent).toContain('建议:/export'); - const shareMessageList = document.querySelector('.message-list'); - expect(shareMessageList).not.toBeNull(); - const shareDraftButtons = within( - shareMessageList as HTMLElement, - ).getAllByRole('button', { name: '导出试玩包' }); - fireEvent.click(shareDraftButtons[shareDraftButtons.length - 1]); - expect(await composerText()).toBe('/export'); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'get_local_game_manifest', - ), - ).toHaveLength(commandCountsBeforeShare.manifestRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(commandCountsBeforeShare.fileRead); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_files', - ), - ).toHaveLength(commandCountsBeforeShare.fileList); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'run_limited_local_command', - ), - ).toHaveLength(commandCountsBeforeShare.runLocal); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'start_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeShare.startPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'activate_local_game_preview', - ), - ).toHaveLength(commandCountsBeforeShare.openPreview); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'export_local_project_package', - ), - ).toHaveLength(commandCountsBeforeShare.exportPackage); - expect( - invoke.mock.calls.filter( - ([command]) => command === 'list_local_project_export_packages', - ), - ).toHaveLength(commandCountsBeforeShare.exportList); - - fireEvent.click(screen.getByRole('button', { name: '权限' })); - expect(await screen.findByText(/策略:\.agent\/policy\.json/)).not.toBeNull(); - const composerInput = screen.getByLabelText('创作想法'); - fireEvent.click(screen.getByRole('button', { name: '索引确认' })); - expect(await composerValue(composerInput)).toBe( - '/policy-confirm project.index', - ); - fireEvent.click(screen.getByRole('button', { name: '资产登记确认' })); - expect(await composerValue(composerInput)).toBe( - '/policy-confirm asset.register', - ); - fireEvent.click(screen.getByRole('button', { name: '记忆写入确认' })); - expect(await composerValue(composerInput)).toBe( - '/policy-confirm memory.write', - ); - fireEvent.click(screen.getByRole('button', { name: '预览确认' })); - expect(await composerValue(composerInput)).toBe( - '/policy-confirm preview.start', - ); - fireEvent.click(screen.getByRole('button', { name: '打开预览确认' })); - expect(await composerValue(composerInput)).toBe( - '/policy-confirm preview.open', - ); - fireEvent.click(screen.getByRole('button', { name: '停止预览确认' })); - expect(await composerValue(composerInput)).toBe( - '/policy-confirm preview.stop', - ); - fireEvent.click(screen.getByRole('button', { name: 'Agent确认' })); - expect(await composerValue(composerInput)).toBe( - '/policy-confirm agent.run_status', - ); - fireEvent.click(screen.getByRole('button', { name: '读对话确认' })); - expect(await composerValue(composerInput)).toBe( - '/policy-confirm conversation.read', - ); - fireEvent.click(screen.getByRole('button', { name: '存对话确认' })); - expect(await composerValue(composerInput)).toBe( - '/policy-confirm conversation.write', - ); - expect(screen.queryByText('project.policy_write')).toBeNull(); - expect(invoke).not.toHaveBeenCalledWith( - 'write_project_permission_policy', - expect.anything(), - ); - - fireEvent.click(screen.getByRole('button', { name: '审计' })); - expect(await screen.findByText(/Agent v1 审计:/)).not.toBeNull(); - - fireEvent.click(screen.getByRole('button', { name: '资产' })); - expect( - await screen.findByText(/uploaded · assets\/uploads\/hero\.txt/), - ).not.toBeNull(); - expect( - within(screen.getByLabelText('最近项目资产')).getByText( - 'uploaded · text/plain · uploaded', - ), - ).not.toBeNull(); - const artManifestReadCountBefore = invoke.mock.calls.filter( - ([command, args]) => - command === 'read_local_project_file' && - args && - 'relativePath' in args && - args.relativePath === 'assets/manifest.art.json', - ).length; - await submitChat('/art'); - expect(await screen.findByText(/美术素材:1 个/)).not.toBeNull(); - expect(screen.getByText(/来源:画板 1/)).not.toBeNull(); - expect( - screen.getByText(/assets\/canvas-sync\/tiles\.png · image\/png · 画板/), - ).not.toBeNull(); - const visualDraftButtons = screen.getAllByRole('button', { - name: '读美术清单', +export function registerProjectToolsAndPreviewTests() { + it('预览快捷操作:/feedback', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeFeedback = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + }; + await submitChat('/feedback'); + const feedbackMessages = await screen.findAllByText(/试玩反馈:/); + const feedbackMessage = messageBubble( + feedbackMessages[feedbackMessages.length - 1], + ); + expect(feedbackMessage.textContent).toContain( + 'Run:run-main-shortcut-trace · passed / done · 1/3 轮 · evaluator-passed', + ); + expect(feedbackMessage.textContent).toContain('预览:未启动 · 建议 /run'); + expect(feedbackMessage.textContent).toContain( + '反馈方向:操作手感;胜负目标;难度;视觉 / 音效;重开路径', + ); + expect(feedbackMessage.textContent).toContain( + '反馈模板:/agent-resume 试玩反馈:保留…;调整…;新增…', + ); + expect(feedbackMessage.textContent).toContain( + '参考:/playtest;/qa;/changes', + ); + expect(feedbackMessage.textContent).toContain('建议:/run'); + const feedbackMessageList = document.querySelector('.message-list'); + expect(feedbackMessageList).not.toBeNull(); + const feedbackDraftButtons = within( + feedbackMessageList as HTMLElement, + ).getAllByRole('button', { name: '启动预览' }); + fireEvent.click(feedbackDraftButtons[feedbackDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeFeedback.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeFeedback.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeFeedback.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeFeedback.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeFeedback.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeFeedback.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeFeedback.exportPackage); }); - fireEvent.click(visualDraftButtons[visualDraftButtons.length - 1]); - expect(await composerValue(composerInput)).toBe( - '/read assets/manifest.art.json', - ); - expect( - invoke.mock.calls.filter( + + it('预览快捷操作:/retention', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeRetention = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + controlAgentRun: invoke.mock.calls.filter( + ([command]) => command === 'control_agent_run', + ).length, + }; + await submitChat('/retention'); + const retentionMessages = await screen.findAllByText(/复玩观察:/); + const retentionMessage = messageBubble( + retentionMessages[retentionMessages.length - 1], + ); + expect(retentionMessage.textContent).toContain('项目:未命名游戏原型'); + expect(retentionMessage.textContent).toContain('目标:做一个厨房弹幕游戏'); + expect(retentionMessage.textContent).toContain( + '当前状态:最近 run 已通过 run-main-shortcut-trace;预览 未启动;自检 已通过;试玩包 待导出', + ); + expect(retentionMessage.textContent).toContain( + '首轮样本:3-5 名测试者;每人 5-10 分钟;先不解释玩法,观察是否能自己完成首局', + ); + expect(retentionMessage.textContent).toContain( + '复玩信号:是否主动重开;失败后是否理解原因;第二局是否更快进入目标;是否愿意换难度/角色/关卡;是否能说出想保留的一点', + ); + expect(retentionMessage.textContent).toContain( + '资产与包装:素材 2 个;发布说明 已生成', + ); + expect(retentionMessage.textContent).toContain('最近试玩证据:暂无'); + expect(retentionMessage.textContent).toContain( + '记录模板:保留 1 项;调弱/调强 1 项;新增 1 项;必须修 1 项;是否愿意再玩一局', + ); + expect(retentionMessage.textContent).toContain( + '暂不做:真实埋点;留存报表;用户画像;A/B 实验;排行榜或账号留存', + ); + expect(retentionMessage.textContent).toContain( + '参考:/playtest;/feedback;/survey;/share;/known-issues', + ); + expect(retentionMessage.textContent).toContain( + '边界:只准备复玩观察清单;不读取文件;不启动或打开预览;不导出试玩包;不上传云端;不发布作品;不写项目', + ); + expect(retentionMessage.textContent).toContain('建议:/run'); + const retentionMessageList = document.querySelector('.message-list'); + expect(retentionMessageList).not.toBeNull(); + const retentionDraftButtons = within( + retentionMessageList as HTMLElement, + ).getAllByRole('button', { name: '启动试玩' }); + fireEvent.click(retentionDraftButtons[retentionDraftButtons.length - 1]); + expect(await composerText()).toBe('/run'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeRetention.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeRetention.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeRetention.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeRetention.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeRetention.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeRetention.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeRetention.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeRetention.exportList); + expect( + invoke.mock.calls.filter(([command]) => command === 'control_agent_run'), + ).toHaveLength(commandCountsBeforeRetention.controlAgentRun); + }); + + it('预览快捷操作:/share', async () => { + const invoke = await openPreviewShortcutProject(); + const commandCountsBeforeShare = { + manifestRead: invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ).length, + fileRead: invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ).length, + fileList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ).length, + runLocal: invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ).length, + startPreview: invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ).length, + openPreview: invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ).length, + exportPackage: invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ).length, + exportList: invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ).length, + }; + await submitChat('/share'); + const shareMessages = await screen.findAllByText(/试玩交付:/); + const shareMessage = messageBubble(shareMessages[shareMessages.length - 1]); + expect(shareMessage.textContent).toContain('项目:未命名游戏原型'); + expect(shareMessage.textContent).toContain('目录:/tmp/authorized-game'); + expect(shareMessage.textContent).toContain( + '原型:最近 run 已通过 run-main-shortcut-trace', + ); + expect(shareMessage.textContent).toContain('本地预览:未启动 · /run'); + expect(shareMessage.textContent).toContain('本地试玩包:待导出 · /export'); + expect(shareMessage.textContent).toContain( + '给测试者:玩法目标 / 操作 / 胜负 / 重开口径见 /rules', + ); + expect(shareMessage.textContent).toContain('反馈收集:/feedback'); + expect(shareMessage.textContent).toContain( + '交付边界:本地 ZIP 和本地预览;不上传云端;不生成公开分享链接', + ); + expect(shareMessage.textContent).toContain('建议:/export'); + const shareMessageList = document.querySelector('.message-list'); + expect(shareMessageList).not.toBeNull(); + const shareDraftButtons = within( + shareMessageList as HTMLElement, + ).getAllByRole('button', { name: '导出试玩包' }); + fireEvent.click(shareDraftButtons[shareDraftButtons.length - 1]); + expect(await composerText()).toBe('/export'); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'get_local_game_manifest', + ), + ).toHaveLength(commandCountsBeforeShare.manifestRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(commandCountsBeforeShare.fileRead); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_files', + ), + ).toHaveLength(commandCountsBeforeShare.fileList); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'run_limited_local_command', + ), + ).toHaveLength(commandCountsBeforeShare.runLocal); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'start_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeShare.startPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'activate_local_game_preview', + ), + ).toHaveLength(commandCountsBeforeShare.openPreview); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'export_local_project_package', + ), + ).toHaveLength(commandCountsBeforeShare.exportPackage); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'list_local_project_export_packages', + ), + ).toHaveLength(commandCountsBeforeShare.exportList); + }); + + it('预览快捷操作:权限操作只填入确认草稿', async () => { + const invoke = await openPreviewShortcutProject(); + fireEvent.click(screen.getByRole('button', { name: '权限' })); + expect( + await screen.findByText(/策略:\.agent\/policy\.json/), + ).not.toBeNull(); + const composerInput = screen.getByLabelText('创作想法'); + fireEvent.click(screen.getByRole('button', { name: '索引确认' })); + expect(await composerValue(composerInput)).toBe( + '/policy-confirm project.index', + ); + fireEvent.click(screen.getByRole('button', { name: '资产登记确认' })); + expect(await composerValue(composerInput)).toBe( + '/policy-confirm asset.register', + ); + fireEvent.click(screen.getByRole('button', { name: '记忆写入确认' })); + expect(await composerValue(composerInput)).toBe( + '/policy-confirm memory.write', + ); + fireEvent.click(screen.getByRole('button', { name: '预览确认' })); + expect(await composerValue(composerInput)).toBe( + '/policy-confirm preview.start', + ); + fireEvent.click(screen.getByRole('button', { name: '打开预览确认' })); + expect(await composerValue(composerInput)).toBe( + '/policy-confirm preview.open', + ); + fireEvent.click(screen.getByRole('button', { name: '停止预览确认' })); + expect(await composerValue(composerInput)).toBe( + '/policy-confirm preview.stop', + ); + fireEvent.click(screen.getByRole('button', { name: 'Agent确认' })); + expect(await composerValue(composerInput)).toBe( + '/policy-confirm agent.run_status', + ); + fireEvent.click(screen.getByRole('button', { name: '读对话确认' })); + expect(await composerValue(composerInput)).toBe( + '/policy-confirm conversation.read', + ); + fireEvent.click(screen.getByRole('button', { name: '存对话确认' })); + expect(await composerValue(composerInput)).toBe( + '/policy-confirm conversation.write', + ); + expect(screen.queryByText('project.policy_write')).toBeNull(); + expect(invoke).not.toHaveBeenCalledWith( + 'write_project_permission_policy', + expect.anything(), + ); + }); + + it('预览快捷操作:审计', async () => { + const invoke = await openPreviewShortcutProject(); + fireEvent.click(screen.getByRole('button', { name: '审计' })); + expect(await screen.findByText(/Agent v1 审计:/)).not.toBeNull(); + + expect(invoke).toHaveBeenCalledWith('get_local_game_manifest', { + projectPath: '/tmp/authorized-game', + commandId: 'agent.audit', + }); + }); + + it('预览快捷操作:资产列表与文件读取', async () => { + const invoke = await openPreviewShortcutProject(); + const composerInput = screen.getByLabelText('创作想法'); + fireEvent.click(screen.getByRole('button', { name: '资产' })); + expect( + await screen.findByText(/uploaded · assets\/uploads\/hero\.txt/), + ).not.toBeNull(); + expect( + within(screen.getByLabelText('最近项目资产')).getByText( + 'uploaded · text/plain · uploaded', + ), + ).not.toBeNull(); + fireEvent.click( + within(screen.getByLabelText('最近项目资产')).getByRole('button', { + name: 'assets/uploads/hero.txt', + }), + ); + expect( + await screen.findByText(/文件:assets\/uploads\/hero\.txt/), + ).not.toBeNull(); + fireEvent.click( + within(screen.getByLabelText('最近项目资产')).getByRole('button', { + name: '填入读取 assets/uploads/hero.txt', + }), + ); + expect(await composerValue(composerInput)).toBe( + '/read assets/uploads/hero.txt', + ); + + expect(invoke).toHaveBeenCalledWith('get_local_game_manifest', { + projectPath: '/tmp/authorized-game', + commandId: 'asset.list', + }); + }); + + it('预览快捷操作:/art', async () => { + const invoke = await openPreviewShortcutProject(); + const composerInput = screen.getByLabelText('创作想法'); + const artManifestReadCountBefore = invoke.mock.calls.filter( ([command, args]) => command === 'read_local_project_file' && args && 'relativePath' in args && args.relativePath === 'assets/manifest.art.json', - ), - ).toHaveLength(artManifestReadCountBefore); - const audioManifestReadCountBefore = invoke.mock.calls.filter( - ([command, args]) => - command === 'read_local_project_file' && - args && - 'relativePath' in args && - args.relativePath === 'assets/manifest.audio.json', - ).length; - await submitChat('/audio'); - expect(await screen.findByText(/音频素材:暂无登记音频/)).not.toBeNull(); - expect( - screen.getByText(/可先登记项目内音效,或把已有画板音频作为素材导入/), - ).not.toBeNull(); - const audioDraftButtons = screen.getAllByRole('button', { - name: '登记音效', + ).length; + await submitChat('/art'); + expect(await screen.findByText(/美术素材:1 个/)).not.toBeNull(); + expect(screen.getByText(/来源:画板 1/)).not.toBeNull(); + expect( + screen.getByText(/assets\/canvas-sync\/tiles\.png · image\/png · 画板/), + ).not.toBeNull(); + const visualDraftButtons = screen.getAllByRole('button', { + name: '读美术清单', + }); + fireEvent.click(visualDraftButtons[visualDraftButtons.length - 1]); + expect(await composerValue(composerInput)).toBe( + '/read assets/manifest.art.json', + ); + expect( + invoke.mock.calls.filter( + ([command, args]) => + command === 'read_local_project_file' && + args && + 'relativePath' in args && + args.relativePath === 'assets/manifest.art.json', + ), + ).toHaveLength(artManifestReadCountBefore); }); - fireEvent.click(audioDraftButtons[audioDraftButtons.length - 1]); - expect(await composerValue(composerInput)).toBe( - '/asset-register assets/audio/sfx.wav audio audio/wav', - ); - expect( - invoke.mock.calls.filter( + + it('预览快捷操作:/audio', async () => { + const invoke = await openPreviewShortcutProject(); + const composerInput = screen.getByLabelText('创作想法'); + const audioManifestReadCountBefore = invoke.mock.calls.filter( ([command, args]) => command === 'read_local_project_file' && args && 'relativePath' in args && args.relativePath === 'assets/manifest.audio.json', - ), - ).toHaveLength(audioManifestReadCountBefore); - for (const [buttonName, draftValue] of [ - ['读入口', '/read game/index.html'], - ['读设计', '/read game/game_design.md'], - ['读数值', '/read game/balance.json'], - ['读美术清单', '/read assets/manifest.art.json'], - ['读音频清单', '/read assets/manifest.audio.json'], - ['读发布说明', '/read exports/README.md'], - ] as const) { - fireEvent.click( - within(screen.getByLabelText('预览快捷操作')).getByRole('button', { - name: buttonName, + ).length; + await submitChat('/audio'); + expect(await screen.findByText(/音频素材:暂无登记音频/)).not.toBeNull(); + expect( + screen.getByText(/可先登记项目内音效,或把已有画板音频作为素材导入/), + ).not.toBeNull(); + const audioDraftButtons = screen.getAllByRole('button', { + name: '登记音效', + }); + fireEvent.click(audioDraftButtons[audioDraftButtons.length - 1]); + expect(await composerValue(composerInput)).toBe( + '/asset-register assets/audio/sfx.wav audio audio/wav', + ); + expect( + invoke.mock.calls.filter( + ([command, args]) => + command === 'read_local_project_file' && + args && + 'relativePath' in args && + args.relativePath === 'assets/manifest.audio.json', + ), + ).toHaveLength(audioManifestReadCountBefore); + }); + + it('预览快捷操作:产物读取草稿与焦点', async () => { + await openPreviewShortcutProject(); + const composerInput = screen.getByLabelText('创作想法'); + for (const [buttonName, draftValue] of [ + ['读入口', '/read game/index.html'], + ['读设计', '/read game/game_design.md'], + ['读数值', '/read game/balance.json'], + ['读美术清单', '/read assets/manifest.art.json'], + ['读音频清单', '/read assets/manifest.audio.json'], + ['读发布说明', '/read exports/README.md'], + ] as const) { + fireEvent.click( + within(screen.getByLabelText('预览快捷操作')).getByRole('button', { + name: buttonName, + }), + ); + expect(await composerValue(composerInput)).toBe(draftValue); + await waitFor(() => expect(document.activeElement).toBe(composerInput)); + } + await submitChat('/artifacts'); + expect(await screen.findByText(/常用生成产物:/)).not.toBeNull(); + expect(screen.getByText(/读入口 · game\/index\.html/)).not.toBeNull(); + expect(screen.getByText(/读发布说明 · exports\/README\.md/)).not.toBeNull(); + const readEntryButtons = screen.getAllByRole('button', { name: '读入口' }); + fireEvent.click(readEntryButtons[readEntryButtons.length - 1]); + expect(await composerValue(composerInput)).toBe('/read game/index.html'); + await waitFor(() => expect(document.activeElement).toBe(composerInput)); + }); + + it('预览快捷操作:/run-artifacts', async () => { + const invoke = await openPreviewShortcutProject(); + const composerInput = screen.getByLabelText('创作想法'); + await submitChat('/run-artifacts'); + const runArtifactsHeading = + await screen.findByText('最近 Run 产物读取命令:'); + expect(messageBubble(runArtifactsHeading).textContent).toContain( + 'exports/README.md · 128B · fnv1a64:exports:/read exports/README.md', + ); + fireEvent.click(screen.getByRole('button', { name: '读取首个 Run 产物' })); + expect(await composerValue(composerInput)).toBe('/read exports/README.md'); + expect(invoke).not.toHaveBeenCalledWith( + 'read_local_project_file', + expect.objectContaining({ + relativePath: 'exports/README.md', }), ); - expect(await composerValue(composerInput)).toBe(draftValue); - await waitFor(() => expect(document.activeElement).toBe(composerInput)); - } - await submitChat('/artifacts'); - expect(await screen.findByText(/常用生成产物:/)).not.toBeNull(); - expect(screen.getByText(/读入口 · game\/index\.html/)).not.toBeNull(); - expect(screen.getByText(/读发布说明 · exports\/README\.md/)).not.toBeNull(); - const readEntryButtons = screen.getAllByRole('button', { name: '读入口' }); - fireEvent.click(readEntryButtons[readEntryButtons.length - 1]); - expect(await composerValue(composerInput)).toBe('/read game/index.html'); - await waitFor(() => expect(document.activeElement).toBe(composerInput)); - await submitChat('/run-artifacts'); - const runArtifactsHeading = - await screen.findByText('最近 Run 产物读取命令:'); - expect(messageBubble(runArtifactsHeading).textContent).toContain( - 'exports/README.md · 128B · fnv1a64:exports:/read exports/README.md', - ); - fireEvent.click(screen.getByRole('button', { name: '读取首个 Run 产物' })); - expect(await composerValue(composerInput)).toBe('/read exports/README.md'); - expect(invoke).not.toHaveBeenCalledWith( - 'read_local_project_file', - expect.objectContaining({ - relativePath: 'exports/README.md', - }), - ); - const fileReadCountBeforePasses = invoke.mock.calls.filter( - ([command]) => command === 'read_local_project_file', - ).length; - await submitChat('/passes'); - const passArtifactMessages = - await screen.findAllByText(/Agent 轮次产物读取命令:/); - const passArtifactMessage = messageBubble( - passArtifactMessages[passArtifactMessages.length - 1], - ); - expect(passArtifactMessage.textContent).toContain( - '.agent/passes/pass-1/agenda.md · 96B · fnv1a64:agenda', - ); - expect(passArtifactMessage.textContent).toContain( - '.agent/passes/pass-1/groups/art/asset.md · 192B · fnv1a64:art-asset', - ); - const passArtifactButtons = screen.getAllByRole('button', { - name: '读取首个轮次产物', }); - fireEvent.click(passArtifactButtons[passArtifactButtons.length - 1]); - expect(await composerValue(composerInput)).toBe( - '/read .agent/passes/pass-1/agenda.md', - ); - expect( - invoke.mock.calls.filter( + + it('预览快捷操作:/passes', async () => { + const invoke = await openPreviewShortcutProject(); + const composerInput = screen.getByLabelText('创作想法'); + const fileReadCountBeforePasses = invoke.mock.calls.filter( ([command]) => command === 'read_local_project_file', - ), - ).toHaveLength(fileReadCountBeforePasses); - await submitChat('/logs'); - expect(await screen.findByText(/常用日志读取命令:/)).not.toBeNull(); - expect( - screen.getByText(/读命令日志 · \.agent\/logs\/command\.log/), - ).not.toBeNull(); - expect( - screen.getByText(/读 Agent 日志 · \.agent\/logs\/agent\.log/), - ).not.toBeNull(); - const commandLogReadCountBefore = invoke.mock.calls.filter( - ([command, args]) => - command === 'read_local_project_file' && - args && - 'relativePath' in args && - args.relativePath === '.agent/logs/command.log', - ).length; - const messageList = document.querySelector('.message-list'); - expect(messageList).not.toBeNull(); - fireEvent.click( - within(messageList as HTMLElement).getByRole('button', { - name: '读命令日志', - }), - ); - expect(await composerValue(composerInput)).toBe( - '/read .agent/logs/command.log', - ); - expect( - invoke.mock.calls.filter( + ).length; + await submitChat('/passes'); + const passArtifactMessages = + await screen.findAllByText(/Agent 轮次产物读取命令:/); + const passArtifactMessage = messageBubble( + passArtifactMessages[passArtifactMessages.length - 1], + ); + expect(passArtifactMessage.textContent).toContain( + '.agent/passes/pass-1/agenda.md · 96B · fnv1a64:agenda', + ); + expect(passArtifactMessage.textContent).toContain( + '.agent/passes/pass-1/groups/art/asset.md · 192B · fnv1a64:art-asset', + ); + const passArtifactButtons = screen.getAllByRole('button', { + name: '读取首个轮次产物', + }); + fireEvent.click(passArtifactButtons[passArtifactButtons.length - 1]); + expect(await composerValue(composerInput)).toBe( + '/read .agent/passes/pass-1/agenda.md', + ); + expect( + invoke.mock.calls.filter( + ([command]) => command === 'read_local_project_file', + ), + ).toHaveLength(fileReadCountBeforePasses); + }); + + it('预览快捷操作:/logs', async () => { + const invoke = await openPreviewShortcutProject(); + const composerInput = screen.getByLabelText('创作想法'); + await submitChat('/logs'); + expect(await screen.findByText(/常用日志读取命令:/)).not.toBeNull(); + expect( + screen.getByText(/读命令日志 · \.agent\/logs\/command\.log/), + ).not.toBeNull(); + expect( + screen.getByText(/读 Agent 日志 · \.agent\/logs\/agent\.log/), + ).not.toBeNull(); + const commandLogReadCountBefore = invoke.mock.calls.filter( ([command, args]) => command === 'read_local_project_file' && args && 'relativePath' in args && args.relativePath === '.agent/logs/command.log', - ), - ).toHaveLength(commandLogReadCountBefore); - fireEvent.click(screen.getByRole('button', { name: '打开画板' })); - expect(await composerValue(composerInput)).toBe('/canvas '); - await waitFor(() => expect(document.activeElement).toBe(composerInput)); - fireEvent.click(screen.getByRole('button', { name: '同步画板' })); - expect(await composerValue(composerInput)).toBe('/sync-canvas-project '); - await waitFor(() => expect(document.activeElement).toBe(composerInput)); - fireEvent.click(screen.getByRole('button', { name: '生成美术' })); - expect(await composerValue(composerInput)).toBe('/generate-art '); - await waitFor(() => expect(document.activeElement).toBe(composerInput)); - fireEvent.click( - within(screen.getByLabelText('预览快捷操作')).getByRole('button', { - name: '登记音效', - }), - ); - expect(await composerValue(composerInput)).toBe( - '/asset-register assets/audio/sfx.wav audio audio/wav', - ); - await waitFor(() => expect(document.activeElement).toBe(composerInput)); - fireEvent.click(screen.getByRole('button', { name: '导入画板资产' })); - expect(await composerValue(composerInput)).toBe('/import-canvas-asset '); - await waitFor(() => expect(document.activeElement).toBe(composerInput)); - fireEvent.click(screen.getByRole('button', { name: '导入画板音频' })); - expect(await composerValue(composerInput)).toBe( - '/import-canvas-asset assets/audio/sfx.wav ', - ); - await waitFor(() => expect(document.activeElement).toBe(composerInput)); - fireEvent.click( - within(screen.getByLabelText('最近项目资产')).getByRole('button', { - name: 'assets/uploads/hero.txt', - }), - ); - expect( - await screen.findByText(/文件:assets\/uploads\/hero\.txt/), - ).not.toBeNull(); - fireEvent.click( - within(screen.getByLabelText('最近项目资产')).getByRole('button', { - name: '填入读取 assets/uploads/hero.txt', - }), - ); - expect(await composerValue(composerInput)).toBe( - '/read assets/uploads/hero.txt', - ); - - fireEvent.click(screen.getByRole('button', { name: '任务' })); - expect(await screen.findByText(/任务拆分:/)).not.toBeNull(); - expect(screen.getByText(/下一步:设计实现组 \/ Director/)).not.toBeNull(); - - fireEvent.click(screen.getByRole('button', { name: 'Trace' })); - const traceHeading = await screen.findByText( - (content, element) => - element?.tagName === 'P' && - content.trimStart().startsWith('Run:run-main-shortcut-trace'), - ); - expect(messageBubble(traceHeading).textContent).toContain('产物快照:'); - expect(screen.getByText(/产物快照:/)).not.toBeNull(); - expect( - screen.getByText(/exports\/README\.md · fnv1a64:exports/), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '读取首个产物' })); - expect(await composerValue(composerInput)).toBe('/read exports/README.md'); - expect(invoke).not.toHaveBeenCalledWith('read_local_project_file', { - projectPath: '/tmp/authorized-game', - relativePath: 'exports/README.md', - commandId: 'file.read', + ).length; + const messageList = document.querySelector('.message-list'); + expect(messageList).not.toBeNull(); + fireEvent.click( + within(messageList as HTMLElement).getByRole('button', { + name: '读命令日志', + }), + ); + expect(await composerValue(composerInput)).toBe( + '/read .agent/logs/command.log', + ); + expect( + invoke.mock.calls.filter( + ([command, args]) => + command === 'read_local_project_file' && + args && + 'relativePath' in args && + args.relativePath === '.agent/logs/command.log', + ), + ).toHaveLength(commandLogReadCountBefore); }); - fireEvent.click(screen.getByRole('button', { name: '文件' })); - expect(await screen.findByText(/本地项目文件:/)).not.toBeNull(); - fireEvent.click( - within(screen.getByLabelText('最近项目文件')).getByRole('button', { - name: 'game/index.html', - }), - ); - expect(await screen.findByText(/文件:game\/index\.html/)).not.toBeNull(); - fireEvent.click( - within(screen.getByLabelText('最近项目文件')).getByRole('button', { - name: '填入读取 game/index.html', - }), - ); - expect(await composerValue(composerInput)).toBe('/read game/index.html'); - fireEvent.click( - within(screen.getByLabelText('最近项目文件')).getByRole('button', { - name: '登记资产 game/index.html', - }), - ); - expect(await composerValue(composerInput)).toBe( - '/asset-register game/index.html document text/html', - ); - expect(invoke).not.toHaveBeenCalledWith( - 'register_local_asset', - expect.anything(), - ); + it('预览快捷操作:画板与素材操作草稿', async () => { + await openPreviewShortcutProject(); + const composerInput = screen.getByLabelText('创作想法'); + fireEvent.click(screen.getByRole('button', { name: '打开画板' })); + expect(await composerValue(composerInput)).toBe('/canvas '); + await waitFor(() => expect(document.activeElement).toBe(composerInput)); + fireEvent.click(screen.getByRole('button', { name: '同步画板' })); + expect(await composerValue(composerInput)).toBe('/sync-canvas-project '); + await waitFor(() => expect(document.activeElement).toBe(composerInput)); + fireEvent.click(screen.getByRole('button', { name: '生成美术' })); + expect(await composerValue(composerInput)).toBe('/generate-art '); + await waitFor(() => expect(document.activeElement).toBe(composerInput)); + fireEvent.click( + within(screen.getByLabelText('预览快捷操作')).getByRole('button', { + name: '登记音效', + }), + ); + expect(await composerValue(composerInput)).toBe( + '/asset-register assets/audio/sfx.wav audio audio/wav', + ); + await waitFor(() => expect(document.activeElement).toBe(composerInput)); + fireEvent.click(screen.getByRole('button', { name: '导入画板资产' })); + expect(await composerValue(composerInput)).toBe('/import-canvas-asset '); + await waitFor(() => expect(document.activeElement).toBe(composerInput)); + fireEvent.click(screen.getByRole('button', { name: '导入画板音频' })); + expect(await composerValue(composerInput)).toBe( + '/import-canvas-asset assets/audio/sfx.wav ', + ); + await waitFor(() => expect(document.activeElement).toBe(composerInput)); + }); - fireEvent.click(screen.getByRole('button', { name: '索引' })); - expect(await screen.findByText(/索引:2 个文件,384B/)).not.toBeNull(); + it('预览快捷操作:任务列表', async () => { + const invoke = await openPreviewShortcutProject(); + fireEvent.click(screen.getByRole('button', { name: '任务' })); + expect(await screen.findByText(/任务拆分:/)).not.toBeNull(); + expect(screen.getByText(/下一步:设计实现组 \/ Director/)).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '记忆' })); - expect(await screen.findByText(/长期记忆:/)).not.toBeNull(); - expect(screen.getByText(/保留厨房主题/)).not.toBeNull(); + expect(invoke).toHaveBeenCalledWith('get_local_game_manifest', { + projectPath: '/tmp/authorized-game', + commandId: 'task.list', + }); + }); - fireEvent.click(screen.getByRole('button', { name: '短期记忆' })); - expect(await screen.findByText(/短期记忆:/)).not.toBeNull(); - expect(screen.getByText(/本轮偏动作反馈/)).not.toBeNull(); + it('预览快捷操作:Trace 与产物读取草稿', async () => { + const invoke = await openPreviewShortcutProject(); + const composerInput = screen.getByLabelText('创作想法'); + fireEvent.click(screen.getByRole('button', { name: 'Trace' })); + const traceHeading = await screen.findByText( + (content, element) => + element?.tagName === 'P' && + content.trimStart().startsWith('Run:run-main-shortcut-trace'), + ); + expect(messageBubble(traceHeading).textContent).toContain('产物快照:'); + expect(screen.getByText(/产物快照:/)).not.toBeNull(); + expect( + screen.getByText(/exports\/README\.md · fnv1a64:exports/), + ).not.toBeNull(); + fireEvent.click(screen.getByRole('button', { name: '读取首个产物' })); + expect(await composerValue(composerInput)).toBe('/read exports/README.md'); + expect(invoke).not.toHaveBeenCalledWith('read_local_project_file', { + projectPath: '/tmp/authorized-game', + relativePath: 'exports/README.md', + commandId: 'file.read', + }); - fireEvent.click(screen.getByRole('button', { name: '黑板' })); - expect(await screen.findByText(/黑板记忆:/)).not.toBeNull(); - expect(screen.getByText(/跨 agent 共享约束/)).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '记到黑板' })); - expect(await composerValue(composerInput)).toBe('/remember blackboard '); - fireEvent.click(screen.getByRole('button', { name: '覆盖黑板' })); - expect(await composerValue(composerInput)).toBe('/memory-set blackboard '); - fireEvent.click(screen.getByRole('button', { name: '清空黑板' })); - expect(await composerValue(composerInput)).toBe('/forget-memory blackboard'); + expect(invoke).toHaveBeenCalledWith('read_local_project_file', { + projectPath: '/tmp/authorized-game', + relativePath: '.agent/run.latest.json', + commandId: 'agent.trace_read', + }); + }); - fireEvent.click(screen.getByRole('button', { name: '快照' })); - expect(await screen.findByText('project.checkpoint')).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText(/已保存 checkpoint:checkpoint-main/), - ).not.toBeNull(); + it('预览快捷操作:文件列表与登记草稿', async () => { + const invoke = await openPreviewShortcutProject(); + const composerInput = screen.getByLabelText('创作想法'); + fireEvent.click(screen.getByRole('button', { name: '文件' })); + expect(await screen.findByText(/本地项目文件:/)).not.toBeNull(); + fireEvent.click( + within(screen.getByLabelText('最近项目文件')).getByRole('button', { + name: 'game/index.html', + }), + ); + expect(await screen.findByText(/文件:game\/index\.html/)).not.toBeNull(); + fireEvent.click( + within(screen.getByLabelText('最近项目文件')).getByRole('button', { + name: '填入读取 game/index.html', + }), + ); + expect(await composerValue(composerInput)).toBe('/read game/index.html'); + fireEvent.click( + within(screen.getByLabelText('最近项目文件')).getByRole('button', { + name: '登记资产 game/index.html', + }), + ); + expect(await composerValue(composerInput)).toBe( + '/asset-register game/index.html document text/html', + ); + expect(invoke).not.toHaveBeenCalledWith( + 'register_local_asset', + expect.anything(), + ); - fireEvent.click(screen.getByRole('button', { name: '快照列表' })); - const checkpointList = await screen.findByLabelText('最近 checkpoint'); - expect(within(checkpointList).getByText('checkpoint-main')).not.toBeNull(); - expect(within(checkpointList).getByText(/3 个文件 · 256B/)).not.toBeNull(); - expect( - within(checkpointList).getByText(/createdAt 1700000002/), - ).not.toBeNull(); - expect( - within(checkpointList).getByRole('button', { - name: '对比 checkpoint-main', - }), - ).not.toBeNull(); - fireEvent.click( - within(checkpointList).getByRole('button', { - name: '填入对比 checkpoint-main', - }), - ); - expect(await composerValue(composerInput)).toBe('/diff checkpoint-main'); - fireEvent.click( - within(checkpointList).getByRole('button', { - name: '填入回滚 checkpoint-main', - }), - ); - expect(await composerValue(composerInput)).toBe('/restore checkpoint-main'); - fireEvent.click( - within(checkpointList).getByRole('button', { - name: '回滚 checkpoint-main', - }), - ); - expect(await screen.findByText(/project\.restore/)).not.toBeNull(); - expect( - screen.getByText( - '从 checkpoint-main 恢复 /tmp/authorized-game 的已跟踪项目文件', - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '取消' })); + expect(invoke).toHaveBeenCalledWith('list_local_project_files', { + projectPath: '/tmp/authorized-game', + }); + expect(invoke).toHaveBeenCalledWith('read_local_project_file', { + projectPath: '/tmp/authorized-game', + relativePath: 'game/index.html', + commandId: 'file.read', + }); + }); - fireEvent.click(screen.getByRole('button', { name: '白名单' })); - expect(await screen.findByText(/可运行受限命令:/)).not.toBeNull(); - expect(screen.getByText(/game\.static_smoke · 静态入口自检/)).not.toBeNull(); + it('预览快捷操作:索引', async () => { + const invoke = await openPreviewShortcutProject(); + fireEvent.click(screen.getByRole('button', { name: '索引' })); + expect(await screen.findByText(/索引:2 个文件,384B/)).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '静态自检' })); - expect(await screen.findByText('准备运行静态入口自检。')).not.toBeNull(); - expect(screen.getByText(/command\.run_limited/)).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect(await screen.findByText(/static smoke passed/)).not.toBeNull(); + expect(invoke).toHaveBeenCalledWith('build_local_project_index', { + projectPath: '/tmp/authorized-game', + }); + }); - fireEvent.click(screen.getByRole('button', { name: '运行' })); - expect(await screen.findByText('game.run_local')).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText( - /运行通过,已载入客户端运行视图:http:\/\/127\.0\.0\.1:3210\//, - ), - ).not.toBeNull(); + it('预览快捷操作:长期、短期与黑板记忆', async () => { + const invoke = await openPreviewShortcutProject(); + const composerInput = screen.getByLabelText('创作想法'); + fireEvent.click(screen.getByRole('button', { name: '记忆' })); + expect(await screen.findByText(/长期记忆:/)).not.toBeNull(); + expect(screen.getByText(/保留厨房主题/)).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '导出' })); - expect(await screen.findByText('project.export_package')).not.toBeNull(); - expect( - screen.getByText( - '从 /tmp/authorized-game/game、assets 和 exports/README.md 导出本地试玩包', - ), - ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText( - /已导出本地试玩包:exports\/playtest-package-unit\.zip/, - ), - ).not.toBeNull(); - const exportRevealButtons = screen.getAllByRole('button', { - name: '显示目录', - }); - fireEvent.click(exportRevealButtons[exportRevealButtons.length - 1]); - expect(await composerText()).toBe('/open-project'); + fireEvent.click(screen.getByRole('button', { name: '短期记忆' })); + expect(await screen.findByText(/短期记忆:/)).not.toBeNull(); + expect(screen.getByText(/本轮偏动作反馈/)).not.toBeNull(); - await submitChat('/export'); - await waitFor(() => - expect(screen.getAllByText('准备导出本地试玩包。').length).toBeGreaterThan( - 1, - ), - ); - expect(screen.getAllByText('project.export_package').length).toBeGreaterThan( - 0, - ); - fireEvent.click(screen.getByRole('button', { name: '取消' })); + fireEvent.click(screen.getByRole('button', { name: '黑板' })); + expect(await screen.findByText(/黑板记忆:/)).not.toBeNull(); + expect(screen.getByText(/跨 agent 共享约束/)).not.toBeNull(); + fireEvent.click(screen.getByRole('button', { name: '记到黑板' })); + expect(await composerValue(composerInput)).toBe('/remember blackboard '); + fireEvent.click(screen.getByRole('button', { name: '覆盖黑板' })); + expect(await composerValue(composerInput)).toBe('/memory-set blackboard '); + fireEvent.click(screen.getByRole('button', { name: '清空黑板' })); + expect(await composerValue(composerInput)).toBe( + '/forget-memory blackboard', + ); - await submitChat('/exports'); - expect(await screen.findByText(/本地试玩包:/)).not.toBeNull(); - expect( - screen.getByText(/exports\/playtest-package-002\.zip · 2048B/), - ).not.toBeNull(); - expect( - screen.getByText(/exports\/playtest-package-001\.zip · 1024B/), - ).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('list_local_project_export_packages', { - projectPath: '/tmp/authorized-game', + expect(invoke).toHaveBeenCalledWith('read_local_game_memory', { + projectPath: '/tmp/authorized-game', + scope: 'long', + }); + expect(invoke).toHaveBeenCalledWith('read_local_game_memory', { + projectPath: '/tmp/authorized-game', + scope: 'short', + }); + expect(invoke).toHaveBeenCalledWith('read_local_game_memory', { + projectPath: '/tmp/authorized-game', + scope: 'blackboard', + }); }); - const exportListRevealButtons = screen.getAllByRole('button', { - name: '显示目录', - }); - fireEvent.click(exportListRevealButtons[exportListRevealButtons.length - 1]); - expect(await composerText()).toBe('/open-project'); - fireEvent.click(screen.getByRole('button', { name: '启动预览' })); - expect(await screen.findByText('preview.start')).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText( - /预览已在客户端运行视图启动:http:\/\/127\.0\.0\.1:3210\//, - ), - ).not.toBeNull(); + it('预览快捷操作:快照保存与回滚取消', async () => { + const invoke = await openPreviewShortcutProject(); + const composerInput = screen.getByLabelText('创作想法'); + fireEvent.click(screen.getByRole('button', { name: '快照' })); + expect(await screen.findByText('project.checkpoint')).not.toBeNull(); + fireEvent.click(screen.getByRole('button', { name: '确认' })); + expect( + await screen.findByText(/已保存 checkpoint:checkpoint-main/), + ).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '打开预览' })); - expect(await screen.findByText('preview.open')).not.toBeNull(); - fireEvent.click(screen.getByRole('button', { name: '确认' })); - expect( - await screen.findByText('已切换到客户端运行视图:http://127.0.0.1:3210/'), - ).not.toBeNull(); + fireEvent.click(screen.getByRole('button', { name: '快照列表' })); + const checkpointList = await screen.findByLabelText('最近 checkpoint'); + expect(within(checkpointList).getByText('checkpoint-main')).not.toBeNull(); + expect(within(checkpointList).getByText(/3 个文件 · 256B/)).not.toBeNull(); + expect( + within(checkpointList).getByText(/createdAt 1700000002/), + ).not.toBeNull(); + expect( + within(checkpointList).getByRole('button', { + name: '对比 checkpoint-main', + }), + ).not.toBeNull(); + fireEvent.click( + within(checkpointList).getByRole('button', { + name: '填入对比 checkpoint-main', + }), + ); + expect(await composerValue(composerInput)).toBe('/diff checkpoint-main'); + fireEvent.click( + within(checkpointList).getByRole('button', { + name: '填入回滚 checkpoint-main', + }), + ); + expect(await composerValue(composerInput)).toBe('/restore checkpoint-main'); + fireEvent.click( + within(checkpointList).getByRole('button', { + name: '回滚 checkpoint-main', + }), + ); + expect(await screen.findByText(/project\.restore/)).not.toBeNull(); + expect( + screen.getByText( + '从 checkpoint-main 恢复 /tmp/authorized-game 的已跟踪项目文件', + ), + ).not.toBeNull(); + fireEvent.click(screen.getByRole('button', { name: '取消' })); - fireEvent.click(screen.getByRole('button', { name: '预览状态' })); - expect( - await screen.findByText('预览运行中:http://127.0.0.1:3210/'), - ).not.toBeNull(); - expect(screen.getByText('preview: 运行中:127.0.0.1:3210')).not.toBeNull(); + expect(invoke).toHaveBeenCalledWith('create_local_project_checkpoint', { + projectPath: '/tmp/authorized-game', + }); + }); - fireEvent.click(screen.getByRole('button', { name: '停止预览' })); - expect(await screen.findByText('预览已停止。')).not.toBeNull(); - expect(screen.getByText('preview: 已停止')).not.toBeNull(); - expect(invoke).toHaveBeenCalledWith('run_limited_local_command', { - projectPath: '/tmp/authorized-game', - commandId: 'game.static_smoke', + it('预览快捷操作:静态自检与本地运行', async () => { + const invoke = await openPreviewShortcutProject(); + fireEvent.click(screen.getByRole('button', { name: '白名单' })); + expect(await screen.findByText(/可运行受限命令:/)).not.toBeNull(); + expect( + screen.getByText(/game\.static_smoke · 静态入口自检/), + ).not.toBeNull(); + + fireEvent.click(screen.getByRole('button', { name: '静态自检' })); + expect(await screen.findByText('准备运行静态入口自检。')).not.toBeNull(); + expect(screen.getByText(/command\.run_limited/)).not.toBeNull(); + fireEvent.click(screen.getByRole('button', { name: '确认' })); + expect(await screen.findByText(/static smoke passed/)).not.toBeNull(); + + fireEvent.click(screen.getByRole('button', { name: '运行' })); + expect(await screen.findByText('game.run_local')).not.toBeNull(); + fireEvent.click(screen.getByRole('button', { name: '确认' })); + expect( + await screen.findByText( + /运行通过,已载入客户端运行视图:http:\/\/127\.0\.0\.1:3210\//, + ), + ).not.toBeNull(); + + expect(invoke).toHaveBeenCalledWith('run_limited_local_command', { + projectPath: '/tmp/authorized-game', + commandId: 'game.static_smoke', + }); }); - expect(invoke).toHaveBeenCalledWith('get_local_game_manifest', { - projectPath: '/tmp/authorized-game', - commandId: 'asset.list', + + it('预览快捷操作:导出确认、取消与包列表', async () => { + const invoke = await openPreviewShortcutProject(); + fireEvent.click(screen.getByRole('button', { name: '导出' })); + expect(await screen.findByText('project.export_package')).not.toBeNull(); + expect( + screen.getByText( + '从 /tmp/authorized-game/game、assets 和 exports/README.md 导出本地试玩包', + ), + ).not.toBeNull(); + fireEvent.click(screen.getByRole('button', { name: '确认' })); + expect( + await screen.findByText( + /已导出本地试玩包:exports\/playtest-package-unit\.zip/, + ), + ).not.toBeNull(); + const exportRevealButtons = screen.getAllByRole('button', { + name: '显示目录', + }); + fireEvent.click(exportRevealButtons[exportRevealButtons.length - 1]); + expect(await composerText()).toBe('/open-project'); + + await submitChat('/export'); + await waitFor(() => + expect( + screen.getAllByText('准备导出本地试玩包。').length, + ).toBeGreaterThan(1), + ); + expect( + screen.getAllByText('project.export_package').length, + ).toBeGreaterThan(0); + fireEvent.click(screen.getByRole('button', { name: '取消' })); + + await submitChat('/exports'); + expect(await screen.findByText(/本地试玩包:/)).not.toBeNull(); + expect( + screen.getByText(/exports\/playtest-package-002\.zip · 2048B/), + ).not.toBeNull(); + expect( + screen.getByText(/exports\/playtest-package-001\.zip · 1024B/), + ).not.toBeNull(); + expect(invoke).toHaveBeenCalledWith('list_local_project_export_packages', { + projectPath: '/tmp/authorized-game', + }); + const exportListRevealButtons = screen.getAllByRole('button', { + name: '显示目录', + }); + fireEvent.click( + exportListRevealButtons[exportListRevealButtons.length - 1], + ); + expect(await composerText()).toBe('/open-project'); + + expect(invoke).toHaveBeenCalledWith('export_local_project_package', { + projectPath: '/tmp/authorized-game', + }); }); - expect(invoke).toHaveBeenCalledWith('get_local_game_manifest', { - projectPath: '/tmp/authorized-game', - commandId: 'task.list', - }); - expect(invoke).toHaveBeenCalledWith('get_local_game_manifest', { - projectPath: '/tmp/authorized-game', - commandId: 'agent.audit', - }); - expect(invoke).toHaveBeenCalledWith('get_game_creation_agent_capabilities'); - expect(invoke).toHaveBeenCalledWith('list_local_project_files', { - projectPath: '/tmp/authorized-game', - }); - expect(invoke).toHaveBeenCalledWith('build_local_project_index', { - projectPath: '/tmp/authorized-game', - }); - expect(invoke).toHaveBeenCalledWith('read_local_game_memory', { - projectPath: '/tmp/authorized-game', - scope: 'long', - }); - expect(invoke).toHaveBeenCalledWith('read_local_game_memory', { - projectPath: '/tmp/authorized-game', - scope: 'short', - }); - expect(invoke).toHaveBeenCalledWith('read_local_game_memory', { - projectPath: '/tmp/authorized-game', - scope: 'blackboard', - }); - expect(invoke).toHaveBeenCalledWith('read_local_project_file', { - projectPath: '/tmp/authorized-game', - relativePath: '.agent/run.latest.json', - commandId: 'agent.trace_read', - }); - expect(invoke).toHaveBeenCalledWith('read_local_project_file', { - projectPath: '/tmp/authorized-game', - relativePath: 'game/index.html', - commandId: 'file.read', - }); - expect(invoke).toHaveBeenCalledWith('create_local_project_checkpoint', { - projectPath: '/tmp/authorized-game', - }); - expect(invoke).toHaveBeenCalledWith('export_local_project_package', { - projectPath: '/tmp/authorized-game', - }); - expect(invoke).toHaveBeenCalledWith('start_local_game_preview', { - projectPath: '/tmp/authorized-game', - }); - expect(invoke).toHaveBeenCalledWith('activate_local_game_preview', { - projectPath: '/tmp/authorized-game', - }); - expect(invoke).toHaveBeenCalledWith('get_local_game_preview_status', { - projectPath: '/tmp/authorized-game', - }); - expect(invoke).toHaveBeenCalledWith('stop_local_game_preview', { - projectPath: '/tmp/authorized-game', + + it('预览快捷操作:预览启动、打开、状态与停止', async () => { + const invoke = await openPreviewShortcutProject(); + fireEvent.click(screen.getByRole('button', { name: '启动预览' })); + expect(await screen.findByText('preview.start')).not.toBeNull(); + fireEvent.click(screen.getByRole('button', { name: '确认' })); + expect( + await screen.findByText( + /预览已在客户端运行视图启动:http:\/\/127\.0\.0\.1:3210\//, + ), + ).not.toBeNull(); + + fireEvent.click(screen.getByRole('button', { name: '打开预览' })); + expect(await screen.findByText('preview.open')).not.toBeNull(); + fireEvent.click(screen.getByRole('button', { name: '确认' })); + expect( + await screen.findByText('已切换到客户端运行视图:http://127.0.0.1:3210/'), + ).not.toBeNull(); + + fireEvent.click(screen.getByRole('button', { name: '预览状态' })); + expect( + await screen.findByText('预览运行中:http://127.0.0.1:3210/'), + ).not.toBeNull(); + expect(screen.getByText('preview: 运行中:127.0.0.1:3210')).not.toBeNull(); + + fireEvent.click(screen.getByRole('button', { name: '停止预览' })); + expect(await screen.findByText('预览已停止。')).not.toBeNull(); + expect(screen.getByText('preview: 已停止')).not.toBeNull(); + expect(invoke).toHaveBeenCalledWith('start_local_game_preview', { + projectPath: '/tmp/authorized-game', + }); + expect(invoke).toHaveBeenCalledWith('activate_local_game_preview', { + projectPath: '/tmp/authorized-game', + }); + expect(invoke).toHaveBeenCalledWith('get_local_game_preview_status', { + projectPath: '/tmp/authorized-game', + }); + expect(invoke).toHaveBeenCalledWith('stop_local_game_preview', { + projectPath: '/tmp/authorized-game', + }); }); } diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/invoke-mock.ts b/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/invoke-mock.ts index f5f522ea7..58935ce7a 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/invoke-mock.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-preview/preview-shortcuts/invoke-mock.ts @@ -7,6 +7,9 @@ export function createPreviewShortcutInvoke({ }: PreviewShortcutFixture) { const invoke = vi.fn( async (command: string, args?: Record) => { + if (command === 'set_agc_plugin_project_path') { + return null; + } if (command === 'append_local_permission_log') { return {}; } @@ -296,7 +299,3 @@ export function createPreviewShortcutInvoke({ return invoke; } - -export type PreviewShortcutInvoke = ReturnType< - typeof createPreviewShortcutInvoke ->; diff --git a/docs/project-memory/shared-memory/development-workflow.md b/docs/project-memory/shared-memory/development-workflow.md index 8dac4f319..b7084db6c 100644 --- a/docs/project-memory/shared-memory/development-workflow.md +++ b/docs/project-memory/shared-memory/development-workflow.md @@ -57,6 +57,8 @@ 提示词测试验证实际请求中的片段来源、动态参数和工具结构;措辞不作为逐字契约。已有行为测试覆盖的限制不再另设整段文案检查。Direct 回合测试复用生产的消息转换和文件投影函数,不维护仅供测试调用的回合编排副本。 +AGC 预览快捷操作的界面测试按独立命令或有状态短流程注册,每例重新建立 fixture、原生调用 mock 和页面,并等待项目打开后再记录调用计数。预览启停、导出确认与取消等连续行为保留在同一用例;互不依赖的只读命令不串成一条长对话,也不共享 DOM 或提高超时来容纳整组流程。 + Rust 分片失败日志保留有界的失败详情,包括 panic 位置、断言和最终通过/失败数量;分片选中数量标为 selected,避免误读为失败数量。修改分片日志时运行 `node --test apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.test.mjs`,用最小 Rust fixture 验证失败详情和成功摘要。 AGC 运行时配置默认值调整时,同步核对 Rust 默认值、分发配置模板、设置弹窗默认草稿和 `runtime-settings.suite.ts` 的恢复默认断言;显式传入旧值的配置读取用例仍验证原值保留,不批量替换测试数据。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index ca6c5e543..f36dd0842 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -93,6 +93,8 @@ Rust 侧在 `server-rs/crates/shared-contracts` 维护唯一权威 `GameCreation ## Agent 提示词与回合测试边界 +预览快捷操作的 UI 回归按独立命令及必要的连续操作拆分,每例使用新的项目 fixture、invoke mock 和页面,项目打开后再采样副作用调用计数。保留命令输出、草稿填入、焦点、权限确认和原生参数断言;预览启停、快照回滚取消、导出确认与取消作为各自短流程验证,使用默认测试超时。定向运行 `npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts -t "预览快捷操作:"`。 + 提示词拼装测试核对动态错误、外置片段和实际 Provider 请求结构,避免绑定可自由润色的中文原句。源码载荷限制由实际动作校验与 Provider 修复测试覆盖;工具参数自动修复和超预算上下文压缩继续验证恢复结果、状态及私有信息隔离。Direct 消息原样转发复用生产请求转换测试,文件变化与版本幂等直接测试生产文件投影函数,删除测试专用的重复回合编排。 Rust 分片日志在失败时输出有界 stdout 尾部中的失败段,保留 panic 位置、断言详情和 Rust 汇总;selected 只表示该分片选中的用例数量。成功分片保留简短摘要。定向验证使用相关 Rust 用例与 `node --test apps/ai-game-creator-shell/scripts/run-rust-shell-test-shards.test.mjs`。