修复项目列表状态互相污染
保留已确认项目的独立状态 隔离项目检查代次和迟到结果 增加坏项目刷新回归测试并同步项目文档
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
||||
type SetStateAction,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
@@ -31,6 +32,9 @@ export function useRecentProjects(setStatus: Dispatch<SetStateAction<string>>) {
|
||||
const [recentWorkspaceRefreshing, setRecentWorkspaceRefreshing] =
|
||||
useState(false);
|
||||
const [projectSearchQuery, setProjectSearchQuery] = useState('');
|
||||
const recentWorkspacesRef = useRef(recentWorkspaces);
|
||||
recentWorkspacesRef.current = recentWorkspaces;
|
||||
const inspectionGenerationRef = useRef(0);
|
||||
|
||||
async function inspectRecentWorkspace(
|
||||
invoke: NonNullable<ReturnType<typeof resolveTauriInvoke>>,
|
||||
@@ -62,19 +66,36 @@ export function useRecentProjects(setStatus: Dispatch<SetStateAction<string>>) {
|
||||
useEffect(() => {
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke || recentWorkspaces.length === 0) {
|
||||
inspectionGenerationRef.current += 1;
|
||||
setRecentWorkspaceStatuses({});
|
||||
setRecentWorkspaceRefreshing(false);
|
||||
return;
|
||||
}
|
||||
const inspectionGeneration = ++inspectionGenerationRef.current;
|
||||
let disposed = false;
|
||||
let pendingCount = recentWorkspaces.length;
|
||||
setRecentWorkspaceStatuses({});
|
||||
// 保留已完成项目的最后一个独立结果。刷新是增量投影:只有新项目或
|
||||
// 尚未完成检查的项目显示“检查中”,不能因为另一个坏目录而把整张列表
|
||||
// 清空成同一个异常状态。
|
||||
setRecentWorkspaceStatuses((current) => {
|
||||
const next: Record<string, LocalProjectDirectoryStatus | null> = {};
|
||||
for (const workspace of recentWorkspaces) {
|
||||
if (Object.prototype.hasOwnProperty.call(current, workspace)) {
|
||||
next[workspace] = current[workspace] ?? null;
|
||||
}
|
||||
}
|
||||
return next;
|
||||
});
|
||||
setRecentWorkspaceRefreshing(true);
|
||||
|
||||
for (const workspace of recentWorkspaces) {
|
||||
void inspectRecentWorkspace(invoke, workspace).then(
|
||||
([projectPath, status]) => {
|
||||
if (disposed) {
|
||||
if (
|
||||
disposed ||
|
||||
inspectionGeneration !== inspectionGenerationRef.current ||
|
||||
!recentWorkspacesRef.current.includes(projectPath)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setRecentWorkspaceStatuses((current) => ({
|
||||
@@ -104,6 +125,9 @@ export function useRecentProjects(setStatus: Dispatch<SetStateAction<string>>) {
|
||||
return;
|
||||
}
|
||||
const [, status] = await inspectRecentWorkspace(invoke, projectPath);
|
||||
if (!recentWorkspacesRef.current.includes(projectPath)) {
|
||||
return;
|
||||
}
|
||||
setRecentWorkspaceStatuses((current) => ({
|
||||
...current,
|
||||
[projectPath]: status,
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
// @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;
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,12 @@ Godot 编辑器操控复用既有 AGC 插件宿主、EditorAdapter、Runner 和
|
||||
> 用途:记录已经确认、会影响后续开发的长期技术/产品/协作决策。短期讨论不要写在这里。
|
||||
> 当前口径(2026-09-18):历史条目的旧路径、旧版本和已退役对象只用于追溯,不构成现行实现依据。策划 Agent V1/V2 的 Runtime、专用命令、审批卡、展示适配和旧测试已删除;当前策划入口统一使用 Design Agent。如与当前代码或 `docs/README.md` 冲突,以当前代码和最新专题文档为准。
|
||||
|
||||
## 2026-09-20 最近项目检查保持项目级隔离
|
||||
|
||||
- 背景:最近项目刷新会重新检查所有路径。若其中一个目录损坏、超时或不可读,清空整张状态表会让已确认正常的项目暂时全部显示“检查中”,用户只能移除坏项目后看到列表恢复。
|
||||
- 决策:最近项目状态按路径独立投影;刷新时保留仍在列表中的最后一次结果,只有新增或尚未检查的项目进入“检查中”。检查代次或列表成员变化后,迟到结果不得写回,单个项目的失败不能改变其它项目的可打开状态。
|
||||
- 验证:`recentProjectsHook.test.tsx` 覆盖“新增慢/坏项目刷新时保留正常项目”;`recentProjectsModel.test.ts`、`unityProjectOpen.test.tsx` 与前端类型检查一并执行。
|
||||
|
||||
## 2026-09-17 GameCreationApp 资源 kind 只保留一份词汇表:严格解析 + `app_log!` 留痕
|
||||
|
||||
- 背景:kind 曾经有三份实现——Rust 手写 `GAME_CREATION_APP_CANONICAL_ASSET_KINDS` + `canonical_game_creation_app_asset_kind()`(带 legacy 别名表与 `font → document` 特例)、TS 手写 `GAME_CREATION_APP_CANONICAL_ASSET_KINDS` + `GAME_CREATION_APP_LEGACY_ASSET_KINDS` + `canonicalGameCreationAppAssetKind()`、以及 ts-rs 生成的 TS union。两份手写表互相引用又各自收口,判据直接分叉(同一个 `"UI"` 一边归一成 `ui-design`、一边收口成 `unknown`),跨语言一致性只能靠正则解析源码的测试来钉。
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
### RecentProjectInspection
|
||||
|
||||
每个最近项目拥有独立 operation 和结果。检查中、可打开、失败、超时、不存在、非目录、未初始化和可导入不能通过一个全局刷新 gate 互相覆盖。
|
||||
每个最近项目拥有独立 operation 和结果。检查中、可打开、失败、超时、不存在、非目录、未初始化和可导入不能通过一个全局刷新 gate 互相覆盖。刷新采用增量投影:已确认的项目结果继续保留,只有新增或尚未完成检查的项目显示“检查中”;项目被移除或检查代次变化后,迟到结果不得写回列表。
|
||||
|
||||
### DevStackIdentity
|
||||
|
||||
|
||||
Reference in New Issue
Block a user