From a8a4b053feb27393e656111928db54b3cb9d96a8 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Wed, 7 Oct 2026 01:08:38 +0800 Subject: [PATCH] =?UTF-8?q?fix(agc):=20=E8=BF=90=E8=A1=8C=E4=B8=8D?= =?UTF-8?q?=E5=8F=AF=E7=94=A8=E6=8F=90=E7=A4=BA=E6=8C=89=E6=B8=85=E5=8D=95?= =?UTF-8?q?=E4=BA=8B=E5=AE=9E=E5=88=86=E5=8F=89=E6=96=87=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/view/project-development/index.tsx | 3 +- .../project-development/runUnavailableHint.ts | 27 ++++ .../appSurface/project-development.suite.ts | 128 ++++++++++++++++++ .../tests/runUnavailableHint.test.ts | 45 ++++++ 4 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 apps/ai-game-creator-shell/src/view/project-development/runUnavailableHint.ts create mode 100644 apps/ai-game-creator-shell/tests/runUnavailableHint.test.ts diff --git a/apps/ai-game-creator-shell/src/view/project-development/index.tsx b/apps/ai-game-creator-shell/src/view/project-development/index.tsx index 255249275..029bb5324 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/index.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/index.tsx @@ -443,6 +443,7 @@ import { projectResourceSectionZoomFromWheel, } from './resourceSectionHeightModel'; import { ResourceTypePanel } from './ResourceTypePanel'; +import { runUnavailableHintText } from './runUnavailableHint'; import { describeProjectResourceCanvasLayoutRead, type ProjectResourceCanvasLayoutReadReport, @@ -10086,7 +10087,7 @@ export default function ProjectDevelopmentView({ className="game-run-unavailable" role="status" > - 首个可运行原型尚未完成,运行视图暂不可用 + {runUnavailableHintText(manifest)}

) : null} diff --git a/apps/ai-game-creator-shell/src/view/project-development/runUnavailableHint.ts b/apps/ai-game-creator-shell/src/view/project-development/runUnavailableHint.ts new file mode 100644 index 000000000..288b4af79 --- /dev/null +++ b/apps/ai-game-creator-shell/src/view/project-development/runUnavailableHint.ts @@ -0,0 +1,27 @@ +import type { GameCreationAppManifest } from '../../../../../packages/shared/src/contracts/gameCreationApp'; + +/** + * 「运行」页签不可用(`run-unavailable-hint`)时给用户看的文案。 + * + * 同一句话对两种情况并不都成立,所以按清单里已有的事实分叉,而不是套一句万能的空话: + * + * - **全新项目**(清单里还没有任何工程内部版本):确实什么都还没产出, + * 「首个可运行原型尚未完成,运行视图暂不可用」是准确的,保持原样。 + * - **已经有工程内容、但本机还没有可运行的构建产物**:典型是**从平台作品 Fork 来的工程源包 + * 项目**——包里就是作者的完整工程,但工程源包按规则不含构建产物(`project_bundle.rs` 排除 + * `game/dist`),AGC 也不在 Fork 时自动构建(与「不自动安装外部工程依赖」的既有策略一致)。 + * 这时原句会让人以为「什么都没有」,必须如实说清:内容已经在本地,只是还没有构建出可运行的 + * 原型,让智能体完成可运行原型之后即可运行。 + * + * 判据只用清单里已有的字段(`versions` 非空 = 这个项目已经有工程内部版本记录)。渲染层拿不到 + * 「预览根有没有 index.html」这种文件事实(那是原生侧的门禁判据),所以这里不新造状态、也不 + * 额外调 IPC——它只决定**措辞**,能不能运行仍由运行门禁与原生预览决定。 + */ +export function runUnavailableHintText( + manifest: Pick, +): string { + const hasProjectVersions = (manifest.versions?.length ?? 0) > 0; + return hasProjectVersions + ? '已有工程内容但还没有可运行的构建产物:让智能体完成可运行原型后即可运行' + : '首个可运行原型尚未完成,运行视图暂不可用'; +} diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index 8004a2abf..41f047b48 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -531,6 +531,134 @@ export function registerProjectWorkbenchFoundationTests() { expect(screen.getByRole('article', { name: /发布 Agent/ })).not.toBeNull(); }); + /** + * Fork 建项之后运行视图必须直接可用。 + * + * 原生侧(`game_fork.rs` 的 `register_forked_project_state_at`)在建项收口时登记「可运行 + * 原型」:成品包形态把发行产物铺进预览根并置 `code-prototype = completed`,同时写一条 + * `initial-1` 工程内部版本。这里钉的是渲染层的投影结果——那句「首个可运行原型尚未完成」 + * 必须消失,运行页签可点并真的发出播放请求。 + */ + it('renders a forked package project as directly runnable with an initial version', () => { + const manifest = createGameCreationAppManifest( + 'workbench-fork-package', + 'Fork 项目(成品包)', + ); + const codePrototype = manifest.tasks.find( + (task) => task.id === 'code-prototype', + ); + if (!codePrototype) { + throw new Error('fixture 必须包含 code-prototype seed 任务'); + } + codePrototype.status = 'completed'; + manifest.versions = [ + { + versionId: 'initial-1', + parentVersionId: null, + projectRevision: 1, + resourceBindings: [], + createdReason: 'initial', + createdAt: 1_791_271_678, + }, + ]; + + const onPlay = vi.fn(); + render( + React.createElement(ProjectDevelopmentView, { + projectName: 'Fork 项目(成品包)', + projectPath: '/tmp/workbench-fork-package', + manifest, + attachments: [], + recentRunStatus: null, + recentRunStopReason: null, + chat: React.createElement( + 'div', + { 'aria-label': '测试项目总控' }, + '项目总控对话内容', + ), + onHomeOpen: vi.fn(), + onProjectsOpen: vi.fn(), + onPlay, + }), + ); + + // 提示必须消失,页签必须不再是「不可用」态。 + expect( + screen.queryByText('首个可运行原型尚未完成,运行视图暂不可用'), + ).toBeNull(); + expect(document.querySelector('#run-unavailable-hint')).toBeNull(); + const runTab = screen.getByRole('tab', { + name: '运行', + }) as HTMLButtonElement; + expect(runTab.getAttribute('data-unavailable')).toBeNull(); + expect(runTab.getAttribute('aria-describedby')).toBeNull(); + + // 点运行 = 切视图 + 发起播放请求(就是 fork 之后用户看到的「直接能跑」)。 + fireEvent.click(runTab); + expect(runTab.getAttribute('aria-selected')).toBe('true'); + expect(onPlay).toHaveBeenCalledTimes(1); + }); + + /** + * 工程源包 Fork 的如实表述:包里是作者的完整工程,但工程源包不含构建产物 + * (`project_bundle.rs` 排除 `game/dist`),AGC 也不在 Fork 时自动构建,所以本机还没有 + * 可运行的构建产物。此时不能再说「首个可运行原型尚未完成」(听着像什么都没有), + * 也不能谎称已经可运行。 + */ + it('tells the truth for a source-only fork that still needs a build', () => { + const manifest = createGameCreationAppManifest( + 'workbench-fork-source', + 'Fork 项目(工程源包)', + ); + manifest.versions = [ + { + versionId: 'initial-1', + parentVersionId: null, + projectRevision: 1, + resourceBindings: [], + createdReason: 'initial', + createdAt: 1_791_271_678, + }, + ]; + + const onPlay = vi.fn(); + render( + React.createElement(ProjectDevelopmentView, { + projectName: 'Fork 项目(工程源包)', + projectPath: '/tmp/workbench-fork-source', + manifest, + attachments: [], + recentRunStatus: null, + recentRunStopReason: null, + chat: React.createElement( + 'div', + { 'aria-label': '测试项目总控' }, + '项目总控对话内容', + ), + onHomeOpen: vi.fn(), + onProjectsOpen: vi.fn(), + onPlay, + }), + ); + + expect( + screen.getByText( + '已有工程内容但还没有可运行的构建产物:让智能体完成可运行原型后即可运行', + ), + ).not.toBeNull(); + expect( + screen.queryByText('首个可运行原型尚未完成,运行视图暂不可用'), + ).toBeNull(); + + const runTab = screen.getByRole('tab', { + name: '运行', + }) as HTMLButtonElement; + expect(runTab.getAttribute('data-unavailable')).toBe('true'); + fireEvent.click(runTab); + expect(runTab.getAttribute('aria-selected')).toBe('false'); + expect(onPlay).not.toHaveBeenCalled(); + }); + it('keeps the approval-mode choices reachable in their own dialog surface', () => { // 审批模式原来是长在会话列头部按钮里的对话框;面板顶部精简之后它由设置浮层里的 // 「操作权限」行打开,但选项与「不可用项给出说明」的语义必须完整保留。 diff --git a/apps/ai-game-creator-shell/tests/runUnavailableHint.test.ts b/apps/ai-game-creator-shell/tests/runUnavailableHint.test.ts new file mode 100644 index 000000000..bd3d8714e --- /dev/null +++ b/apps/ai-game-creator-shell/tests/runUnavailableHint.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, it } from 'vitest'; + +import { runUnavailableHintText } from '../src/view/project-development/runUnavailableHint'; + +/** + * 「运行」页签不可用时的文案(`run-unavailable-hint`)。 + * + * 为什么要有这条用例:同一句「首个可运行原型尚未完成」对**已有工程内容、只是本机还没构建出 + * 可运行产物**的情况(典型是从平台作品 Fork 来的工程源包项目)是误导——内容明明已经在本地。 + * 文案因此必须按清单事实分叉,而分叉判据只能是清单里已有的字段。 + */ +describe('runUnavailableHintText', () => { + it('全新项目(清单里还没有工程内部版本)沿用原有措辞', () => { + expect(runUnavailableHintText({ versions: [] })).toBe( + '首个可运行原型尚未完成,运行视图暂不可用', + ); + }); + + it('versions 字段整体缺失时按「没有版本」处理,不抛错', () => { + expect(runUnavailableHintText({} as { versions: [] })).toBe( + '首个可运行原型尚未完成,运行视图暂不可用', + ); + }); + + it('已有工程内容但没有可运行的构建产物时如实说明需要先完成原型', () => { + const text = runUnavailableHintText({ + versions: [ + { + versionId: 'initial-1', + parentVersionId: null, + projectRevision: 1, + resourceBindings: [], + createdReason: 'initial', + createdAt: 1_791_271_678, + }, + ], + }); + + expect(text).toBe( + '已有工程内容但还没有可运行的构建产物:让智能体完成可运行原型后即可运行', + ); + // 不能再用「首个可运行原型尚未完成」那种"什么都还没有"的说法。 + expect(text).not.toContain('首个可运行原型尚未完成'); + }); +});