feat: 配置中心-提示词与词表(四件套编辑/结构化词表/试金石面板/保存前验证)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-13 20:05:42 +08:00
parent cac91e9e3e
commit 39d72c53aa
8 changed files with 2229 additions and 10 deletions
@@ -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([])
})
})