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 与画布验收待办的进展说明
208 lines
7.0 KiB
TypeScript
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('一二三');
|
|
});
|
|
});
|