1c2f82e246
## 变更内容 ### 1. 普通资源画布隐藏游戏代码 - 资源画布的大纲、分页和卡片展示不再显示“游戏代码”栏目。 - 保留内部完整的五栏目布局契约,避免已有代码资源坐标被重写。 - 补充回归测试,确认游戏代码不会出现在普通资源画布中,且代码端点不可见时不会渲染对应依赖边。 ### 2. 增加资源类型角标 - 资源卡右上角增加类型角标,用于快速识别资源类型。 - 类型从既有资源投影字段推导,覆盖图片、SVG、视频、音频、文档、任务产物、Agent 回执、项目版本等类型,并为未知类型兜底。 - 该能力仅用于前端展示,不新增后端字段或修改资源投影契约。 ### 3. 限制默认自动适配缩放 - 画布首次自动适配和显式复位时的默认最大缩放统一限制为 `1.5`,避免内容较少或只有一个资源时卡片被放得过大。 - 该限制对设计文档、美术、音频、游戏代码、项目版本全部资源栏目生效。 - 用户主动缩放仍沿用现有上限,不受默认适配上限影响。 - 用户调整后的画布缩放会按排序模式和栏目保留;切换到其他栏目再返回后,仍恢复用户调整后的 viewport,而不是重新自动放大。 ## 兼容性 - 不修改后端 API、SpacetimeDB schema 或 `/api/external/v1`。 - 资源类型角标是展示层推导逻辑,不改变资源数据结构。 - 内部布局仍保留游戏代码栏目契约,已有布局数据不需要迁移。 ## 验证 - `resourceCanvasLayoutModel.test.ts` - `project-development.suite.ts` 相关 App Surface 用例 - `npm run typecheck` - `npm run check:encoding` - Prettier 相关文件检查 - `git diff --check` 历史分支检查不替代最新 head CI;PR 更新后将以最新 CI 结果为准。 --------- Co-authored-by: 段舒康 <kdletters@qq.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/208 Co-authored-by: 董羽秦 <suzmii@qq.com> Co-committed-by: 董羽秦 <suzmii@qq.com>
347 lines
9.3 KiB
TypeScript
347 lines
9.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { createGameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp';
|
|
import {
|
|
projectResourcesFromReadModels,
|
|
projectResourceTypeLabel,
|
|
} from '../src/view/project-development/resourceProjectionModel';
|
|
|
|
describe('项目资源投影', () => {
|
|
it('只把明确资源投影到固定分类,未知任务产物不会伪装成项目版本', () => {
|
|
const manifest = createGameCreationAppManifest(
|
|
'resource-projection',
|
|
'资源投影测试',
|
|
);
|
|
const designTask = manifest.tasks.find(
|
|
(task) => task.id === 'design-foundation',
|
|
);
|
|
const audioTask = manifest.tasks.find(
|
|
(task) => task.id === 'audio-asset-plan',
|
|
);
|
|
if (!designTask || !audioTask) {
|
|
throw new Error('缺少资源投影测试任务');
|
|
}
|
|
designTask.status = 'completed';
|
|
designTask.artifacts = [
|
|
'memory/game-design.md',
|
|
'assets/ui-preview.svg',
|
|
'build/game.bundle',
|
|
];
|
|
audioTask.status = 'completed';
|
|
audioTask.artifacts = ['audio/unregistered-bgm.wav'];
|
|
manifest.assets = [
|
|
{
|
|
id: 'game-entry',
|
|
kind: 'game-entry',
|
|
mediaType: 'text/html',
|
|
localPath: 'game/index.html',
|
|
source: { kind: 'generated' },
|
|
},
|
|
{
|
|
id: 'registered-bgm',
|
|
kind: 'bgm',
|
|
mediaType: 'audio/wav',
|
|
localPath: 'audio/registered-bgm.wav',
|
|
source: { kind: 'generated' },
|
|
},
|
|
{
|
|
id: 'character-animation',
|
|
kind: 'character-animation',
|
|
mediaType: 'application/json',
|
|
localPath: 'assets/character-animation.json',
|
|
source: { kind: 'generated' },
|
|
},
|
|
{
|
|
id: 'unknown-binary',
|
|
kind: 'binary',
|
|
mediaType: 'application/octet-stream',
|
|
localPath: 'build/game.bundle',
|
|
source: { kind: 'generated' },
|
|
},
|
|
];
|
|
manifest.versions = [
|
|
{
|
|
versionId: 'version-1',
|
|
parentVersionId: null,
|
|
projectRevision: 7,
|
|
resourceBindings: [
|
|
{ slotId: 'background-music', resourceId: 'registered-bgm' },
|
|
],
|
|
createdReason: 'initial',
|
|
createdAt: 1,
|
|
},
|
|
];
|
|
|
|
const resources = projectResourcesFromReadModels(
|
|
manifest,
|
|
[
|
|
{
|
|
fileName: 'rules.yaml',
|
|
mediaType: 'application/yaml',
|
|
localPath: 'imports/rules.yaml',
|
|
status: 'imported',
|
|
},
|
|
{
|
|
fileName: 'voice.ogg',
|
|
mediaType: 'audio/ogg',
|
|
localPath: 'imports/voice.ogg',
|
|
status: 'imported',
|
|
},
|
|
{
|
|
fileName: 'archive.zip',
|
|
mediaType: 'application/zip',
|
|
localPath: 'imports/archive.zip',
|
|
status: 'imported',
|
|
},
|
|
],
|
|
[
|
|
{
|
|
agentId: 'design-foundation',
|
|
runId: 'final-run',
|
|
label: '玩法策划 Agent',
|
|
title: '玩法文档回执',
|
|
content: '回执正文',
|
|
updatedAt: 1,
|
|
},
|
|
],
|
|
);
|
|
|
|
expect(resources.map(({ id, category }) => ({ id, category }))).toEqual(
|
|
expect.arrayContaining([
|
|
{
|
|
id: 'task:design-foundation:memory/game-design.md',
|
|
category: 'document',
|
|
},
|
|
{ id: 'asset:game-entry', category: 'code' },
|
|
{
|
|
id: 'task:design-foundation:assets/ui-preview.svg',
|
|
category: 'art',
|
|
},
|
|
{ id: 'asset:registered-bgm', category: 'audio' },
|
|
{ id: 'asset:character-animation', category: 'art' },
|
|
{ id: 'attachment:imports/rules.yaml', category: 'document' },
|
|
{ id: 'attachment:imports/voice.ogg', category: 'audio' },
|
|
{
|
|
id: 'agent-result:design-foundation:final-run',
|
|
category: 'document',
|
|
},
|
|
{
|
|
id: 'task:audio-asset-plan:audio/unregistered-bgm.wav',
|
|
category: 'audio',
|
|
},
|
|
{ id: 'version:version-1', category: 'version' },
|
|
]),
|
|
);
|
|
expect(
|
|
resources.some(({ id }) =>
|
|
[
|
|
'task:design-foundation:build/game.bundle',
|
|
'asset:unknown-binary',
|
|
'attachment:imports/archive.zip',
|
|
].includes(id),
|
|
),
|
|
).toBe(false);
|
|
expect(
|
|
resources.filter(({ category }) => category === 'version'),
|
|
).toHaveLength(1);
|
|
});
|
|
|
|
it('显示名称变化不会改变正式资源身份', () => {
|
|
const manifest = createGameCreationAppManifest(
|
|
'stable-resource-id',
|
|
'稳定资源身份测试',
|
|
);
|
|
manifest.versions = [
|
|
{
|
|
versionId: 'stable-version',
|
|
parentVersionId: null,
|
|
projectRevision: 1,
|
|
resourceBindings: [],
|
|
createdReason: 'initial',
|
|
createdAt: 1,
|
|
},
|
|
];
|
|
const first = projectResourcesFromReadModels(
|
|
manifest,
|
|
[],
|
|
[
|
|
{
|
|
agentId: 'design-foundation',
|
|
runId: 'stable-run',
|
|
label: '策划 Agent',
|
|
title: '旧标题',
|
|
content: '正文',
|
|
updatedAt: 1,
|
|
},
|
|
],
|
|
);
|
|
const second = projectResourcesFromReadModels(
|
|
manifest,
|
|
[],
|
|
[
|
|
{
|
|
agentId: 'design-foundation',
|
|
runId: 'stable-run',
|
|
label: '策划 Agent',
|
|
title: '新标题',
|
|
content: '正文',
|
|
updatedAt: 2,
|
|
},
|
|
],
|
|
);
|
|
|
|
expect(first.map(({ id }) => id)).toEqual(second.map(({ id }) => id));
|
|
});
|
|
|
|
it('只从 manifest 投影版本并保留直接父子关系', () => {
|
|
const manifest = createGameCreationAppManifest(
|
|
'version-projection',
|
|
'版本投影测试',
|
|
);
|
|
manifest.versions = [
|
|
{
|
|
versionId: 'version-root',
|
|
parentVersionId: null,
|
|
projectRevision: 2,
|
|
resourceBindings: [],
|
|
createdReason: 'initial',
|
|
createdAt: 100,
|
|
},
|
|
{
|
|
versionId: 'version-child',
|
|
parentVersionId: 'version-root',
|
|
projectRevision: 3,
|
|
resourceBindings: [],
|
|
createdReason: 'agent-revision',
|
|
createdAt: 200,
|
|
},
|
|
];
|
|
|
|
const versions = projectResourcesFromReadModels(manifest, [], []).filter(
|
|
(resource) => resource.category === 'version',
|
|
);
|
|
|
|
expect(versions.map((version) => version.label)).toEqual([
|
|
'版本 1',
|
|
'版本 2',
|
|
]);
|
|
expect(versions[0]?.version?.childVersionIds).toEqual(['version-child']);
|
|
expect(versions[1]?.version?.parentVersionId).toBe('version-root');
|
|
});
|
|
|
|
it('按稳定优先级推导资源卡类型标识并为未知类型兜底', () => {
|
|
expect(
|
|
projectResourceTypeLabel({
|
|
category: 'version',
|
|
subtype: 'project-version',
|
|
path: 'versions/v1',
|
|
mediaType: '正式项目版本',
|
|
}),
|
|
).toBe('项目版本');
|
|
expect(
|
|
projectResourceTypeLabel({
|
|
category: 'document',
|
|
subtype: 'agent-result',
|
|
path: '专业 Agent 文本回执',
|
|
mediaType: 'Agent 历史文本回执',
|
|
}),
|
|
).toBe('Agent 回执');
|
|
expect(
|
|
projectResourceTypeLabel({
|
|
category: 'document',
|
|
subtype: 'task-artifact',
|
|
path: 'memory/plan.md',
|
|
mediaType: '项目文档',
|
|
}),
|
|
).toBe('文档');
|
|
expect(
|
|
projectResourceTypeLabel({
|
|
category: 'audio',
|
|
subtype: 'task-artifact',
|
|
path: 'audio/theme.wav',
|
|
mediaType: '音乐音效产物',
|
|
}),
|
|
).toBe('音频');
|
|
expect(
|
|
projectResourceTypeLabel({
|
|
category: 'art',
|
|
subtype: 'task-artifact',
|
|
path: 'assets/hero.svg',
|
|
mediaType: '美术产物',
|
|
}),
|
|
).toBe('SVG');
|
|
expect(
|
|
projectResourceTypeLabel({
|
|
category: 'art',
|
|
subtype: 'task-artifact',
|
|
path: 'assets/unknown',
|
|
mediaType: '美术产物',
|
|
}),
|
|
).toBe('图片');
|
|
expect(
|
|
projectResourceTypeLabel({
|
|
category: 'art',
|
|
subtype: 'character',
|
|
path: 'assets/hero.svg',
|
|
mediaType: 'image/svg+xml',
|
|
}),
|
|
).toBe('SVG');
|
|
expect(
|
|
projectResourceTypeLabel({
|
|
category: 'art',
|
|
subtype: 'video',
|
|
path: 'assets/intro.mp4',
|
|
mediaType: 'video/mp4',
|
|
}),
|
|
).toBe('视频');
|
|
expect(
|
|
projectResourceTypeLabel({
|
|
category: 'art',
|
|
subtype: 'character',
|
|
path: 'assets/hero.png',
|
|
mediaType: 'image/png',
|
|
}),
|
|
).toBe('图片');
|
|
expect(
|
|
projectResourceTypeLabel({
|
|
category: 'document',
|
|
subtype: 'attachment',
|
|
path: 'docs/rules.yaml',
|
|
mediaType: 'application/yaml',
|
|
}),
|
|
).toBe('文档');
|
|
expect(
|
|
projectResourceTypeLabel({
|
|
category: 'document',
|
|
subtype: 'unknown',
|
|
path: 'data/blob',
|
|
mediaType: 'application/octet-stream',
|
|
}),
|
|
).toBe('文档');
|
|
expect(
|
|
projectResourceTypeLabel({
|
|
category: 'code',
|
|
subtype: 'source',
|
|
path: 'game/main.ts',
|
|
mediaType: 'text/typescript',
|
|
}),
|
|
).toBe('游戏代码');
|
|
expect(
|
|
projectResourceTypeLabel({
|
|
category: 'art',
|
|
subtype: 'unknown',
|
|
path: 'assets/blob',
|
|
mediaType: 'application/octet-stream',
|
|
}),
|
|
).toBe('图片');
|
|
expect(
|
|
projectResourceTypeLabel({
|
|
category: 'unsupported' as never,
|
|
subtype: 'unknown',
|
|
path: 'data/blob',
|
|
mediaType: 'application/octet-stream',
|
|
}),
|
|
).toBe('未知');
|
|
});
|
|
});
|