fix(agc): 运行不可用提示按清单事实分叉文案

This commit is contained in:
2026-10-07 01:08:38 +08:00
parent 32714298be
commit a8a4b053fe
4 changed files with 202 additions and 1 deletions
@@ -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)}
</p>
) : null}
@@ -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<GameCreationAppManifest, 'versions'>,
): string {
const hasProjectVersions = (manifest.versions?.length ?? 0) > 0;
return hasProjectVersions
? '已有工程内容但还没有可运行的构建产物:让智能体完成可运行原型后即可运行'
: '首个可运行原型尚未完成,运行视图暂不可用';
}
@@ -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', () => {
// 审批模式原来是长在会话列头部按钮里的对话框;面板顶部精简之后它由设置浮层里的
// 「操作权限」行打开,但选项与「不可用项给出说明」的语义必须完整保留。
@@ -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('首个可运行原型尚未完成');
});
});