5bdf25cfd7
- useRecentProjects:区分可重试失败与提权/权限类失败,后者不再就地重试、也不驱动整表重查,避免自己驱动 UAC 反复弹窗 - useRecentProjects:提权类失败的项目在用户主动打开/新建项目或重命名刷新前跳过检查 - useRecentProjects:退避重查预算绑定到失败集合,集合变化或整轮无失败即重置,长期坏目录不再吃满额度 - useRecentProjects:重命名后的单条刷新复用同一套就地重试与有界重查 - app/tauri:删除重复的 TauriInvoke 类型,统一引用 app/types 的既有定义 - tests/recentProjectsHook:失败不跨轮保留改为断言在途「检查中」,新增提权类失败不重试用例 - docs:decision-log、pitfalls、生命周期方案与异步闭环方案同步失败自愈口径与提权边界
209 lines
6.3 KiB
TypeScript
209 lines
6.3 KiB
TypeScript
// @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);
|
||
});
|