Files
Genarrative/apps/ai-game-creator-shell/tests/resourceCanvasToolbarModel.test.ts
T
suzmii e765c9f8a6 资源卡工具条接通「生成动画」:只放行真能跑通的角色图派生
- resourceCanvasToolbarModel:只有在「已登记的栅格角色图」上放行 character-animation
  (非角色 / 未登记为正式素材 / 非栅格三类都不放行;Rust 侧角色动画要解码源图取宽高,SVG 必失败);其余动作仍按 opt-in 不渲染,并写明不做它们各自的阻塞证据。
- index.tsx:把源解析与 revision 缓存抽成 resolveResourceDeriveSource,快速编辑与生成动画共用,避免同一个源在两条链路上得到两份 manifest 视图;新增生成动画的源/浮层状态、请求身份(与快速编辑同一套「提示词变了才重铸」口径)与打开/提交/关闭时机。
- 面板复用共享 ImageCanvasCharacterAnimationPanelView,参数入口显式关闭(比例/时长/清晰度与模型由客户端 Rust 固定为 same/720p/32 帧/4 秒/seedance2.0-fast,改它不改变请求);档位常量与 Rust 常量同值,泥点价按同一算法算,避免按钮上出现与实际计费不符的数字。
- 「改造」(redraw) 不再另起入口:其语义由既有「快速编辑」承担,接独立按钮只是同链路重复入口。
- 断言改写(四不写):角色资源那条由「只放行快速编辑与下载」改写为「放行生成动画」,并新增三条反向守卫(非角色图片 / 角色图未登记 / 非栅格角色图均不放行)。
- 端到端新增「从角色资源卡生成动画」:断言 editKind/generationMode/prompt/assetName/sourceAssetId 与泥点价档位,并断言参数入口不存在。
2026-09-11 20:08:22 +08:00

265 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest';
import { createGameCreationAppManifest } from '../../../packages/shared/src/contracts/gameCreationApp';
import { resolveResourceCanvasToolbarActions } from '../src/features/resource-canvas/resourceCanvasToolbarModel';
import type {
ProjectResource,
ProjectVersionResourceSummary,
} from '../src/view/project-development/resourceProjectionModel';
function createResource(
overrides: Partial<ProjectResource> = {},
): ProjectResource {
return {
id: 'asset:hero',
category: 'ui-interaction',
subtype: 'icon',
label: '主角立绘',
path: 'assets/hero.png',
mediaType: 'image/png',
sourceLabel: '生成',
taskTitle: null,
manifestAssetId: 'hero',
producerTaskId: null,
externalResourceId: null,
referenceResourceIds: [],
dependencies: [],
dependencyDepth: 0,
...overrides,
};
}
/** 虚拟版本条目:只有版本摘要,没有对应文件。 */
function createVersionSummary(): ProjectVersionResourceSummary {
return {
versionId: 'version-1',
parentVersionId: null,
projectRevision: 3,
resourceBindings: [],
createdReason: 'initial',
createdAt: 1,
label: '版本 1',
childVersionIds: [],
};
}
function createManifest() {
return createGameCreationAppManifest(
'project-toolbar-actions',
'工具条动作测试',
);
}
/** 把种子任务的第一个任务改成已完成并登记指定产物,用于覆盖任务产物正规化门禁。 */
function createManifestWithProducingTask(
status: 'completed' | 'running',
artifactPath: string,
) {
const manifest = createManifest();
manifest.tasks = manifest.tasks.map((task, index) =>
index === 0 ? { ...task, status, artifacts: [artifactPath] } : task,
);
return manifest;
}
function toolbarActions(
manifest: ReturnType<typeof createManifest>,
resource: ProjectResource,
) {
return [...resolveResourceCanvasToolbarActions(manifest, resource)];
}
/**
* AGC 资源画布工具条的放行判据只有一条:**进集合的动作必须真能跑通**。
*
* `ImageCanvasSelectedLayerToolbarView` 收到 `supportedActions` 后完全以集合为准,
* 宿主编排层里还是空回调的动作一旦进集合就会渲染成"点了没反应"的按钮,因此这里
* 逐条钉住当前真正接通的动作集合(见
* `docs/project-memory/shared-memory/pitfalls.md` 的「资源画布工具条的『改造』
* 『角色动画』按钮点了没反应」)。
*
* 当前真实接通的是「快速编辑」(normalize → derive(editKind='image-reference'))、
* 「生成动画」(normalize → derive(editKind='character-animation') → 服务端
* `/api/editor/character-animations/generations`)与「下载」(资源面板同一条原生保存
* 对话框 + `save_local_project_asset_file` 链路)。
*/
describe('resource canvas toolbar actions', () => {
it('已登记 manifest 资产的栅格图片放行快速编辑与下载', () => {
expect(toolbarActions(createManifest(), createResource())).toEqual([
'quick-edit',
'download',
]);
});
it('音频与视频资源不再放行空回调的改造动作,但仍放行下载', () => {
const manifest = createManifest();
expect(
toolbarActions(
manifest,
createResource({
id: 'asset:bgm',
category: 'audio',
subtype: 'background-music',
label: '背景音乐',
path: 'assets/bgm.mp3',
mediaType: 'audio/mpeg',
manifestAssetId: 'bgm',
}),
),
).toEqual(['download']);
expect(
toolbarActions(
manifest,
createResource({
id: 'asset:intro',
subtype: 'video',
label: '开场动画',
path: 'assets/intro.mp4',
mediaType: 'video/mp4',
manifestAssetId: 'intro',
}),
),
).toEqual(['download']);
});
it('角色资源放行「生成动画」:源是已登记的栅格图片', () => {
// 这条断言由「不再放行空回调的角色动画动作」改写而来(四不写:旧断言描述的是
// 未接通状态,已被真实链路取代,不留墓碑)。宿主编排层现在把
// `onOpenCharacterAnimationPanel` 接到 `submitResourceCharacterAnimation`
// derive editKind='character-animation'),所以进集合不再是假按钮。
expect(
toolbarActions(
createManifest(),
createResource({ id: 'asset:hero-action', subtype: 'character' }),
),
).toEqual(['quick-edit', 'character-animation', 'download']);
});
it('非角色的图片资源不放行生成动画,避免渲染出与源类型不匹配的按钮', () => {
expect(
toolbarActions(
createManifest(),
createResource({ id: 'asset:scene', subtype: 'scene' }),
),
).toEqual(['quick-edit', 'download']);
expect(
toolbarActions(
createManifest(),
createResource({ id: 'asset:icon', subtype: 'icon' }),
),
).toEqual(['quick-edit', 'download']);
});
it('角色资源没登记成正式素材时不放行生成动画,只留下真能跑通的下载', () => {
expect(
toolbarActions(
createManifest(),
createResource({
id: 'asset:hero-artifact',
subtype: 'character',
manifestAssetId: null,
producerTaskId: 'task-hero',
}),
),
).toEqual(['download']);
});
it('非栅格的角色资源不放行生成动画:Rust 要解码源图取宽高,SVG 会直接失败', () => {
expect(
toolbarActions(
createManifest(),
createResource({
id: 'asset:hero-svg',
subtype: 'character',
path: 'assets/hero.svg',
mediaType: 'image/svg+xml',
}),
),
).toEqual(['download']);
});
it('未登记 manifest 资产且没有已完成任务产物背书的资源只放行下载', () => {
expect(
toolbarActions(
createManifest(),
createResource({ manifestAssetId: null, producerTaskId: 'task-art' }),
),
).toEqual(['download']);
});
it('只认已完成任务的栅格产物,未完成任务的产物不放行快速编辑但可下载', () => {
const artifactPath = 'assets/hero.png';
const completedManifest = createManifestWithProducingTask(
'completed',
artifactPath,
);
expect(
toolbarActions(
completedManifest,
createResource({
manifestAssetId: null,
producerTaskId: completedManifest.tasks[0].id,
}),
),
).toEqual(['quick-edit', 'download']);
const runningManifest = createManifestWithProducingTask(
'running',
artifactPath,
);
expect(
toolbarActions(
runningManifest,
createResource({
manifestAssetId: null,
producerTaskId: runningManifest.tasks[0].id,
}),
),
).toEqual(['download']);
});
it('非栅格图片不进快速编辑的派生门禁,但仍可下载', () => {
expect(
toolbarActions(
createManifest(),
createResource({
id: 'asset:icon',
path: 'assets/icon.svg',
mediaType: 'image/svg+xml',
}),
),
).toEqual(['download']);
});
it('拿得到本地文件路径的资源才放行下载', () => {
expect(toolbarActions(createManifest(), createResource())).toContain(
'download',
);
});
it('拿不到本地文件路径的资源不放行下载,避免渲染出点了报错的按钮', () => {
const manifest = createManifest();
expect(
toolbarActions(
manifest,
createResource({ path: '', manifestAssetId: 'virtual' }),
),
).not.toContain('download');
expect(
toolbarActions(
manifest,
createResource({
id: 'version:1',
category: 'version',
subtype: 'version',
label: '版本 1',
path: '',
mediaType: 'application/vnd.genarrative.project-version+json',
manifestAssetId: null,
version: createVersionSummary(),
}),
),
).not.toContain('download');
});
});