补逐栏目断言:资源总览每个非空栏目都必须挂载真实卡片本体

上一轮把「总览其它栏目不画卡片」的回归结论建立在推理上(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 逐字节一致(变异验证后已还原并核对)。
This commit is contained in:
2026-09-11 01:59:21 +08:00
parent 84b8f5130b
commit e3616a44cc
@@ -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<string, unknown>) => {
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<HTMLElement>(
`.game-resource-book-scene-card[data-resource-book-category="${category}"]`,
);
expect(sceneCard).not.toBeNull();
const cardBody = sceneCard?.querySelector<HTMLElement>(
'.game-resource-card',
);
expect(cardBody).not.toBeNull();
expect(cardBody?.dataset.resourceCardId).toBe(expectedCardId);
expect(
expectedCategoryByResourceId.get(cardBody!.dataset.resourceCardId!),
).toBe(category);
});
}
});
}