diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index d6f7cc924..d6b0cb8cd 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -12,6 +12,7 @@ import { import { normalizeProjectResourceGraph } from '../../src/view/project-development/resourceDependencyGraphModel'; import { ResourceDependencyOverlay } from '../../src/view/project-development/ResourceDependencyOverlay'; import type { ProjectAgentResultSummary } from '../../src/view/project-development/resourceProjectionModel'; +import { projectResourcesFromReadModels } from '../../src/view/project-development/resourceProjectionModel'; import { act, agentRuntimeUserInputRequest, @@ -8442,4 +8443,155 @@ export function registerProjectAgentStatusTests() { expect.anything(), ); }); + + /** + * 资源总览(main 态)必须为**每个非空栏目**挂载真实卡片本体。 + * + * 回归背景:卡片预览 Hook 的 `resources` 入参同时决定 `identityByResourceId`,而 + * `renderResourceBookCard` 在身份缺失时直接不挂载卡片。曾为缩小热预取范围把该入参 + * 收窄成「当前分页栏目」,结果总览里除当前栏目外只剩栏目标题栏加空的层叠占位。 + * + * 本用例自带 manifest(不改上面共用 fixture),并把 `category` 显式写死, + * 让 `audio` / `document` / `character` / `unclassified` / `version` 五个栏目同时非空。 + * `audio` 刻意保持 `idle`(音频不接受可见性预取),用来证明卡片挂载不依赖预览是否已就绪。 + */ + it('mounts a real card body in every non-empty resource section overview', async () => { + const manifest = createGameCreationAppManifest( + 'workbench-section-card-bodies', + '总览卡片栏目项目', + ); + // manifest 资产上的分类字段名是 `category`(落盘权威值);不写时只会按 `kind` 派生。 + manifest.assets = [ + { + id: 'section-card-document', + kind: 'design-document', + mediaType: 'text/markdown', + localPath: 'memory/section-card.md', + source: { kind: 'generated', taskId: 'design-foundation' }, + category: 'document', + }, + { + id: 'section-card-character', + kind: 'character', + mediaType: 'image/png', + localPath: 'assets/section-card-character.png', + source: { kind: 'generated', taskId: 'art-asset-plan' }, + category: 'character', + }, + { + id: 'section-card-audio', + kind: 'background-music', + mediaType: 'audio/mpeg', + localPath: 'assets/section-card-audio.mp3', + source: { kind: 'generated', taskId: 'audio-asset-plan' }, + category: 'audio', + }, + { + id: 'section-card-unclassified', + kind: 'game-code', + mediaType: 'text/javascript', + localPath: 'game/section-card.js', + source: { kind: 'generated', taskId: 'code-prototype' }, + category: 'unclassified', + }, + ]; + manifest.versions = [ + { + versionId: 'section-card-version', + parentVersionId: null, + projectRevision: 1, + resourceBindings: [], + createdReason: 'initial', + createdAt: 1, + }, + ]; + const sectionProjectResources = projectResourcesFromReadModels( + manifest, + [], + [], + ); + const expectedCategoryByResourceId = new Map( + sectionProjectResources.map((resource) => [ + resource.id, + resource.category, + ]), + ); + let layoutRevision = 0; + const invoke = vi.fn( + async (command: string, args?: Record) => { + if (command === 'read_local_project_resource_graph') { + return resourceGraphForInputs([]); + } + if (command === 'read_local_project_resource_canvas_layout') { + return { + schemaVersion: 'game-creator-resource-layout.v1', + projectId: args?.expectedProjectId, + mode: args?.mode, + revision: layoutRevision, + positions: [], + updatedAt: layoutRevision, + }; + } + if (command === 'update_local_project_resource_canvas_layout') { + layoutRevision += 1; + return { + status: 'updated', + layout: { + schemaVersion: 'game-creator-resource-layout.v1', + projectId: args?.expectedProjectId, + mode: args?.mode, + revision: layoutRevision, + positions: args?.positions, + updatedAt: layoutRevision, + }, + }; + } + throw new Error(`unexpected invoke ${command}`); + }, + ); + window.__TAURI__ = { core: { invoke } }; + + render( + React.createElement(ProjectDevelopmentView, { + projectName: manifest.name, + projectPath: '/tmp/workbench-section-card-bodies', + manifest, + attachments: [], + recentRunStatus: null, + recentRunStopReason: null, + supervisor: React.createElement('div', null, '项目总控'), + onHomeOpen: vi.fn(), + onProjectsOpen: vi.fn(), + }), + ); + + // 逐栏目断言:栏目非空是前提,真实卡片本体存在才是回归判据。 + for (const category of [ + 'character', + 'audio', + 'document', + 'unclassified', + 'version', + ]) { + const resourcesInCategory = sectionProjectResources.filter( + (resource) => resource.category === category, + ); + expect(resourcesInCategory.length).toBeGreaterThan(0); + const expectedCardId = resourcesInCategory[0]!.id; + await waitFor(() => { + const sceneCard = document.querySelector( + `.game-resource-book-scene-card[data-resource-book-category="${category}"]`, + ); + expect(sceneCard).not.toBeNull(); + const cardBody = sceneCard?.querySelector( + '.game-resource-card', + ); + expect(cardBody).not.toBeNull(); + expect(cardBody?.dataset.resourceCardId).toBe(expectedCardId); + expect( + expectedCategoryByResourceId.get(cardBody!.dataset.resourceCardId!), + ).toBe(category); + }); + } + }); }