Files
Genarrative/apps/ai-game-creator-shell/tests/recentProjectsHook.test.tsx
suzmii d94ee96836
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust smoke (pull_request) Has been cancelled
补一条用例:Rust 侧取消 UAC 的稳定标记同样不触发重试
- tests/recentProjectsHook:AGC_ACL_ELEVATION_DENIED 这类失败只检查一次,钉住 Rust 错误标记与前端「不可重试」判定之间的契约
2026-09-23 20:11:34 +08:00

236 lines
7.1 KiB
TypeScript
Raw Permalink 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 侧取消 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);
});