补逐栏目断言:资源总览每个非空栏目都必须挂载真实卡片本体
上一轮把「总览其它栏目不画卡片」的回归结论建立在推理上(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:
@@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user