071faa482c
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口 完成项目 TypeScript/Prettier 与 Rust 全量格式化 修复 Pingora expected executable 门禁的空白敏感误报 同步开发运维文档与 AGC skill pack 格式化忽略规则
230 lines
8.6 KiB
TypeScript
230 lines
8.6 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
|
||
import {
|
||
appendBackgroundMusicPromptPreset,
|
||
BACKGROUND_MUSIC_PROMPT_PRESETS,
|
||
type BackgroundMusicPromptPreset,
|
||
} from './ImageCanvasBackgroundMusicPresetModel';
|
||
|
||
// 权威设计《画板音乐生成入口设计》「预设库与追加规则」的固定文案。
|
||
// 这张表就是转录校验本身,改动必须先改权威设计。
|
||
const AUTHORITATIVE_PRESETS: ReadonlyArray<
|
||
[BackgroundMusicPromptPreset['group'], string, string]
|
||
> = [
|
||
['purpose', '菜单待机', '低干扰、适合菜单待机的背景音乐'],
|
||
['purpose', '休闲消除', '轻快可爱的休闲消除背景音乐'],
|
||
['purpose', '解谜思考', '安静专注的解谜思考背景音乐'],
|
||
['purpose', '探索冒险', '温和推进的探索冒险背景音乐'],
|
||
['purpose', '对白场景', '克制柔和、留出对白空间的背景音乐'],
|
||
['purpose', '战斗前', '蓄势待发的战斗前背景音乐'],
|
||
['purpose', '胜利结算', '明亮满足的胜利结算背景音乐'],
|
||
['purpose', '失败结算', '克制低落的失败结算背景音乐'],
|
||
['purpose', '日常经营', '轻松有序的日常经营背景音乐'],
|
||
['purpose', '农场经营', '自然温暖的农场经营背景音乐'],
|
||
['purpose', '校园日常', '青春轻松的校园日常背景音乐'],
|
||
['purpose', '美食厨房', '温暖活泼的美食厨房背景音乐'],
|
||
['atmosphere', '温暖治愈', '柔和明亮的治愈背景音乐'],
|
||
['atmosphere', '神秘悬疑', '克制神秘的悬疑背景音乐'],
|
||
['atmosphere', '紧张推进', '稳定推进、逐渐紧张的背景音乐'],
|
||
['scene', '森林自然', '清新自然的森林背景音乐'],
|
||
['scene', '雨夜静谧', '雨夜静谧、略带神秘感的背景音乐'],
|
||
['scene', '海洋漂流', '开阔舒缓的海洋漂流背景音乐'],
|
||
['scene', '山野远行', '自由舒展的山野远行背景音乐'],
|
||
['scene', '糖果乐园', '甜美活泼的糖果乐园背景音乐'],
|
||
['scene', '温馨小屋', '温暖安静的温馨小屋背景音乐'],
|
||
['scene', '城市夜晚', '克制迷人的城市夜晚背景音乐'],
|
||
['scene', '太空科幻', '空灵未来感的太空科幻背景音乐'],
|
||
['scene', '赛博街区', '冷静律动的赛博街区背景音乐'],
|
||
['scene', '古风幻想', '空灵雅致的古风幻想背景音乐'],
|
||
['scene', '童话花园', '梦幻轻盈的童话花园背景音乐'],
|
||
['scene', '海底遗迹', '深邃神秘的海底遗迹背景音乐'],
|
||
['scene', '沙漠遗迹', '苍茫神秘的沙漠遗迹背景音乐'],
|
||
['scene', '熔岩洞穴', '炽热压迫的熔岩洞穴背景音乐'],
|
||
['scene', '奇妙博物馆', '好奇灵动的奇妙博物馆背景音乐'],
|
||
];
|
||
|
||
function presetByLabel(label: string) {
|
||
const preset = BACKGROUND_MUSIC_PROMPT_PRESETS.find(
|
||
(candidate) => candidate.label === label,
|
||
);
|
||
if (!preset) {
|
||
throw new Error(`missing background music preset: ${label}`);
|
||
}
|
||
return preset;
|
||
}
|
||
|
||
const explorationPreset = presetByLabel('探索冒险');
|
||
const healingPreset = presetByLabel('温暖治愈');
|
||
|
||
describe('ImageCanvasBackgroundMusicPresetModel', () => {
|
||
it('keeps the authoritative three groups at 12 / 3 / 15 and 30 total', () => {
|
||
const groupCounts = BACKGROUND_MUSIC_PROMPT_PRESETS.reduce<
|
||
Record<string, number>
|
||
>(
|
||
(counts, preset) => ({
|
||
...counts,
|
||
[preset.group]: (counts[preset.group] ?? 0) + 1,
|
||
}),
|
||
{},
|
||
);
|
||
|
||
expect(BACKGROUND_MUSIC_PROMPT_PRESETS).toHaveLength(30);
|
||
expect(groupCounts).toEqual({ purpose: 12, atmosphere: 3, scene: 15 });
|
||
});
|
||
|
||
it('copies every authoritative label and prompt without rewriting them', () => {
|
||
expect(
|
||
BACKGROUND_MUSIC_PROMPT_PRESETS.map((preset) => [
|
||
preset.group,
|
||
preset.label,
|
||
preset.prompt,
|
||
]),
|
||
).toEqual(AUTHORITATIVE_PRESETS);
|
||
});
|
||
|
||
it('exposes stable unique ids that never appear in the visible prompt', () => {
|
||
const ids = BACKGROUND_MUSIC_PROMPT_PRESETS.map((preset) => preset.id);
|
||
|
||
expect(new Set(ids).size).toBe(ids.length);
|
||
for (const preset of BACKGROUND_MUSIC_PROMPT_PRESETS) {
|
||
expect(preset.id).toMatch(/^[a-z][a-z0-9-]*$/u);
|
||
expect(preset.prompt).not.toContain(preset.id);
|
||
expect(preset.prompt).not.toContain(preset.group);
|
||
}
|
||
});
|
||
|
||
it('writes the preset text directly into an empty or all-whitespace prompt', () => {
|
||
// 不可见 code point 一律用显式转义:字面量在编辑器和 lint autofix 里容易被吃掉。
|
||
for (const currentPrompt of ['', ' ', '
', ' \t\n\r\n
']) {
|
||
expect(
|
||
appendBackgroundMusicPromptPreset(currentPrompt, explorationPreset),
|
||
).toBe(explorationPreset.prompt);
|
||
}
|
||
});
|
||
|
||
it('appends directly after Chinese, ASCII and other Unicode punctuation', () => {
|
||
for (const ending of [
|
||
'。',
|
||
',',
|
||
'、',
|
||
'!',
|
||
'?',
|
||
':',
|
||
';',
|
||
'”',
|
||
')',
|
||
'.',
|
||
',',
|
||
'!',
|
||
'?',
|
||
';',
|
||
':',
|
||
'-',
|
||
'_',
|
||
'(',
|
||
')',
|
||
'"',
|
||
"'",
|
||
'…',
|
||
'—',
|
||
'「',
|
||
'」',
|
||
'·',
|
||
]) {
|
||
const currentPrompt = `阳光动物园${ending}`;
|
||
|
||
expect(
|
||
appendBackgroundMusicPromptPreset(currentPrompt, explorationPreset),
|
||
).toBe(`${currentPrompt}${explorationPreset.prompt}`);
|
||
}
|
||
});
|
||
|
||
it('inserts a Chinese period after non-punctuation, symbols, emoji and combining marks', () => {
|
||
// `+` `=` `$` 属于 Unicode 符号而不是标点,必须补句号;emoji 与组合字符
|
||
// 用于证明末位是按 code point 而不是 UTF-16 code unit 读取的。
|
||
for (const ending of [
|
||
'园',
|
||
'a',
|
||
'7',
|
||
'+',
|
||
'=',
|
||
'$',
|
||
'\u{1f3b5}',
|
||
'\u{1f469}\u{1f3a4}',
|
||
'á',
|
||
]) {
|
||
const currentPrompt = `阳光动物园${ending}`;
|
||
|
||
expect(
|
||
appendBackgroundMusicPromptPreset(currentPrompt, explorationPreset),
|
||
).toBe(`${currentPrompt}。${explorationPreset.prompt}`);
|
||
}
|
||
});
|
||
|
||
it('removes only boundary whitespace and keeps internal CR / LF / CRLF unchanged', () => {
|
||
const currentPrompt = ' 阳光动物园\n第二行\r\n第三行\r第四行 ';
|
||
|
||
expect(
|
||
appendBackgroundMusicPromptPreset(currentPrompt, explorationPreset),
|
||
).toBe(`阳光动物园\n第二行\r\n第三行\r第四行。${explorationPreset.prompt}`);
|
||
});
|
||
|
||
it('keeps boundary zero-width code points because they are not Unicode White_Space', () => {
|
||
const currentPrompt = '阳光动物园';
|
||
|
||
expect(
|
||
appendBackgroundMusicPromptPreset(currentPrompt, explorationPreset),
|
||
).toBe(`${currentPrompt}。${explorationPreset.prompt}`);
|
||
});
|
||
|
||
it('appends again on every repeated click of the same preset', () => {
|
||
const first = appendBackgroundMusicPromptPreset('', explorationPreset);
|
||
const second = appendBackgroundMusicPromptPreset(first, explorationPreset);
|
||
const third = appendBackgroundMusicPromptPreset(second, explorationPreset);
|
||
|
||
expect(second).toBe(`${first}。${explorationPreset.prompt}`);
|
||
expect(third).toBe(`${second}。${explorationPreset.prompt}`);
|
||
});
|
||
|
||
it('merges different presets in click order without deduplicating', () => {
|
||
const withExploration = appendBackgroundMusicPromptPreset(
|
||
'阳光动物园',
|
||
explorationPreset,
|
||
);
|
||
|
||
expect(
|
||
appendBackgroundMusicPromptPreset(withExploration, healingPreset),
|
||
).toBe(`阳光动物园。${explorationPreset.prompt}。${healingPreset.prompt}`);
|
||
});
|
||
|
||
it('keeps the whole text after crossing the 200 and 2000 code point limits', () => {
|
||
for (const currentLength of [200, 2001]) {
|
||
const currentPrompt = 'A'.repeat(currentLength);
|
||
const appended = appendBackgroundMusicPromptPreset(
|
||
currentPrompt,
|
||
explorationPreset,
|
||
);
|
||
|
||
expect(appended.startsWith(currentPrompt)).toBe(true);
|
||
expect(appended).toBe(`${currentPrompt}。${explorationPreset.prompt}`);
|
||
expect(Array.from(appended)).toHaveLength(
|
||
currentLength + 1 + Array.from(explorationPreset.prompt).length,
|
||
);
|
||
}
|
||
});
|
||
|
||
it('returns only the visible prompt, never the preset id or group metadata', () => {
|
||
const appended = appendBackgroundMusicPromptPreset(
|
||
'阳光动物园',
|
||
explorationPreset,
|
||
);
|
||
|
||
// chip 上的 label 可能本来就是预设正文的一部分,所以只断言结果逐字等于
|
||
// 「canonical + 分隔符 + preset.prompt」,并禁止隐藏元数据泄漏。
|
||
expect(typeof appended).toBe('string');
|
||
expect(appended).toBe(`阳光动物园。${explorationPreset.prompt}`);
|
||
expect(appended).not.toContain(explorationPreset.id);
|
||
expect(appended).not.toContain(explorationPreset.group);
|
||
});
|
||
});
|