feat: 配置中心-提示词与词表(四件套编辑/结构化词表/试金石面板/保存前验证)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,359 @@
|
||||
/**
|
||||
* 配置中心提示词/词表编辑模型单元测试(Phase 3 Task 7b)
|
||||
*
|
||||
* 钉住 src/views/config/center/model.ts 的纯逻辑契约(事实源:
|
||||
* app/platform/config_validation.py 的 PROMPTS_SIZE_CAPS 与
|
||||
* app/engine/wall_schema.py 的结构/双形态正则条目约定):
|
||||
* - 提示词:UTF-8 字节口径(中文 3 字节/字)、50/100KB 上限判定、
|
||||
* 原样快照 → 表单值的防御归一;
|
||||
* - 词表:正则条目双形态(纯字符串 | {pattern, flags, comment})的
|
||||
* 解析/序列化往返、空行剔除、语言池清洗、initWallEdit 对透传键的
|
||||
* 保留、buildWallPayload 的整组重组;
|
||||
* - 变体键镜像:reminders 六类 / cc_templates 四组与后端常量同构。
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
CC_TEMPLATE_VARIANTS,
|
||||
DEFAULT_TOUCHSTONE_SENTENCES,
|
||||
NO_CONTACT_KEYS,
|
||||
POOL_FIELDS,
|
||||
PROMPTS_CAPS,
|
||||
PROMPTS_KEYS,
|
||||
REMINDER_VARIANTS,
|
||||
TRIGGER_KEYS,
|
||||
ZERO_NUMBER_KEYS,
|
||||
buildWallPayload,
|
||||
formatKB,
|
||||
initWallEdit,
|
||||
normalizePromptsGroup,
|
||||
parsePatternEntry,
|
||||
promptByteInfo,
|
||||
promptsOverCap,
|
||||
rowToEntry,
|
||||
rowsToEntries,
|
||||
rowToSingleEntry,
|
||||
touchstoneSentences,
|
||||
utf8Bytes,
|
||||
type PromptsDraft
|
||||
} from '../model'
|
||||
|
||||
// ---- 提示词四件套 -------------------------------------------------------------------
|
||||
|
||||
describe('utf8Bytes / formatKB(UTF-8 字节口径)', () => {
|
||||
it('ASCII 1 字节/字符、中文 3 字节/字、空串为 0', () => {
|
||||
expect(utf8Bytes('')).toBe(0)
|
||||
expect(utf8Bytes('abc')).toBe(3)
|
||||
expect(utf8Bytes('中文')).toBe(6) // 后端 len('中文'.encode('utf-8')) == 6
|
||||
expect(utf8Bytes('价格500')).toBe(3 * 2 + 3)
|
||||
})
|
||||
|
||||
it('formatKB:KB 一位小数', () => {
|
||||
expect(formatKB(0)).toBe('0.0KB')
|
||||
expect(formatKB(50 * 1024)).toBe('50.0KB')
|
||||
expect(formatKB(1536)).toBe('1.5KB')
|
||||
})
|
||||
})
|
||||
|
||||
describe('提示词字节上限(config_validation.PROMPTS_SIZE_CAPS 镜像)', () => {
|
||||
it('company_facts 50KB、其余 100KB', () => {
|
||||
expect(PROMPTS_CAPS.company_facts).toBe(50 * 1024)
|
||||
for (const key of PROMPTS_KEYS) {
|
||||
if (key !== 'company_facts') expect(PROMPTS_CAPS[key]).toBe(100 * 1024)
|
||||
}
|
||||
})
|
||||
|
||||
it('边界:恰好等于上限不算超限,超 1 字节算', () => {
|
||||
expect(promptByteInfo('company_facts', '').over).toBe(false)
|
||||
expect(promptByteInfo('company_facts', '中'.repeat(50 * 1024 / 3)).over).toBe(false)
|
||||
expect(promptByteInfo('company_facts', `a`.repeat(50 * 1024 + 1)).over).toBe(true)
|
||||
expect(promptByteInfo('system', 'a'.repeat(100 * 1024)).over).toBe(false)
|
||||
expect(promptByteInfo('evaluator', 'a'.repeat(100 * 1024 + 1)).over).toBe(true)
|
||||
})
|
||||
|
||||
it('promptsOverCap:任一件超限即 true(禁存判定)', () => {
|
||||
const draft: PromptsDraft = {
|
||||
system: '',
|
||||
company_facts: 'a'.repeat(50 * 1024 + 1),
|
||||
extractor: '',
|
||||
evaluator: ''
|
||||
}
|
||||
expect(promptsOverCap(draft)).toBe(true)
|
||||
expect(promptsOverCap(normalizePromptsGroup({}))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizePromptsGroup(原样快照 → 表单值)', () => {
|
||||
it('缺键/非字符串补空串、字符串原样保留', () => {
|
||||
expect(
|
||||
normalizePromptsGroup({ system: 'S', company_facts: 123, extractor: null })
|
||||
).toEqual({ system: 'S', company_facts: '', extractor: '', evaluator: '' })
|
||||
expect(normalizePromptsGroup(null)).toEqual({
|
||||
system: '',
|
||||
company_facts: '',
|
||||
extractor: '',
|
||||
evaluator: ''
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ---- 正则条目双形态 -----------------------------------------------------------------
|
||||
|
||||
describe('parsePatternEntry(字符串/dict 双形态 → 编辑行)', () => {
|
||||
it('纯字符串条目:无 flags 无备注', () => {
|
||||
expect(parsePatternEntry('\\$\\s?\\d')).toEqual({
|
||||
pattern: '\\$\\s?\\d',
|
||||
flags: '',
|
||||
comment: '',
|
||||
showComment: false
|
||||
})
|
||||
})
|
||||
|
||||
it('dict 条目:pattern/flags/comment 各自归位', () => {
|
||||
expect(
|
||||
parsePatternEntry({ pattern: '\\d+', flags: 'i', comment: '# 时间承诺' })
|
||||
).toEqual({
|
||||
pattern: '\\d+',
|
||||
flags: 'i',
|
||||
comment: '# 时间承诺',
|
||||
showComment: false
|
||||
})
|
||||
})
|
||||
|
||||
it('dict 条目无 comment 键(wall.json 真实形态)与异常值防御', () => {
|
||||
expect(parsePatternEntry({ pattern: 'warranty', flags: 'i' }).comment).toBe('')
|
||||
expect(parsePatternEntry({ pattern: 'x' }).flags).toBe('')
|
||||
// 异常值:数字/null/缺 pattern → 归空行(存量脏数据防御)
|
||||
expect(parsePatternEntry(42).pattern).toBe('')
|
||||
expect(parsePatternEntry(null).pattern).toBe('')
|
||||
expect(parsePatternEntry({ flags: 'i' }).pattern).toBe('')
|
||||
expect(parsePatternEntry({ pattern: 1, flags: 'x', comment: 2 })).toEqual({
|
||||
pattern: '',
|
||||
flags: '',
|
||||
comment: '',
|
||||
showComment: false
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('rowToEntry / rowsToEntries(编辑行 → 条目序列化)', () => {
|
||||
it('无 flags 无备注 → 纯字符串(与 wall.json 极简条目一致)', () => {
|
||||
expect(
|
||||
rowToEntry({ pattern: '\\$\\s?\\d', flags: '', comment: '', showComment: false })
|
||||
).toBe('\\$\\s?\\d')
|
||||
})
|
||||
|
||||
it('有 flags/备注 → dict 且只带非空键(comment-only 条目不带 flags 键)', () => {
|
||||
expect(
|
||||
rowToEntry({ pattern: '\\d+', flags: 'i', comment: '', showComment: false })
|
||||
).toEqual({ pattern: '\\d+', flags: 'i' })
|
||||
expect(
|
||||
rowToEntry({ pattern: 'x', flags: '', comment: '# 说明', showComment: false })
|
||||
).toEqual({ pattern: 'x', comment: '# 说明' })
|
||||
expect(
|
||||
rowToEntry({ pattern: 'x', flags: 'i', comment: '# 说明', showComment: false })
|
||||
).toEqual({ pattern: 'x', flags: 'i', comment: '# 说明' })
|
||||
})
|
||||
|
||||
it('空 pattern 行剔除、纯空白备注视为无备注', () => {
|
||||
expect(
|
||||
rowToEntry({ pattern: '', flags: 'i', comment: 'x', showComment: false })
|
||||
).toBeNull()
|
||||
expect(
|
||||
rowsToEntries([
|
||||
{ pattern: 'a', flags: '', comment: ' ', showComment: false },
|
||||
{ pattern: '', flags: '', comment: '', showComment: false },
|
||||
{ pattern: 'b', flags: 'i', comment: '', showComment: false }
|
||||
])
|
||||
).toEqual(['a', { pattern: 'b', flags: 'i' }])
|
||||
})
|
||||
|
||||
it('rowToSingleEntry(triggers/zero_number):无标志非空 → 纯字符串;空 pattern → dict 显性失败', () => {
|
||||
expect(
|
||||
rowToSingleEntry({ pattern: 'price', flags: 'i', comment: '', showComment: false })
|
||||
).toEqual({ pattern: 'price', flags: 'i' })
|
||||
// wall.json zero_number 出厂是纯字符串——无标志无备注保持字符串(往返无损)
|
||||
expect(
|
||||
rowToSingleEntry({ pattern: 'budget|预算', flags: '', comment: '', showComment: false })
|
||||
).toBe('budget|预算')
|
||||
// 空 pattern 绝不能出字符串形态('' 会编成匹配一切的正则)——dict 让后端报路径
|
||||
expect(
|
||||
rowToSingleEntry({ pattern: '', flags: '', comment: '', showComment: false })
|
||||
).toEqual({ pattern: '' })
|
||||
})
|
||||
})
|
||||
|
||||
// ---- 词表整体模型 -------------------------------------------------------------------
|
||||
|
||||
/** 最小但结构完整的 wall 快照(对照 seed_profiles/jiancai/wall.json 形态) */
|
||||
function miniWall(): Record<string, unknown> {
|
||||
return {
|
||||
price_patterns: [
|
||||
'\\$\\s?\\d',
|
||||
{ pattern: '\\d+\\s*天', comment: '# 中文时限' },
|
||||
{ pattern: 'warranty', flags: 'i' }
|
||||
],
|
||||
deflections: { en: ['A?'], zh: ['B?'], es: ['C?'] },
|
||||
deflections_contact: { en: ['A.'], zh: ['B.'] },
|
||||
pivots: { en: ['P'], zh: ['p'] },
|
||||
pivots_contact: { en: ['Q'], zh: ['q'] },
|
||||
no_contact: { web: 'W', app: 'A', already: 'D' },
|
||||
zero_number: {
|
||||
long_number: '\\+?\\d[\\d\\s\\-().]{8,17}\\d',
|
||||
any_email: '[a-z]+@[a-z]+\\.[a-z]{2,}'
|
||||
},
|
||||
reask_patterns: [{ pattern: 'your\\s+whatsapp', flags: 'i' }],
|
||||
triggers: {
|
||||
boundary: { pattern: 'price', flags: 'i' },
|
||||
budget: { pattern: 'budget|预算', flags: 'i' },
|
||||
contact_ask: { pattern: 'your\\s+number', flags: 'i' }
|
||||
},
|
||||
reminders: {
|
||||
boundary: { budget: 'r-b', normal: 'r-n' },
|
||||
contact_ask: { app_already: 'ca-aa', app: 'ca-a', web: 'ca-w' },
|
||||
language_follow: { zh: 'lf-zh', es: 'lf-es' },
|
||||
lead_given: {
|
||||
forbidden: 'lg-f',
|
||||
respond_script: 'lg-r',
|
||||
boundary_tail: 'lg-b',
|
||||
suspect: 'lg-s'
|
||||
},
|
||||
cc_guide: { echo: 'cg-e', ask: 'cg-a' },
|
||||
email_light: { en: 'el-en', zh: 'el-zh' }
|
||||
},
|
||||
cc_templates: {
|
||||
ask: { en: 't-a-en', zh: 't-a-zh', es: 't-a-es' },
|
||||
wall: { en: 't-w-en', zh: 't-w-zh', es: 't-w-es' },
|
||||
note: { en: 't-n-en', zh: 't-n-zh' },
|
||||
echo: { en: 't-e-en', zh: 't-e-zh', es: 't-e-es' }
|
||||
},
|
||||
_meta: { source: 'extract_config.py' }
|
||||
}
|
||||
}
|
||||
|
||||
describe('initWallEdit(快照 → 编辑模型)', () => {
|
||||
it('缺失/空对象/非对象 → null(面板空态)', () => {
|
||||
expect(initWallEdit(null)).toBeNull()
|
||||
expect(initWallEdit(undefined)).toBeNull()
|
||||
expect(initWallEdit({})).toBeNull()
|
||||
expect(initWallEdit('x' as unknown as Record<string, unknown>)).toBeNull()
|
||||
})
|
||||
|
||||
it('双形态正则解析 + 透传键保留 + 六类/四组变体补齐', () => {
|
||||
const model = initWallEdit(miniWall())!
|
||||
expect(model.priceRows).toHaveLength(3)
|
||||
expect(model.priceRows[0]).toMatchObject({ pattern: '\\$\\s?\\d', flags: '' })
|
||||
expect(model.priceRows[1]).toMatchObject({
|
||||
pattern: '\\d+\\s*天',
|
||||
flags: '',
|
||||
comment: '# 中文时限'
|
||||
})
|
||||
expect(model.priceRows[2]).toMatchObject({ pattern: 'warranty', flags: 'i' })
|
||||
expect(model.reaskRows).toHaveLength(1)
|
||||
expect(model.singles.boundary).toMatchObject({ pattern: 'price', flags: 'i' })
|
||||
expect(model.singles.long_number.pattern).toBe('\\+?\\d[\\d\\s\\-().]{8,17}\\d')
|
||||
expect(model.pools.deflections).toEqual({ en: ['A?'], zh: ['B?'], es: ['C?'] })
|
||||
expect(model.noContact).toEqual({ web: 'W', app: 'A', already: 'D' })
|
||||
expect(model.reminders.boundary.budget).toBe('r-b')
|
||||
expect(model.ccTemplates.note).toEqual({ en: 't-n-en', zh: 't-n-zh' })
|
||||
// 未接管键(含 "_" 前缀元信息)原样透传
|
||||
expect(model.passthrough).toEqual({ _meta: { source: 'extract_config.py' } })
|
||||
})
|
||||
|
||||
it('脏数据防御:非字符串池项剔除、非对象区补空、非列表正则区为空', () => {
|
||||
const model = initWallEdit({
|
||||
price_patterns: 'oops',
|
||||
deflections: { en: ['ok', 42, null], zh: 'oops', es: [] },
|
||||
triggers: 'oops',
|
||||
no_contact: 'oops',
|
||||
reminders: 'oops',
|
||||
cc_templates: 'oops'
|
||||
})!
|
||||
expect(model.priceRows).toEqual([])
|
||||
expect(model.pools.deflections).toEqual({ en: ['ok'] })
|
||||
expect(model.singles.boundary.pattern).toBe('')
|
||||
expect(model.noContact.web).toBe('')
|
||||
expect(model.reminders.boundary.budget).toBe('')
|
||||
expect(model.ccTemplates.ask.en).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildWallPayload(编辑模型 → PUT config/wall 整组载荷)', () => {
|
||||
it('结构化区重组 + 透传键合入 + 序列化规则生效', () => {
|
||||
const model = initWallEdit(miniWall())!
|
||||
// 编辑:价格墙加一条(rich 形态)+ 留一条空行(应被剔除);
|
||||
// deflections.en 加一条空句(应被剔除);en 池清空句子(整键应被丢弃)
|
||||
model.priceRows.push({ pattern: '\\d+k\\b', flags: 'i', comment: '', showComment: false })
|
||||
model.priceRows.push({ pattern: '', flags: '', comment: '', showComment: false })
|
||||
model.pools.deflections.en.push('')
|
||||
model.pools.deflections_contact.en = ['']
|
||||
|
||||
const payload = buildWallPayload(model)
|
||||
|
||||
expect(payload.price_patterns).toEqual([
|
||||
'\\$\\s?\\d',
|
||||
{ pattern: '\\d+\\s*天', comment: '# 中文时限' },
|
||||
{ pattern: 'warranty', flags: 'i' },
|
||||
{ pattern: '\\d+k\\b', flags: 'i' }
|
||||
])
|
||||
expect(payload.reask_patterns).toEqual([{ pattern: 'your\\s+whatsapp', flags: 'i' }])
|
||||
expect(payload.triggers).toEqual({
|
||||
boundary: { pattern: 'price', flags: 'i' },
|
||||
budget: { pattern: 'budget|预算', flags: 'i' },
|
||||
contact_ask: { pattern: 'your\\s+number', flags: 'i' }
|
||||
})
|
||||
expect(payload.zero_number).toEqual({
|
||||
long_number: '\\+?\\d[\\d\\s\\-().]{8,17}\\d',
|
||||
any_email: '[a-z]+@[a-z]+\\.[a-z]{2,}'
|
||||
})
|
||||
expect(payload.deflections).toEqual({ en: ['A?'], zh: ['B?'], es: ['C?'] })
|
||||
// deflections_contact.en 清空后整键丢弃(zh 保留)
|
||||
expect(payload.deflections_contact).toEqual({ zh: ['B.'] })
|
||||
expect(payload.no_contact).toEqual({ web: 'W', app: 'A', already: 'D' })
|
||||
expect(payload.reminders).toEqual(miniWall().reminders)
|
||||
expect(payload.cc_templates).toEqual(miniWall().cc_templates)
|
||||
expect(payload._meta).toEqual({ source: 'extract_config.py' })
|
||||
})
|
||||
|
||||
it('initWallEdit → buildWallPayload 无编辑时结构等价往返(序列化形态按约定归一)', () => {
|
||||
const source = miniWall()
|
||||
const payload = buildWallPayload(initWallEdit(source)!)
|
||||
expect(payload.price_patterns).toEqual(source.price_patterns)
|
||||
expect(payload.triggers).toEqual(source.triggers)
|
||||
expect(payload.pivots).toEqual(source.pivots)
|
||||
expect(payload.reminders).toEqual(source.reminders)
|
||||
expect(payload.cc_templates).toEqual(source.cc_templates)
|
||||
expect(payload._meta).toEqual(source._meta)
|
||||
})
|
||||
})
|
||||
|
||||
// ---- 变体键镜像与试金石 -------------------------------------------------------------
|
||||
|
||||
describe('变体键镜像(wall_schema 常量同构)', () => {
|
||||
it('reminders 六类 / cc_templates 四组 / 池与单正则键清单', () => {
|
||||
expect(Object.keys(REMINDER_VARIANTS).sort()).toEqual(
|
||||
['boundary', 'cc_guide', 'contact_ask', 'email_light', 'language_follow', 'lead_given']
|
||||
)
|
||||
expect(Object.keys(CC_TEMPLATE_VARIANTS).sort()).toEqual(['ask', 'echo', 'note', 'wall'])
|
||||
expect(POOL_FIELDS).toEqual([
|
||||
'deflections',
|
||||
'deflections_contact',
|
||||
'pivots',
|
||||
'pivots_contact'
|
||||
])
|
||||
expect([...TRIGGER_KEYS]).toEqual(['boundary', 'budget', 'contact_ask'])
|
||||
expect([...ZERO_NUMBER_KEYS]).toEqual(['long_number', 'any_email'])
|
||||
expect([...NO_CONTACT_KEYS]).toEqual(['web', 'app', 'already'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('试金石句集', () => {
|
||||
it('默认 3 示例句(覆盖 blocked/replaced/pass 三态)且非空', () => {
|
||||
expect(DEFAULT_TOUCHSTONE_SENTENCES).toHaveLength(3)
|
||||
for (const s of DEFAULT_TOUCHSTONE_SENTENCES) expect(s.trim()).not.toBe('')
|
||||
})
|
||||
|
||||
it('touchstoneSentences:trim + 空行过滤', () => {
|
||||
expect(touchstoneSentences([' a ', '', ' ', 'b'])).toEqual(['a', 'b'])
|
||||
expect(touchstoneSentences([])).toEqual([])
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user