/** * 配置中心提示词/词表/凭据编辑模型单元测试(Phase 3 Task 7b + 7c) * * 钉住 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 四组与后端常量同构; * - 凭据三组(Task 7c):normalize(掩码哨兵态/数值转串/缺键防御)、 * build(密钥哨兵往返——不动保存回 '__SET__' 保持库值;空串 = 清空; * 新值优先;数值空串 → null)与 validate 前端预检(端口 1-65535 / * 邮箱含 @ / push URL 协议与正数·1-10 整数边界,与后端校验同款)。 */ import { describe, expect, it } from 'vitest' import { CC_TEMPLATE_VARIANTS, DEFAULT_TOUCHSTONE_SENTENCES, NO_CONTACT_KEYS, POOL_FIELDS, PROMPTS_CAPS, PROMPTS_KEYS, PUSH_PROBE_REASON_LABELS, REMINDER_VARIANTS, TRIGGER_KEYS, ZERO_NUMBER_KEYS, asSecretMasked, buildAiCredsPayload, buildEmailCredsPayload, buildPushCredsPayload, buildWallPayload, formatKB, initWallEdit, normalizeAiCredsGroup, normalizeEmailCredsGroup, normalizePromptsGroup, normalizePushCredsGroup, parsePatternEntry, promptByteInfo, promptsOverCap, rowToEntry, rowsToEntries, rowToSingleEntry, touchstoneSentences, utf8Bytes, validateEmailCredsForm, validatePushCredsForm, type EmailCredsForm, type PromptsDraft, type PushCredsForm } 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 { 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)).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([]) }) }) // ---- 凭据三组(email / push / ai,Phase 3 Task 7c) ----------------------------------- describe('asSecretMasked(GET 密钥值 → 掩码哨兵态)', () => { it('哨兵原样 / 空串与 null·undefined → 空串(区分「配了」与「没配」)', () => { expect(asSecretMasked('__SET__')).toBe('__SET__') expect(asSecretMasked('')).toBe('') expect(asSecretMasked(null)).toBe('') expect(asSecretMasked(undefined)).toBe('') }) it('掩码失效漏出的任意非空值也归哨兵(防原始密钥回传库外)', () => { expect(asSecretMasked('sk-real-secret')).toBe('__SET__') expect(asSecretMasked(123)).toBe('') }) }) describe('normalize 三组(GET 原样组 → 表单 + 密钥态)', () => { it('email:smtp 完整组 → 表单值 + 密码哨兵;密钥输入恒空', () => { const state = normalizeEmailCredsGroup({ smtp: { host: 'smtp.x.com', port: 465, user: 'bot@x.com', password: '__SET__' }, sender_name: 'HESUPIPE', notify_emails: ['a@x.com', 'b@x.com'] }) expect(state.form).toEqual({ smtpHost: 'smtp.x.com', smtpPort: '465', smtpUser: 'bot@x.com', smtpPassword: '', senderName: 'HESUPIPE', notifyEmails: ['a@x.com', 'b@x.com'] }) expect(state.passwordMasked).toBe('__SET__') }) it('email:jiancai 档案形态(无 smtp 键)→ 全空槽 + 未设置密钥态', () => { const state = normalizeEmailCredsGroup({ sender_name: 'HESUPIPE', notify_emails: ['hesu@hesuu.com'] }) expect(state.form.smtpHost).toBe('') expect(state.form.smtpPort).toBe('') expect(state.form.smtpPassword).toBe('') expect(state.passwordMasked).toBe('') }) it('email:smtp null / 类型漂移防御', () => { const state = normalizeEmailCredsGroup({ smtp: null, notify_emails: [1, 'a@x.com'] }) expect(state.form.smtpHost).toBe('') expect(state.form.notifyEmails).toEqual(['a@x.com']) expect(normalizeEmailCredsGroup(undefined).form.notifyEmails).toEqual([]) }) it('push:完整组 → 表单(数值转串)+ api_key 哨兵', () => { const state = normalizePushCredsGroup({ enabled: true, url: 'https://push.example/v1', api_key: '__SET__', prefix: 'ai-kefu-jc', timeout: 5.5, max_attempts: 3, retry_backoff_base: 1 }) expect(state.form).toEqual({ enabled: true, url: 'https://push.example/v1', apiKey: '', prefix: 'ai-kefu-jc', timeout: '5.5', maxAttempts: '3', backoffBase: '1' }) expect(state.apiKeyMasked).toBe('__SET__') }) it('push:缺省/未启用 → enabled false + 数值空串(null → 空槽)', () => { const state = normalizePushCredsGroup({ prefix: 'ai-kefu-jc' }) expect(state.form.enabled).toBe(false) expect(state.form.timeout).toBe('') expect(state.form.maxAttempts).toBe('') expect(state.form.backoffBase).toBe('') expect(state.form.apiKey).toBe('') expect(state.apiKeyMasked).toBe('') }) it('ai:缺省组 → 空表单(出厂默认由后端深合并兜底)+ 未设置密钥态', () => { const state = normalizeAiCredsGroup(null) expect(state.form).toEqual({ provider: '', apiKey: '', baseUrl: '', model: '' }) expect(state.apiKeyMasked).toBe('') const set = normalizeAiCredsGroup({ provider: 'glm', api_key: '__SET__', model: 'glm-4-flash' }) expect(set.form.provider).toBe('glm') expect(set.form.apiKey).toBe('') expect(set.apiKeyMasked).toBe('__SET__') }) }) describe('build 三组载荷(表单 → PUT 整组)+ 密钥哨兵往返', () => { it('email:普通字段 trim、空行剔除、端口转整数', () => { const payload = buildEmailCredsPayload( { smtpHost: ' smtp.x.com ', smtpPort: ' 465 ', smtpUser: ' bot@x.com ', smtpPassword: '', senderName: ' HESUPIPE ', notifyEmails: [' a@x.com ', '', ' '] }, '' ) expect(payload).toEqual({ smtp: { host: 'smtp.x.com', port: 465, user: 'bot@x.com', password: '' }, sender_name: 'HESUPIPE', notify_emails: ['a@x.com'] }) }) it('email 密钥三态:未设置不动 → 空串;已设置不动 → 哨兵;输入新值 → 新值', () => { const form = (): EmailCredsForm => ({ smtpHost: 'smtp.x.com', smtpPort: '465', smtpUser: 'bot@x.com', smtpPassword: '', senderName: '', notifyEmails: [] }) // 已设置 + 未动 → 哨兵(后端还原库值,库值不变) expect(buildEmailCredsPayload(form(), '__SET__').smtp).toMatchObject({ password: '__SET__' }) // 未设置 + 未动 → 空串(「清除」意图同为空串 = 显式清空) expect(buildEmailCredsPayload(form(), '').smtp).toMatchObject({ password: '' }) // 输入新值 → 新值优先(无论原态) const typed = { ...form(), smtpPassword: 'new-pw' } expect(buildEmailCredsPayload(typed, '__SET__').smtp).toMatchObject({ password: 'new-pw' }) }) it('push:数值空串 → null(未设置哨兵)、enabled 布尔原样、密钥哨兵往返', () => { const payload = buildPushCredsPayload( { enabled: true, url: ' https://push.example/v1 ', apiKey: '', prefix: 'demo-p', timeout: '5.5', maxAttempts: '3', backoffBase: '' }, '__SET__' ) expect(payload).toEqual({ enabled: true, url: 'https://push.example/v1', api_key: '__SET__', prefix: 'demo-p', timeout: 5.5, max_attempts: 3, retry_backoff_base: null }) }) it('ai:api_key 哨兵/新值/清空三态 + 字段 trim', () => { const base = { provider: ' glm ', apiKey: '', baseUrl: ' https://x.com ', model: ' glm-4 ' } expect(buildAiCredsPayload({ ...base }, '__SET__')).toEqual({ provider: 'glm', api_key: '__SET__', base_url: 'https://x.com', model: 'glm-4' }) expect(buildAiCredsPayload({ ...base, apiKey: 'sk-new' }, '').api_key).toBe('sk-new') expect(buildAiCredsPayload({ ...base }, '').api_key).toBe('') }) }) describe('validate 前端预检(后端 400 的前置层)', () => { it('email:端口非整数/越界报错;空串合法(= 平台默认)', () => { const ok: EmailCredsForm = { smtpHost: '', smtpPort: '', smtpUser: '', smtpPassword: '', senderName: '', notifyEmails: [] } expect(validateEmailCredsForm(ok)).toEqual([]) for (const bad of ['0', '65536', 'abc', '1.5', '-1']) { expect(validateEmailCredsForm({ ...ok, smtpPort: bad })).toHaveLength(1) } expect(validateEmailCredsForm({ ...ok, smtpPort: '465' })).toEqual([]) expect(validateEmailCredsForm({ ...ok, smtpPort: '65535' })).toEqual([]) }) it('email:通知邮箱非空行须含 @(空行提交时剔除,不报错)', () => { const base: EmailCredsForm = { smtpHost: '', smtpPort: '', smtpUser: '', smtpPassword: '', senderName: '', notifyEmails: [] } expect(validateEmailCredsForm({ ...base, notifyEmails: ['', ' '] })).toEqual([]) expect( validateEmailCredsForm({ ...base, notifyEmails: ['a@x.com', 'not-an-email'] }) ).toHaveLength(1) }) it('push:URL 协议 / 正数 / 1-10 整数边界(与后端 _check_push 同款)', () => { const ok: PushCredsForm = { enabled: false, url: 'https://push.example/v1', apiKey: '', prefix: '', timeout: '10', maxAttempts: '3', backoffBase: '1' } expect(validatePushCredsForm(ok)).toEqual([]) // URL 空串合法(= 平台默认);非 http(s) 协议报错 expect(validatePushCredsForm({ ...ok, url: '' })).toEqual([]) expect(validatePushCredsForm({ ...ok, url: 'ftp://x' })).toHaveLength(1) // timeout 正数 expect(validatePushCredsForm({ ...ok, timeout: '0' })).toHaveLength(1) expect(validatePushCredsForm({ ...ok, timeout: '-1' })).toHaveLength(1) expect(validatePushCredsForm({ ...ok, timeout: 'abc' })).toHaveLength(1) expect(validatePushCredsForm({ ...ok, timeout: '0.5' })).toEqual([]) // max_attempts 1-10 整数 expect(validatePushCredsForm({ ...ok, maxAttempts: '0' })).toHaveLength(1) expect(validatePushCredsForm({ ...ok, maxAttempts: '11' })).toHaveLength(1) expect(validatePushCredsForm({ ...ok, maxAttempts: '2.5' })).toHaveLength(1) expect(validatePushCredsForm({ ...ok, maxAttempts: '10' })).toEqual([]) // backoff 正数;全部空串 = 平台默认,合法 expect(validatePushCredsForm({ ...ok, backoffBase: '0' })).toHaveLength(1) expect( validatePushCredsForm({ ...ok, url: '', timeout: '', maxAttempts: '', backoffBase: '' }) ).toEqual([]) }) }) describe('PUSH_PROBE_REASON_LABELS(不可达原因 → 修复提示)', () => { it('覆盖后端 reason 全部分类(dns/timeout/connect)', () => { expect(PUSH_PROBE_REASON_LABELS.dns).toContain('域名解析') expect(PUSH_PROBE_REASON_LABELS.timeout).toContain('超时') expect(PUSH_PROBE_REASON_LABELS.connect).toContain('拒绝') }) })