From e3616a44ccd82bd85e085b3cc088eb29435b8c2a Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Fri, 11 Sep 2026 01:59:21 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=80=90=E6=A0=8F=E7=9B=AE=E6=96=AD?= =?UTF-8?q?=E8=A8=80=EF=BC=9A=E8=B5=84=E6=BA=90=E6=80=BB=E8=A7=88=E6=AF=8F?= =?UTF-8?q?=E4=B8=AA=E9=9D=9E=E7=A9=BA=E6=A0=8F=E7=9B=AE=E9=83=BD=E5=BF=85?= =?UTF-8?q?=E9=A1=BB=E6=8C=82=E8=BD=BD=E7=9C=9F=E5=AE=9E=E5=8D=A1=E7=89=87?= =?UTF-8?q?=E6=9C=AC=E4=BD=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一轮把「总览其它栏目不画卡片」的回归结论建立在推理上(identity map 由全量投影构建 ⇒ 不会有卡片因身份缺失而不挂载)。本轮把它变成实测。 - 新增 `appSurface > mounts a real card body in every non-empty resource section overview`:自带 manifest,把 `category` 显式写死,让 `character` / `audio` / `document` / `unclassified` / `version` 五个栏目同时非空(外加空的 `ui-interaction` / `scene`),再逐栏目断言 `.game-resource-book-scene-card[data-resource-book-category=…]` 内存在 `.game-resource-card` 本体,并用 `.game-resource-card` 的 `data-resource-card-id` 回查投影结果,证明卡片确实落在被断言的栏目(不是靠「有子节点」这种恒真守卫蒙过去)。`audio` 刻意保持 `idle`(音频不接受可见性预取),用来证明卡片挂载不依赖预览是否已就绪。 - 自带 manifest 而不是改共用 fixture:`addSectionResources` 的四个资产被同一测试后续多处断言引用(例如 `选中资源:待归类 section.md`),给它补 `category` 会把 `section.md` 从「待归类」挪进「文档」,连带打挂那些既有断言。新用例因此只新增、不改动既有断言。 - 分类字段名踩坑记录:manifest 资产上的字段名是 `category`,不是 `assetCategory`(`assetCategory` 是前端投影后的 `ProjectResource` 字段)。先写成 `assetCategory` 时它被静默忽略,`gameCreationAppAssetCategory` 只能按 `kind` 派生,`design-document` / `game-code` 都不是 canonical kind,于是 `section.md` 落在「待归类」、`document` 栏目为空。 - 变异验证(本断言的价值所在):把 `resources` 改回 `activePageResources`(即回到回归状态)后,本用例与既有 `uses one full-page canvas per resource section with dependency-only guide lines` **双双失败**(`expected null not to be null`);恢复后两者通过。断言确实守住了这条回归。 - 验证:`npm run test -- apps/ai-game-creator-shell/tests` 81 files passed,1172 passed / 4 skipped / 0 failed;typecheck exit 0;eslint 与 prettier 干净;check:encoding 4374 文件;`git diff --check` 干净。本轮只改测试文件,`src/**` 与 HEAD 逐字节一致(变异验证后已还原并核对)。 --- .../appSurface/project-development.suite.ts | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) 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); + }); + } + }); }