Files
Genarrative/apps/ai-game-creator-shell/tests/previewActivation.test.tsx
suzmii 6dde321bd1
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m5s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m13s
Project CI / Frontend tests (pull_request) Successful in 3m15s
Project CI / Repository checks (pull_request) Failing after 15s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m56s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 7m55s
Project CI / AI game creator shell web tests (pull_request) Failing after 3m21s
Project CI / Native shell tests (pull_request) Successful in 8m53s
修 review 发现:运行反馈全面退出对话区,并补齐顶栏与守卫细节
- executeRunLocal 的三条失败路径(缺 Tauri / 缺项目 / 启动预览报错)也改走 onRunNotice 失败色提示,对话区不再出现过程行;随之删除已无调用方的 announceProjectChatMessage 与不再有意义的 announceToChat 参数
- RunNoticeToast 的失败提示延长到 6 秒(成功仍 2.6 秒),避免错误一闪而过
- 版本入口在 UI 编辑器壳里不渲染;版本名改由一层 span 承载省略号,长版本名不再被硬裁
- 视图拿不到 onNotice 时把「在浏览器打开失败」写进 console,不再静默吞掉
- check-native-shells 增加「运行页视图里 openUrl( 只有一个调用点」的判据,堵住自动开浏览器的口子
- 新增外壳级接线用例 runNoticeShellWiring;previewActivation 补「启动失败走失败色提示且不写对话区」;runNoticeToast 补失败时长;样式守卫补省略号
- decision-log 收敛到最终口径(成功与失败都出对话区、失败留 6 秒、编辑器不挂版本入口)
2026-09-22 13:51:30 +08:00

192 lines
6.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** @vitest-environment jsdom */
/**
* 运行入口「预览已经在跑就切视图」的回归用例。
*
* 背景:工作台壳过去用 `/preview open` 聊天命令切到已经在跑的客户端运行视图,那条命令链
* 随 Project Supervisor 前端链路整体退役;按 ADR(`docs/adr/【ADR】DirectProject独立聊天
* 容器与工作台钱包布局-2026-09-18.md`),这个动作由 DirectProject 的「运行」入口承接,
* 结果经聊天自己的 `announce` 交给聊天的本地消息流。
*
* 因此运行入口进入客户端运行视图前先问 Rust 要活体预览:`activate_local_game_preview`
* 只核对内存 registry 并回传 loopback 地址,活着就复用(不重启预览服务),没活着
* (stopped / 不属于这个项目 / 权限位要求确认)才回落到 `start_local_game_preview`。
*
* 成功反馈走工作台壳的 toast(`onRunNotice`),**不再**写进对话区:那条「已载入客户端
* 运行视图:URL」以前常驻对话底部挡住运行画面。用例因此直接断言这条通道,并反过来钉住
* 对话容器里没有这类提示。
*
* 这条行为过去只剩 `scripts/check-native-shells.mjs` 的源码守卫与 Rust 实现,前端没有
* 用例;一旦有人再把它当死链路删掉,守卫会先报「missing await invoke」而不知道现场。
*/
import { beforeEach, describe, it, vi } from 'vitest';
import type { GameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp';
import {
App,
createGameCreationAppManifest,
createProjectChatRuntimeHarness,
expect,
render,
screen,
waitFor,
} from './appSurface/harness';
const PROJECT_PATH = '/tmp/preview-activation-project';
const PROJECT_ID = 'preview-activation-project';
const PREVIEW_URL = 'http://127.0.0.1:43210/game/index.html';
function createFixtureManifest(): GameCreationAppManifest {
return createGameCreationAppManifest(PROJECT_ID, '预览切换项目');
}
/**
* `activate_local_game_preview` 的替身由用例给定:它决定「这条预览还活着吗」,
* 其余命令沿用聊天 harness,运行入口之外的链路保持真实形状。
*/
function installTauri(
activateLocalGamePreview: () => unknown,
options: { startFails?: string } = {},
) {
const manifest = createFixtureManifest();
const chatHarness = createProjectChatRuntimeHarness({
projectPath: PROJECT_PATH,
});
const invokeCounts = new Map<string, number>();
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
invokeCounts.set(command, (invokeCounts.get(command) ?? 0) + 1);
if (command === 'get_local_game_manifest') {
return manifest;
}
if (command === 'get_local_game_project_revision') {
return { revision: 3 };
}
if (command === 'activate_local_game_preview') {
return activateLocalGamePreview();
}
if (command === 'start_local_game_preview') {
if (options.startFails) {
throw new Error(options.startFails);
}
return { url: PREVIEW_URL, port: 43210, root: PROJECT_PATH };
}
return chatHarness.invoke(command, args);
},
);
window.__TAURI__ = {
core: { invoke: invoke as never },
event: { listen: chatHarness.listen as never },
};
return { invoke, invokeCounts };
}
/** 「运行」入口由工作台壳的播放请求驱动,这里直接用同一形状的请求触发。 */
function renderRunningProjectChat() {
const onRunNotice = vi.fn();
render(
<App
initialProjectPath={PROJECT_PATH}
initialProjectManifest={createFixtureManifest()}
onPlayRequestHandled={vi.fn()}
onRunNotice={onRunNotice}
playRequest={{ projectPath: PROJECT_PATH, requestId: 1 }}
/>,
);
return { onRunNotice };
}
/** 运行反馈走 toast;对话容器里不该再出现这类过程提示。 */
async function expectRunNoticeWithoutChatTip(
onRunNotice: ReturnType<typeof vi.fn>,
message: string,
) {
await waitFor(() => expect(onRunNotice).toHaveBeenCalledWith({ message }));
const surface = await screen.findByLabelText('陶泥儿项目对话');
expect(surface.textContent ?? '').not.toContain('运行通过');
expect(surface.textContent ?? '').not.toContain('已切换到客户端运行视图');
}
beforeEach(() => {
window.history.pushState({}, '', '/');
});
describe('运行入口切到已经在跑的客户端预览', () => {
it('预览还活着时只切视图,不重启预览服务', async () => {
const { invoke, invokeCounts } = installTauri(() => ({
status: 'running',
url: PREVIEW_URL,
port: 43210,
root: PROJECT_PATH,
}));
const { onRunNotice } = renderRunningProjectChat();
await waitFor(() =>
expect(invoke).toHaveBeenCalledWith('activate_local_game_preview', {
projectPath: PROJECT_PATH,
}),
);
await expectRunNoticeWithoutChatTip(onRunNotice, '已载入客户端运行视图');
expect(invokeCounts.get('start_local_game_preview') ?? 0).toBe(0);
});
it('预览不在跑时回落到重新启动预览', async () => {
const { invokeCounts } = installTauri(() => ({
status: 'stopped',
url: null,
port: null,
root: null,
}));
const { onRunNotice } = renderRunningProjectChat();
await waitFor(() =>
expect(invokeCounts.get('start_local_game_preview') ?? 0).toBeGreaterThan(
0,
),
);
await expectRunNoticeWithoutChatTip(
onRunNotice,
'运行通过,已载入客户端运行视图',
);
});
it('权限位要求确认时不把拒绝当错误,直接回落到重新启动预览', async () => {
const { invokeCounts } = installTauri(() => {
throw new Error('需要先确认预览权限');
});
const { onRunNotice } = renderRunningProjectChat();
await waitFor(() =>
expect(invokeCounts.get('start_local_game_preview') ?? 0).toBeGreaterThan(
0,
),
);
await expectRunNoticeWithoutChatTip(
onRunNotice,
'运行通过,已载入客户端运行视图',
);
});
it('启动预览失败时走失败色提示,不再写进对话区', async () => {
installTauri(
() => ({ status: 'stopped', url: null, port: null, root: null }),
{ startFails: '预览端口被占用' },
);
const { onRunNotice } = renderRunningProjectChat();
await waitFor(() =>
expect(onRunNotice).toHaveBeenCalledWith({
tone: 'error',
message: '运行游戏失败:预览端口被占用',
}),
);
const surface = await screen.findByLabelText('陶泥儿项目对话');
expect(surface.textContent ?? '').not.toContain('预览端口被占用');
});
});