Files
Genarrative/apps/ai-game-creator-shell/tests/previewActivation.test.tsx
T
k88936 5315b233c8
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m58s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 3m49s
Project CI / Backend tests (pull_request) Successful in 5m7s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 10m7s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m15s
Project CI / Native shell tests (pull_request) Successful in 6m37s
Project CI / Frontend tests (pull_request) Successful in 3m8s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m45s
Project CI / Repository checks (pull_request) Successful in 1m56s
预览激活回接「运行」入口,聊天 @ 清单打开选择器时按需重读
- App.tsx 新增 activateRunningPreview,运行入口先问 Rust 要活体预览(activate_local_game_preview),命中就只切客户端运行视图
- App.tsx 的 executeRunLocal 命中活体预览时经 DirectProject 聊天的 announce 播报「已切换到客户端运行视图」,不再重复调用 start_local_game_preview;stopped / 非本项目 / 权限位要求确认一律回落到重新启动
- check-config.mjs 从 native-only 白名单移除 activate_local_game_preview 并同步注释(该门禁要求 App invoke 与白名单互斥)
- DirectProjectComposer 新增 onReferencePickerOpen,工具条按钮与附件菜单两处选择器入口打开前先刷新清单
- DirectProjectChatView 用 useDirectProjectManifest 的 refresh 重读聊天自己那份 @ 清单,不新增壳到聊天的清单推送通道
- 新增 tests/previewActivation.test.tsx,钉住切视图不重启 / stopped 回落重启 / 权限确认回落重启三条路径
- decision-log 记录本轮两处回归的背景、决策、取舍与验证方式
2026-09-21 23:09:43 +08:00

154 lines
5.2 KiB
TypeScript
Raw 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`。
*
* 这条行为过去只剩 `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) {
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') {
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() {
return render(
<App
initialProjectPath={PROJECT_PATH}
initialProjectManifest={createFixtureManifest()}
onPlayRequestHandled={vi.fn()}
playRequest={{ projectPath: PROJECT_PATH, requestId: 1 }}
/>,
);
}
/** 运行结果由聊天自己的本地消息流展示,这里按对话容器文本断言。 */
async function expectChatAnnouncement(text: string) {
const surface = await screen.findByLabelText('陶泥儿项目对话');
await waitFor(() => expect(surface.textContent ?? '').toContain(text));
}
beforeEach(() => {
window.history.pushState({}, '', '/');
});
describe('运行入口切到已经在跑的客户端预览', () => {
it('预览还活着时只切视图,不重启预览服务', async () => {
const { invoke, invokeCounts } = installTauri(() => ({
status: 'running',
url: PREVIEW_URL,
port: 43210,
root: PROJECT_PATH,
}));
renderRunningProjectChat();
await waitFor(() =>
expect(invoke).toHaveBeenCalledWith('activate_local_game_preview', {
projectPath: PROJECT_PATH,
}),
);
await expectChatAnnouncement(`已切换到客户端运行视图:${PREVIEW_URL}`);
expect(invokeCounts.get('start_local_game_preview') ?? 0).toBe(0);
});
it('预览不在跑时回落到重新启动预览', async () => {
const { invokeCounts } = installTauri(() => ({
status: 'stopped',
url: null,
port: null,
root: null,
}));
renderRunningProjectChat();
await waitFor(() =>
expect(invokeCounts.get('start_local_game_preview') ?? 0).toBeGreaterThan(
0,
),
);
await expectChatAnnouncement(
`运行通过,已载入客户端运行视图:${PREVIEW_URL}`,
);
});
it('权限位要求确认时不把拒绝当错误,直接回落到重新启动预览', async () => {
const { invokeCounts } = installTauri(() => {
throw new Error('需要先确认预览权限');
});
renderRunningProjectChat();
await waitFor(() =>
expect(invokeCounts.get('start_local_game_preview') ?? 0).toBeGreaterThan(
0,
),
);
await expectChatAnnouncement(
`运行通过,已载入客户端运行视图:${PREVIEW_URL}`,
);
});
});