Files
Genarrative/apps/ai-game-creator-shell/tests/recentProjectsHook.test.tsx
T
suzmii 5bdf25cfd7 评审收口:提权类失败不再重试、重命名路径复用重试、退避预算绑定失败集合
- useRecentProjects:区分可重试失败与提权/权限类失败,后者不再就地重试、也不驱动整表重查,避免自己驱动 UAC 反复弹窗
- useRecentProjects:提权类失败的项目在用户主动打开/新建项目或重命名刷新前跳过检查
- useRecentProjects:退避重查预算绑定到失败集合,集合变化或整轮无失败即重置,长期坏目录不再吃满额度
- useRecentProjects:重命名后的单条刷新复用同一套就地重试与有界重查
- app/tauri:删除重复的 TauriInvoke 类型,统一引用 app/types 的既有定义
- tests/recentProjectsHook:失败不跨轮保留改为断言在途「检查中」,新增提权类失败不重试用例
- docs:decision-log、pitfalls、生命周期方案与异步闭环方案同步失败自愈口径与提权边界
2026-09-23 16:41:10 +08:00

209 lines
6.3 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);
});