66bb240115
- 新增 features/app-shell/sessionPreview.ts 的 resolveSessionPreviewOnProjectOpen:进入项目时问一次 get_local_game_preview_status(内存 registry 是活体的唯一真相),确实 running 才把它作为本次会话的预览;记录写着 running 而 registry 没有活体时判为陈旧,复用现役 stop_local_game_preview 把落盘记录对齐成 stopped,并同时把会话 manifest 按真相投影成 stopped(否则运行入口会继续拿着打不开的 URL 渲染运行画面)。 - useHomeProjectCreation.ts:enterProjectDevelopment 改为 async,先做这次核对再进工作台,并按核对结果决定会话预览与 manifest 投影;三个调用点(新建项目、创建项目、打开项目)都改为 await。 - preview.rs:同一个项目重启预览时,旧 registry 身份的收尾不再覆盖刚落盘的 running——只有当旧预览属于**另一个项目**时才替它收尾。此前同项目重启会把刚写进去的 running 覆盖成 stopped,下次进项目便丢掉"预览在跑"这条事实(记录反方向失真)。 - preview.rs:stop_local_game_preview_for_root 在没有活体可停、但落盘记录仍是 running 时也把记录收尾成 stopped(stop 的语义变成"真的停干净,包括记录"),因此陈旧记录既可由进入项目触发对齐,也可由用户显式停止对齐。 - tests/project.rs:新增三条 Rust 定向用例——同项目重启后记录仍为 running 且端口是新预览的;换项目启动时旧项目记录写 stopped、新项目写 running;registry 无活体而记录写着 running 时 stop 会把记录对齐成 stopped。 - tests/sessionPreview.test.ts(新增):五条用例覆盖活体优先、陈旧记录对齐并投影、registry 不可读按没有在跑处理、记录本来不是 running 时不发多余停止命令、无 invoke 时零请求。变异验证:把活体核对去掉(直接信落盘记录)→「registry 里没有活体时不给会话预览」与「registry 不可读」两条立即红灯(已实测)。 - project-development.suite.ts:把「打开项目不查预览状态」那条断言改为「恰好核对一次预览活体」,并保留邻近的 start_local_game_preview 必须为 0 的断言——被新行为取代的是"从不核验","开项目不偷偷启动预览"这条原意一字未改。 - PRD §4.1:状态机按当前实现改写(进入项目一律落到 resource-overview;run 的自动进入只认内存 registry 的活体确认;落盘 preview 是记录而非活体,陈旧记录不触发切换也不参与渲染),并把退出收尾一并写进该节说明。
159 lines
4.8 KiB
TypeScript
159 lines
4.8 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
import type { GameCreationAppPreviewState } from '../../../packages/shared/src/contracts/gameCreationApp';
|
|
import { resolveSessionPreviewOnProjectOpen } from '../src/features/app-shell/sessionPreview';
|
|
|
|
const projectPath = '/tmp/session-preview-project';
|
|
|
|
function createInvoke(
|
|
responses: Record<string, unknown>,
|
|
calls: Array<[string, Record<string, unknown> | undefined]> = [],
|
|
) {
|
|
return vi.fn(async (command: string, args?: Record<string, unknown>) => {
|
|
calls.push([command, args]);
|
|
if (!(command in responses)) {
|
|
throw new Error(`unexpected invoke ${command}`);
|
|
}
|
|
return responses[command];
|
|
});
|
|
}
|
|
|
|
describe('进入项目时的预览活体核验', () => {
|
|
it('registry 里没有活体时不给会话预览,并把陈旧的 running 记录对齐成 stopped', async () => {
|
|
const calls: Array<[string, Record<string, unknown> | undefined]> = [];
|
|
const invoke = createInvoke(
|
|
{
|
|
get_local_game_preview_status: {
|
|
status: 'stopped',
|
|
url: null,
|
|
port: null,
|
|
root: null,
|
|
},
|
|
stop_local_game_preview: {
|
|
status: 'stopped',
|
|
url: null,
|
|
port: null,
|
|
root: null,
|
|
},
|
|
},
|
|
calls,
|
|
);
|
|
|
|
const resolution = await resolveSessionPreviewOnProjectOpen({
|
|
invoke,
|
|
projectPath,
|
|
recordedPreview: {
|
|
status: 'running',
|
|
url: 'http://127.0.0.1:4173/',
|
|
port: 4173,
|
|
},
|
|
});
|
|
|
|
// 没有活体预览:本次会话不带预览(工作台因此不会自动进运行视图)。
|
|
expect(resolution.sessionPreview).toBeNull();
|
|
// 落盘记录被判陈旧,会话 manifest 立刻按真相投影。
|
|
expect(resolution.manifestPreviewPatch).toEqual({ status: 'stopped' });
|
|
expect(calls).toEqual([
|
|
['get_local_game_preview_status', { projectPath }],
|
|
['stop_local_game_preview', { projectPath }],
|
|
]);
|
|
});
|
|
|
|
it('registry 确实在跑时只认活体,且不写落盘记录', async () => {
|
|
const calls: Array<[string, Record<string, unknown> | undefined]> = [];
|
|
const invoke = createInvoke(
|
|
{
|
|
get_local_game_preview_status: {
|
|
status: 'running',
|
|
url: 'http://127.0.0.1:5210/',
|
|
port: 5210,
|
|
root: projectPath,
|
|
},
|
|
},
|
|
calls,
|
|
);
|
|
|
|
const resolution = await resolveSessionPreviewOnProjectOpen({
|
|
invoke,
|
|
projectPath,
|
|
// 落盘记录落后于真相(例如同项目重启后记录没跟上)也不影响:活体说了算。
|
|
recordedPreview: { status: 'stopped' },
|
|
});
|
|
|
|
expect(resolution.sessionPreview).toEqual({
|
|
status: 'running',
|
|
url: 'http://127.0.0.1:5210/',
|
|
port: 5210,
|
|
});
|
|
expect(resolution.manifestPreviewPatch).toBeNull();
|
|
expect(calls).toEqual([['get_local_game_preview_status', { projectPath }]]);
|
|
});
|
|
|
|
it('registry 不可读时按没有在跑处理,不把落盘记录当活体', async () => {
|
|
const invoke = vi.fn(async (command: string) => {
|
|
if (command === 'get_local_game_preview_status') {
|
|
throw new Error('需要在陶泥儿客户端内运行');
|
|
}
|
|
return { status: 'stopped', url: null, port: null, root: null };
|
|
});
|
|
|
|
const resolution = await resolveSessionPreviewOnProjectOpen({
|
|
invoke,
|
|
projectPath,
|
|
recordedPreview: {
|
|
status: 'running',
|
|
url: 'http://127.0.0.1:4173/',
|
|
port: 4173,
|
|
},
|
|
});
|
|
|
|
expect(resolution.sessionPreview).toBeNull();
|
|
expect(resolution.manifestPreviewPatch).toEqual({ status: 'stopped' });
|
|
});
|
|
|
|
it('记录本来就不是 running 时只读活体,不发多余的停止命令', async () => {
|
|
const calls: Array<[string, Record<string, unknown> | undefined]> = [];
|
|
const invoke = createInvoke(
|
|
{
|
|
get_local_game_preview_status: {
|
|
status: 'stopped',
|
|
url: null,
|
|
port: null,
|
|
root: null,
|
|
},
|
|
},
|
|
calls,
|
|
);
|
|
|
|
const stoppedRecord: GameCreationAppPreviewState = { status: 'stopped' };
|
|
const resolution = await resolveSessionPreviewOnProjectOpen({
|
|
invoke,
|
|
projectPath,
|
|
recordedPreview: stoppedRecord,
|
|
});
|
|
|
|
expect(resolution).toEqual({
|
|
sessionPreview: null,
|
|
manifestPreviewPatch: null,
|
|
});
|
|
expect(calls).toEqual([['get_local_game_preview_status', { projectPath }]]);
|
|
});
|
|
|
|
it('没有 invoke(浏览器里跑)时不做任何请求', async () => {
|
|
const resolution = await resolveSessionPreviewOnProjectOpen({
|
|
invoke: null,
|
|
projectPath,
|
|
recordedPreview: {
|
|
status: 'running',
|
|
url: 'http://127.0.0.1:4173/',
|
|
port: 4173,
|
|
},
|
|
});
|
|
|
|
expect(resolution).toEqual({
|
|
sessionPreview: null,
|
|
manifestPreviewPatch: null,
|
|
});
|
|
});
|
|
});
|