Files
Genarrative/apps/ai-game-creator-shell/tests/resourceCanvasQuickEditModel.test.ts
T
suzmii 1e17d2c852
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Successful in 7m44s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 7m57s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 5m44s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 5m53s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m21s
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m27s
Project CI / Frontend tests (pull_request) Successful in 7m4s
Project CI / AI game creator shell web tests (pull_request) Successful in 6m22s
Project CI / Native shell tests (pull_request) Successful in 10m13s
补齐画布验收第一批:润色回包提示、未提交草稿恢复与派生名预检
AI 润色回包与原文相同时给出明确提示,不再落下点了等于没点的「恢复原文」假入口
润色「与原文相同」的判据改为规范化之后要写回宿主的文本,回包被长度上限截回原文同样算没变化
共享聊天输入区显示润色提示,并让提示随用户手改提示词失效
快速编辑未提交草稿改按资源路径归属,收起面板或被旁路重投影后都不再静默丢掉
恢复入口按路径与本会话草稿对号,并区分「本会话未提交」与原生账本条目
账本读取失败时入口仍报出本会话草稿条数,「继续编辑」重开面板回填提示词与 @ 引用
派生资源名预检接入快速编辑与角色动画两条提交路径,早于正规化写盘
名称预检自身抛错时回写面板失败态,不再留下「点了没反应」的提交按钮
浮层关闭判定改在捕获阶段冻结,避免浮层内按钮卸载自己时整块面板被自己人收掉
恢复入口动作按钮收进统一容器,补移动端纵向排布与禁用态样式
空草稿表改工厂函数,跨语言对表用例按模块路径解析 Rust 源码
补回归用例:旁路重投影后的草稿、原样回包提示、提交前名称拦截、草稿表读写与跨卡隔离
同步 decision-log 与画布验收待办的进展说明
2026-09-20 17:36:02 +08:00

208 lines
7.0 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,
sourceType: 'upload',
};
const RULES_REFERENCE: ChatReference = {
type: 'resource',
resourceId: 'rules-resource',
kind: 'game-rules',
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('一二三');
});
});