Files
Genarrative/apps/ai-game-creator-shell/tests/resourceCanvasQuickEditModel.test.ts
T
k88936 594063a1e0 测试:修正 AGC 测试文件的类型错误
- 按 vitest 0.34 的参数元组口径改写 mock 泛型
- 修正 noUncheckedIndexedAccess 下的下标访问
- 补齐与当前类型脱节的测试桩字段
- 按 scripts/*.mjs 的运行时契约补局部类型声明
2026-10-03 17:41:30 +08:00

210 lines
7.2 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type { CanvasLayer } from '../../../src/components/image-editor/ImageCanvasEditorTypes';
import type { ChatReference } from '../src/features/project-workspace/resourceReferences';
import {
createEmptyResourceQuickEditDraftStore,
createResourceQuickEditPanelDraft,
dropResourceQuickEditDraft,
isResourceQuickEditDraftEmpty,
listResourceQuickEditDraftEntries,
readResourceQuickEditDraft,
resourceQuickEditDraftPromptExcerpt,
restoreResourceQuickEditPanelDraft,
writeResourceQuickEditDraft,
} from '../src/features/resource-canvas/resourceCanvasQuickEditModel';
const SOURCE_LAYER: CanvasLayer = {
id: 'asset:source-art',
resourceId: 'asset:source-art',
title: 'source-art.png',
src: '/source-art.png',
x: 0,
y: 0,
width: 512,
height: 512,
originalWidth: 1024,
originalHeight: 1024,
zIndex: 1,
// 现行 CanvasSourceType 只认 'uploaded' / 'generated' / 'mock_generated'。
sourceType: 'uploaded',
};
const RULES_REFERENCE: ChatReference = {
type: 'resource',
resourceId: 'rules-resource',
// 旧的 `game-rules` kind 已不在当前联合里,夹具按真实类型收敛到最接近的 document。
kind: 'document',
mediaType: 'text/markdown',
label: 'rules',
category: 'document',
tags: [],
source: 'asset-picker',
};
describe('快速编辑草稿表', () => {
it('提示词与引用各自上报时互相补齐,不改动原表', () => {
const withPrompt = writeResourceQuickEditDraft(
createEmptyResourceQuickEditDraftStore(),
{ key: 'asset:source-art', prompt: '把夜色改成星空', updatedAt: 10 },
);
const withReferences = writeResourceQuickEditDraft(withPrompt, {
key: 'asset:source-art',
references: [RULES_REFERENCE],
updatedAt: 20,
});
// 只补引用那一次不能把提示词清掉,反之亦然:输入区两条状态是分别上报的。
expect(
readResourceQuickEditDraft(withReferences, 'asset:source-art'),
).toEqual({
prompt: '把夜色改成星空',
references: [RULES_REFERENCE],
updatedAt: 20,
});
expect(readResourceQuickEditDraft(withPrompt, 'asset:source-art')).toEqual({
prompt: '把夜色改成星空',
references: [],
updatedAt: 10,
});
// 只读语义:写操作返回新表、源表不动;空表走工厂,每次都是独立的一张。
expect(withPrompt.size).toBe(1);
expect(createEmptyResourceQuickEditDraftStore().size).toBe(0);
expect(createEmptyResourceQuickEditDraftStore()).not.toBe(
createEmptyResourceQuickEditDraftStore(),
);
});
/**
* 键是资源**路径**,不是投影 id:正规化把卡片从 `task:…` 换成 `asset:…` 之后,
* 同一张卡的草稿必须在恢复入口里照样对得上号,否则用户刚写的提示词会静默消失。
*/
it('草稿按路径归属,卡片换成 asset 投影后仍在入口里', () => {
const store = writeResourceQuickEditDraft(
createEmptyResourceQuickEditDraftStore(),
{
key: 'assets/generated/hero.png',
prompt: '改成红色头发',
references: [RULES_REFERENCE],
updatedAt: 10,
},
);
// 正规化前后是同一路径的两张卡;入口只认路径,落在哪一张上都该列出来。
expect(
listResourceQuickEditDraftEntries(store, [
{ path: 'assets/generated/hero.png', label: 'hero.png(正式素材)' },
]),
).toEqual([
{
key: 'assets/generated/hero.png',
label: 'hero.png(正式素材)',
prompt: '改成红色头发',
updatedAt: 10,
},
]);
// 资源真的不在项目里时(删掉了)才不入列表,记录本身不在这一步删。
expect(listResourceQuickEditDraftEntries(store, [])).toEqual([]);
expect(store.size).toBe(1);
});
it('恢复入口按最后改动时间从新到旧', () => {
const store = writeResourceQuickEditDraft(
writeResourceQuickEditDraft(createEmptyResourceQuickEditDraftStore(), {
key: 'assets/older.png',
prompt: '早写的',
updatedAt: 10,
}),
{ key: 'assets/newer.png', prompt: '后写的', updatedAt: 30 },
);
expect(
listResourceQuickEditDraftEntries(store, [
{ path: 'assets/older.png', label: 'older.png' },
{ path: 'assets/newer.png', label: 'newer.png' },
]).map((entry) => entry.key),
).toEqual(['assets/newer.png', 'assets/older.png']);
});
it('提示词与引用都清空后不留条目', () => {
const store = writeResourceQuickEditDraft(
createEmptyResourceQuickEditDraftStore(),
{
key: 'asset:source-art',
prompt: '把夜色改成星空',
updatedAt: 10,
},
);
const cleared = writeResourceQuickEditDraft(store, {
key: 'asset:source-art',
prompt: '',
updatedAt: 20,
});
expect(
isResourceQuickEditDraftEmpty({ prompt: ' ', references: [] }),
).toBe(true);
expect(
readResourceQuickEditDraft(cleared, 'asset:source-art'),
).toBeUndefined();
expect(cleared.size).toBe(0);
});
it('丢弃只删目标键,未记录过时原样返回', () => {
const store = writeResourceQuickEditDraft(
createEmptyResourceQuickEditDraftStore(),
{
key: 'asset:a',
prompt: 'A 的草稿',
updatedAt: 10,
},
);
const withTwo = writeResourceQuickEditDraft(store, {
key: 'asset:b',
prompt: 'B 的草稿',
updatedAt: 20,
});
const dropped = dropResourceQuickEditDraft(withTwo, 'asset:a');
expect(readResourceQuickEditDraft(dropped, 'asset:a')).toBeUndefined();
expect(readResourceQuickEditDraft(dropped, 'asset:b')?.prompt).toBe(
'B 的草稿',
);
expect(dropResourceQuickEditDraft(dropped, 'asset:missing')).toBe(dropped);
expect(dropResourceQuickEditDraft(dropped, null)).toBe(dropped);
expect(readResourceQuickEditDraft(dropped, null)).toBeUndefined();
});
it('回填只覆盖提示词,模型与比例仍取当下默认值', () => {
const panelDraft = createResourceQuickEditPanelDraft(SOURCE_LAYER);
const restored = restoreResourceQuickEditPanelDraft(panelDraft, {
prompt: '把夜色改成星空',
references: [],
updatedAt: 10,
});
expect(restored.prompt).toBe('把夜色改成星空');
expect(restored.model).toBe(panelDraft.model);
expect(restored.aspectRatio).toBe(panelDraft.aspectRatio);
expect(restored.size).toBe(panelDraft.size);
expect(restored.status).toBe('idle');
// 没有草稿时面板原样返回:默认面板不该被这一步改写。
expect(restoreResourceQuickEditPanelDraft(panelDraft, undefined)).toBe(
panelDraft,
);
});
it('摘要压缩空白并按上限截断', () => {
expect(resourceQuickEditDraftPromptExcerpt('')).toBe('(尚未填写提示词)');
expect(resourceQuickEditDraftPromptExcerpt(' \n ')).toBe(
'(尚未填写提示词)',
);
expect(resourceQuickEditDraftPromptExcerpt('把夜色\n改成 星空')).toBe(
'把夜色 改成 星空',
);
expect(resourceQuickEditDraftPromptExcerpt('一二三四五', 3)).toBe(
'一二三…',
);
expect(resourceQuickEditDraftPromptExcerpt('一二三', 3)).toBe('一二三');
});
});