背景:A4 需要字典三层合并、配置版本与命名规则的共享契约。\n改动:新增 dict/config 契约、system-configs 定义与校验、命名模板解析;新增 dict_types/dict_items/system_config_versions 迁移与 13 类/96 条字典、26 项配置种子。\n影响面:shared 新增导出;prisma 新增表与迁移;客户基础字段 DTO 硬上界调整为 50(实际生效上限仍由配置动态校验)。\n回滚:回滚本提交并保留新增表(不删数据)。\n验证:shared typecheck/test 147;seed 幂等;prisma migrate dev 已应用。
112 lines
3.6 KiB
TypeScript
112 lines
3.6 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
|
||
import {
|
||
dedupeName,
|
||
formatNamingDate,
|
||
formatNamingSequence,
|
||
resolveNamingTemplate,
|
||
sanitizeFilename,
|
||
validateNamingTemplate,
|
||
} from './naming.js';
|
||
|
||
describe('sanitizeFilename', () => {
|
||
it('替换路径分隔符与非法字符,保留中文', () => {
|
||
expect(sanitizeFilename('素材/标签:01*?.png')).toBe('素材_标签_01__.png');
|
||
});
|
||
|
||
it('去除首尾空格与点,空名回退', () => {
|
||
expect(sanitizeFilename(' ..脚本草稿 .. ')).toBe('脚本草稿');
|
||
expect(sanitizeFilename(' ')).toBe('未命名');
|
||
expect(sanitizeFilename('***', { fallback: 'file' })).toBe('file');
|
||
});
|
||
|
||
it('Windows 保留名追加下划线', () => {
|
||
expect(sanitizeFilename('CON')).toBe('CON_');
|
||
expect(sanitizeFilename('lpt1')).toBe('lpt1_');
|
||
});
|
||
|
||
it('超长时保留扩展名', () => {
|
||
const name = sanitizeFilename(`${'a'.repeat(200)}.png`, { maxLength: 20 });
|
||
expect(name).toHaveLength(20);
|
||
expect(name.endsWith('.png')).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe('dedupeName(R-01 同名自动加序号)', () => {
|
||
it('不冲突时原样返回', () => {
|
||
expect(dedupeName('脚本-01', new Set())).toBe('脚本-01');
|
||
});
|
||
|
||
it('冲突时追加两位序号', () => {
|
||
const existing = new Set(['素材.png', '素材-02.png']);
|
||
expect(dedupeName('素材.png', existing)).toBe('素材-03.png');
|
||
});
|
||
|
||
it('带序号的名字基于原始基名递增', () => {
|
||
const existing = new Set(['素材-02.png', '素材-03.png']);
|
||
expect(dedupeName('素材-03.png', existing)).toBe('素材-04.png');
|
||
});
|
||
|
||
it('序号耗尽时抛错', () => {
|
||
const existing = new Set(['a', ...Array.from({ length: 3 }, (_, i) => `a-0${String(i + 2)}`)]);
|
||
expect(() => dedupeName('a', existing, 3)).toThrow(RangeError);
|
||
});
|
||
});
|
||
|
||
describe('命名模板(A4-BE-03 / R-01)', () => {
|
||
const context = {
|
||
industry: '电商',
|
||
customer: '花间集',
|
||
direction: '口播',
|
||
date: '20261104',
|
||
sequence: 1,
|
||
spec: '1080x1920',
|
||
};
|
||
|
||
it('默认脚本模板解析为「行业-客户-方向-日期-序号」', () => {
|
||
expect(resolveNamingTemplate('{行业}-{客户}-{脚本方向}-{日期}-{序号}', context)).toBe(
|
||
'电商-花间集-口播-20261104-1',
|
||
);
|
||
});
|
||
|
||
it('序号可先补零后传入', () => {
|
||
expect(
|
||
resolveNamingTemplate('{行业}-{客户}-{日期}-{序号}', {
|
||
...context,
|
||
sequence: formatNamingSequence(context.sequence),
|
||
}),
|
||
).toBe('电商-花间集-20261104-01');
|
||
});
|
||
|
||
it('兼容英文 key 与规格后缀', () => {
|
||
expect(resolveNamingTemplate('{industry}-{customer}-{date}-{seq}-{spec}', context)).toBe(
|
||
'电商-花间集-20261104-1-1080x1920',
|
||
);
|
||
});
|
||
|
||
it('未知占位符 / 非法字符 / 空模板被拒绝', () => {
|
||
expect(validateNamingTemplate('{行业}-{未知}').valid).toBe(false);
|
||
expect(validateNamingTemplate('{行业}*{客户}').valid).toBe(false);
|
||
expect(validateNamingTemplate(' ').valid).toBe(false);
|
||
});
|
||
|
||
it('缺失占位符值时抛错', () => {
|
||
expect(() => resolveNamingTemplate('{行业}-{客户}', { ...context, customer: '' })).toThrow(
|
||
'命名占位符缺少值',
|
||
);
|
||
});
|
||
|
||
it('超过最大长度抛错', () => {
|
||
expect(() =>
|
||
resolveNamingTemplate('{客户}', { ...context, customer: '客'.repeat(101) }),
|
||
).toThrow('不能超过 100 个字符');
|
||
});
|
||
|
||
it('日期与序号格式化', () => {
|
||
expect(formatNamingDate(new Date(2026, 10, 4))).toBe('20261104');
|
||
expect(formatNamingSequence(7)).toBe('07');
|
||
expect(formatNamingSequence(123)).toBe('123');
|
||
expect(() => formatNamingSequence(-1)).toThrow();
|
||
});
|
||
});
|