Files
Genarrative/apps/ai-game-creator-shell/tests/recentProjectsHook.test.tsx
T
kdletters 1e62d8317a 合并 origin/master 到 AGC 渲染层下沉分支并完成三处对齐
- 活动回合事实源统一到 Direct 线程管理器:删除 direct_runtime 的第二份快照,接单、进度内容变化与收口各广播一次活动回合变更事件
- 平台维护态判定移入 Rust 并在渲染层只订阅单一事件:新增 platform_maintenance 模块与各平台 facade 错误分支的分类入口
- 封面生成请求补 generationInputs.source,保持队列回填后仍能拿到平台素材 ID
- 渲染层按 master 5398a53e6 退役诊断详情入口:删除 agentRuntimeErrorDetail 与「查看详情」交互及其专属用例
- 冲突收口:nginx SPA 白名单、.gitignore、mobile 检查脚本、capabilities 描述取上游,两个已退役计划随上游删除,文档保留双方条目
- 新增并回写本里程碑取证、decision-log 与 pitfalls 的 2026-09-28 记录
2026-09-28 15:07:30 +08:00

299 lines
9.0 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
import { act, renderHook, waitFor } from '@testing-library/react';
import { afterEach, expect, test, vi } from 'vitest';
import { useRecentProjects } from '../src/features/app-shell/useRecentProjects';
const READY_PROJECT = {
projectPath: '/tmp/ready-project',
exists: true,
isDirectory: true,
isGameCreatorProject: true,
isGodotProject: false,
godotProjectRoot: null,
isCocosProject: false,
cocosProjectRoot: null,
isUnityProject: false,
unityProjectRoot: null,
projectName: '正常项目',
recentRunStatus: null,
recentRunStopReason: null,
};
afterEach(() => {
delete window.__TAURI__;
window.localStorage.clear();
});
test('刷新坏项目时保留其它项目已确认的正常状态', async () => {
let finishPendingInspection: (() => void) | null = null;
const pendingInspection = new Promise<typeof READY_PROJECT>((resolve) => {
finishPendingInspection = () =>
resolve({
...READY_PROJECT,
projectPath: '/tmp/slow-broken-project',
projectName: '慢速坏项目',
});
});
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command !== 'inspect_local_project_directory') {
throw new Error(`unexpected invoke ${command}`);
}
const projectPath = String(args?.projectPath ?? '');
if (projectPath === '/tmp/slow-broken-project') {
return pendingInspection;
}
return READY_PROJECT;
},
);
window.__TAURI__ = { core: { invoke } };
window.localStorage.setItem(
'genarrative-ai-game-creator.recent-workspaces.v1',
JSON.stringify(['/tmp/ready-project']),
);
const { result } = renderHook(() => useRecentProjects(vi.fn()));
await waitFor(() => {
expect(result.current.projectRows[0]).toMatchObject({
name: '正常项目',
status: '本地项目',
canOpen: true,
});
});
act(() => {
result.current.rememberRecentWorkspace('/tmp/slow-broken-project');
});
await waitFor(() => {
expect(result.current.projectRows).toHaveLength(2);
const readyRow = result.current.projectRows.find(
(row) => row.path === '/tmp/ready-project',
);
const slowRow = result.current.projectRows.find(
(row) => row.path === '/tmp/slow-broken-project',
);
expect(readyRow).toMatchObject({
name: '正常项目',
status: '本地项目',
canOpen: true,
});
expect(slowRow).toMatchObject({
name: 'slow-broken-project',
status: '检查中',
canOpen: false,
});
});
await act(async () => {
finishPendingInspection?.();
await pendingInspection;
});
});
test('单次目录检查失败会就地重试,不会把整行钉成「检查失败」', async () => {
let attempts = 0;
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command !== 'inspect_local_project_directory') {
throw new Error(`unexpected invoke ${command}`);
}
attempts += 1;
if (attempts === 1) {
throw new Error('Tauri IPC 瞬时失败');
}
return READY_PROJECT;
},
);
window.__TAURI__ = { core: { invoke } };
window.localStorage.setItem(
'genarrative-ai-game-creator.recent-workspaces.v1',
JSON.stringify(['/tmp/ready-project']),
);
const { result } = renderHook(() => useRecentProjects(vi.fn()));
await waitFor(() => {
expect(result.current.projectRows[0]).toMatchObject({
status: '本地项目',
canOpen: true,
});
});
expect(attempts).toBe(2);
expect(result.current.projectRows[0]?.status).not.toBe('检查失败');
});
test('失败结果不跨轮保留:刷新时该项回到「检查中」并重新检查', async () => {
const inspectCounts: Record<string, number> = {};
let secondRoundPending: (() => void) | null = null;
const secondRoundInspection = new Promise<typeof READY_PROJECT>((resolve) => {
secondRoundPending = () => resolve(READY_PROJECT);
});
let brokenRound = 0;
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command !== 'inspect_local_project_directory') {
throw new Error(`unexpected invoke ${command}`);
}
const projectPath = String(args?.projectPath ?? '');
inspectCounts[projectPath] = (inspectCounts[projectPath] ?? 0) + 1;
if (projectPath === '/tmp/broken-project') {
brokenRound += 1;
if (brokenRound > 2) {
return secondRoundInspection;
}
throw new Error('Tauri IPC 持续失败');
}
return { ...READY_PROJECT, projectPath };
},
);
window.__TAURI__ = { core: { invoke } };
window.localStorage.setItem(
'genarrative-ai-game-creator.recent-workspaces.v1',
JSON.stringify(['/tmp/broken-project']),
);
const { result } = renderHook(() => useRecentProjects(vi.fn()));
await waitFor(() => {
expect(result.current.projectRows[0]?.status).toBe('检查失败');
});
expect(inspectCounts['/tmp/broken-project']).toBe(2);
act(() => {
result.current.rememberRecentWorkspace('/tmp/ready-project');
});
// 上一轮的失败结果不进新投影:第二轮在途时该项目显示「检查中」而不是沿用「检查失败」。
await waitFor(() => {
expect(
result.current.projectRows.find(
(row) => row.path === '/tmp/broken-project',
)?.status,
).toBe('检查中');
});
await act(async () => {
secondRoundPending?.();
await secondRoundInspection;
});
});
test('提权类失败不重试:不放大 UAC 弹窗', async () => {
let attempts = 0;
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command !== 'inspect_local_project_directory') {
throw new Error(`unexpected invoke ${command}`);
}
attempts += 1;
throw new Error(
'读取待修复私有对象失败:C:\\p\\.agent(DACL 不包含当前用户)',
);
},
);
window.__TAURI__ = { core: { invoke } };
window.localStorage.setItem(
'genarrative-ai-game-creator.recent-workspaces.v1',
JSON.stringify(['/tmp/elevation-project']),
);
const { result } = renderHook(() => useRecentProjects(vi.fn()));
await waitFor(() => {
expect(result.current.projectRows[0]?.status).toBe('检查失败');
});
expect(attempts).toBe(1);
});
test('Rust 偏好事件与 command 返回同时到达时只检查新增项目一次', async () => {
let snapshot = {
projectCreationDirectory: null,
recentWorkspaces: ['/tmp/ready-project'],
};
let emitPreferenceEvent:
| ((event: { payload: typeof snapshot }) => void)
| null = null;
const inspectCounts: Record<string, number> = {};
const invoke = vi.fn(
async (command: string, args?: Record<string, unknown>) => {
if (command === 'read_workspace_preferences') {
return snapshot;
}
if (command === 'remember_recent_workspace') {
const projectPath = String(args?.projectPath ?? '');
snapshot = {
...snapshot,
recentWorkspaces: [projectPath, ...snapshot.recentWorkspaces],
};
emitPreferenceEvent?.({ payload: snapshot });
return snapshot;
}
if (command === 'inspect_local_project_directory') {
const projectPath = String(args?.projectPath ?? '');
inspectCounts[projectPath] = (inspectCounts[projectPath] ?? 0) + 1;
return { ...READY_PROJECT, projectPath };
}
throw new Error('unexpected invoke ' + command);
},
);
window.__TAURI__ = {
core: { invoke },
event: {
listen: vi.fn(async (_event, handler) => {
emitPreferenceEvent = handler;
return () => {
emitPreferenceEvent = null;
};
}),
},
};
const { result } = renderHook(() => useRecentProjects(vi.fn()));
await waitFor(() => {
expect(result.current.projectRows[0]).toMatchObject({
path: '/tmp/ready-project',
canOpen: true,
});
});
act(() => {
result.current.rememberRecentWorkspace('/tmp/new-project');
});
await waitFor(() => {
expect(result.current.projectRows[0]).toMatchObject({
path: '/tmp/new-project',
canOpen: true,
});
});
expect(inspectCounts['/tmp/new-project']).toBe(1);
test('Rust 侧取消 UAC 的稳定标记同样不触发重试', async () => {
let attempts = 0;
const invoke = vi.fn(
async (command: string, _args?: Record<string, unknown>) => {
if (command !== 'inspect_local_project_directory') {
throw new Error(`unexpected invoke ${command}`);
}
attempts += 1;
throw new Error(
'AGC_ACL_ELEVATION_DENIED:AGC ACL 提权修复被用户取消(exit code Some(1223))',
);
},
);
window.__TAURI__ = { core: { invoke } };
window.localStorage.setItem(
'genarrative-ai-game-creator.recent-workspaces.v1',
JSON.stringify(['/tmp/denied-elevation-project']),
);
const { result } = renderHook(() => useRecentProjects(vi.fn()));
await waitFor(() => {
expect(result.current.projectRows[0]?.status).toBe('检查失败');
});
expect(attempts).toBe(1);
});
});