// @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((resolve) => { finishPendingInspection = () => resolve({ ...READY_PROJECT, projectPath: '/tmp/slow-broken-project', projectName: '慢速坏项目', }); }); const invoke = vi.fn( async (command: string, args?: Record) => { 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) => { 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 = {}; let secondRoundPending: (() => void) | null = null; const secondRoundInspection = new Promise((resolve) => { secondRoundPending = () => resolve(READY_PROJECT); }); let brokenRound = 0; const invoke = vi.fn( async (command: string, args?: Record) => { 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) => { 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); });