// @ts-nocheck import { replacePlaceholders, parsePidMap, parseMainPid, parseCandidates, } from '../reports/md-splitter.js'; import { validateCandidateSelection, formatValidationFailure, } from './masking-validator.js'; const PASS = '\x1b[32m✓\x1b[0m'; const FAIL = '\x1b[31m✗\x1b[0m'; const isBrowser = typeof window !== 'undefined'; function ok(msg) { console.log(`${isBrowser ? '✓' : PASS} ${msg}`); } function err(msg) { console.log(`${isBrowser ? '✗' : FAIL} ${msg}`); } function header(s) { console.log(`\n=== ${s} ===`); } let _pass = 0, _fail = 0; function check(cond, msg) { if (cond) { _pass++; ok(msg); } else { _fail++; err(msg); } } function test_replacePlaceholders() { header('test 1: replacePlaceholders'); const pidMap = { 'emp_test1': { name: '曹云飞', id: '00856' }, 'emp_test2': { name: '张磊', id: '01126' }, }; let r = replacePlaceholders( '| 姓名 | 工号 |\n|---|---|\n| [NAME:emp_test1] | [ID:emp_test1] |', pidMap ); check(r.text.includes('曹云飞'), '1.1 NAME 替换为真实姓名'); check(r.text.includes('00856'), '1.1 ID 替换为真实工号'); check(!r.text.includes('[NAME:'), '1.1 无 token 残留'); check(r.warnings.length === 0, '1.1 无 warnings'); r = replacePlaceholders( '[NAME:emp_test1] vs [NAME:emp_test2]', pidMap ); check(r.text === '曹云飞 vs 张磊', `1.2 多 token 替换: "${r.text}"`); r = replacePlaceholders('[NAME:emp_unknown]', pidMap); check(r.text === '[NAME:emp_unknown]', '1.3 未匹配保留原文'); check(r.warnings.length === 1, `1.3 记 1 个 warning(实际 ${r.warnings.length})`); check(r.warnings[0].pid === 'emp_unknown', '1.3 warning 含 pid'); r = replacePlaceholders('[NAME:emp_test1]', {}); check(r.text === '[NAME:emp_test1]', '1.4 空 pidMap 保留原文'); check(r.warnings.length === 1, '1.4 空 pidMap 记 warning'); r = replacePlaceholders('', pidMap); check(r.text === '', '1.5 空 md 返回空'); check(r.warnings.length === 0, '1.5 空 md 无 warning'); r = replacePlaceholders('emp_test1', pidMap); check(r.text === 'emp_test1', '1.6 默认不替换裸 emp_xxx'); r = replacePlaceholders('emp_test1', pidMap, { replaceBare: true }); check(r.text === '00856', `1.7 replaceBare 开启时替换裸 emp_xxx 为工号(实际: ${r.text})`); const extPidMap = { 'emp_s1': { type: 'station', name: '三林东' }, 'emp_c1': { type: 'carType', name: '11A01' }, 'emp_t1': { type: 'trainNo', name: '1113' }, 'emp_e1': { type: 'employee', name: '曹云飞', id: '00856' }, }; r = replacePlaceholders('[STATION:emp_s1] [CAR:emp_c1] [TRAIN:emp_t1] [NAME:emp_e1]', extPidMap); check(r.text === '三林东 11A01 1113 曹云飞', `1.8 扩展 token 类型(实际: "${r.text}")`); check(r.warnings.length === 0, '1.8 扩展 token 无 warning'); r = replacePlaceholders('[ID:emp_s1]', extPidMap); check(r.text === '[ID:emp_s1]', '1.9 [ID:] 对车站无 id,保留原文'); check(r.warnings.length === 1, '1.9 [ID:] 车站记 warning'); r = replacePlaceholders('[ID:emp_e1]', extPidMap); check(r.text === '00856', '1.9 [ID:] 员工返回工号'); } function test_parseMeta() { header('test 2: parsePidMap / parseMainPid / parseCandidates'); let meta = { pidMap: { emp_test1: { name: '曹云飞', id: '00856' } }, mainPid: 'emp_test1', candidates: null, }; check(parsePidMap(meta).emp_test1.name === '曹云飞', '2.1 parsePidMap 单员工'); check(parseMainPid(meta) === 'emp_test1', '2.1 parseMainPid'); check(parseCandidates(meta) === null, '2.1 parseCandidates null'); meta = { pidMap: { emp_a: { name: '曹云飞', id: '00856' }, emp_b: { name: '张磊', id: '01126' }, }, mainPid: null, candidates: ['emp_a', 'emp_b'], }; check(Object.keys(parsePidMap(meta)).length === 2, '2.2 parsePidMap 多候选'); check(parseMainPid(meta) === null, '2.2 parseMainPid null'); check(parseCandidates(meta).length === 2, '2.2 parseCandidates 2 个'); check(Object.keys(parsePidMap(null)).length === 0, '2.3 空 meta pidMap'); check(parseMainPid(null) === null, '2.3 空 meta mainPid'); check(parseCandidates(null) === null, '2.3 空 meta candidates'); check(Object.keys(parsePidMap({})).length === 0, '2.4 空 object pidMap'); check(parseMainPid({}) === null, '2.4 空 object mainPid'); } function test_validateCandidateSelection() { header('test 3: validateCandidateSelection'); const cands = [{ employeeId: 'emp_a' }, { employeeId: 'emp_b' }]; check(validateCandidateSelection('emp_a', cands).valid, '3.1 emp_a 假名精确'); check(validateCandidateSelection('选 emp_b', cands).valid, '3.1 emp_b 含上下文'); check(validateCandidateSelection('emp_a', cands).employeeId === 'emp_a', '3.1 返回 employeeId'); check(validateCandidateSelection('1 号', cands).valid, '3.2 「1 号」'); check(validateCandidateSelection('选 1', cands).valid, '3.2 「选 1」'); check(validateCandidateSelection('第一个', cands).valid, '3.2 「第一个」'); check(validateCandidateSelection('第二号', cands).valid, '3.2 「第二号」'); check(validateCandidateSelection('1 号', cands).employeeId === 'emp_a', '3.2 序号→employeeId'); check(validateCandidateSelection('2 号', cands).employeeId === 'emp_b', '3.2 序号 2→emp_b'); let r = validateCandidateSelection('7 号', cands); check(!r.valid, '3.3 7 号 越界失败'); check(r.reason === 'INDEX_OUT_OF_RANGE', '3.3 reason=INDEX_OUT_OF_RANGE'); check(r.index === 7 && r.total === 2, '3.3 含 index/total'); r = validateCandidateSelection('emp_xxx', cands); check(!r.valid, '3.4 emp_xxx 不在候选'); check(r.reason === 'EMP_NOT_IN_CANDIDATES', '3.4 reason=EMP_NOT_IN_CANDIDATES'); r = validateCandidateSelection('曹云飞', cands); check(!r.valid, '3.5 用真实姓名绕过 → 失败'); check(r.reason === 'UNRECOGNIZED_FORMAT', '3.5 reason=UNRECOGNIZED_FORMAT'); check(validateCandidateSelection('1 号', []).valid === false, '3.6 空 candidates'); check(validateCandidateSelection('', cands).valid === false, '3.7 空 AI 回复'); r = validateCandidateSelection('1 号', ['emp_x', 'emp_y']); check(r.valid && r.employeeId === 'emp_x', '3.8 字符串数组候选'); } function test_formatValidationFailure() { header('test 4: formatValidationFailure'); let s = formatValidationFailure({ reason: 'INDEX_OUT_OF_RANGE', index: 7, total: 2 }); check(s.includes('7') && s.includes('2'), '4.1 INDEX_OUT_OF_RANGE 含 index/total'); check(s.includes('1-2'), '4.1 提示候选范围'); s = formatValidationFailure({ reason: 'EMP_NOT_IN_CANDIDATES', attempted: 'emp_xxx', validIds: ['emp_a', 'emp_b'] }); check(s.includes('emp_xxx'), '4.2 EMP_NOT_IN_CANDIDATES 含 attempted'); check(s.includes('emp_a'), '4.2 含候选列表'); s = formatValidationFailure({ reason: 'UNRECOGNIZED_FORMAT' }); check(s.includes('序号'), '4.3 UNRECOGNIZED_FORMAT 提示序号'); s = formatValidationFailure({ reason: 'NO_CANDIDATES' }); check(s.length > 0, '4.4 NO_CANDIDATES 有提示'); s = formatValidationFailure({ reason: 'NO_AI_RESPONSE' }); check(s.length > 0, '4.5 NO_AI_RESPONSE 有提示'); check(formatValidationFailure({ valid: true }) === '', '4.6 valid 返回空字符串'); s = formatValidationFailure({ reason: 'UNKNOWN_REASON' }); check(s.length > 0, '4.7 未知 reason 兜底'); } function test_endToEndMock() { header('test 5: 端到端 Mock(模拟多候选→选择→校验)'); const multiCandidateMeta = { pidMap: { emp_test1: { name: '曹云飞', id: '00856' }, emp_test2: { name: '张磊', id: '01126' }, }, mainPid: null, candidates: ['emp_test1', 'emp_test2'], }; const pidMap = parsePidMap(multiCandidateMeta); const cands = parseCandidates(multiCandidateMeta); check(cands && cands.length === 2, '5.1 解析多候选'); const aiResponse = '好的,为您查询 2 号员工'; const v = validateCandidateSelection(aiResponse, cands); check(v.valid, '5.2 「2 号」校验通过'); check(v.employeeId === 'emp_test2', `5.3 提取 employeeId=${v.employeeId}`); const personReportMeta = { pidMap: { emp_test2: { name: '张磊', id: '01126' } }, mainPid: 'emp_test2', candidates: null, }; const personPidMap = parsePidMap(personReportMeta); const mdWithToken = `# 个人报告\n\n| 姓名 | 工号 |\n|---|---|\n| [NAME:emp_test2] | [ID:emp_test2] |`; const { text: uiText } = replacePlaceholders(mdWithToken, personPidMap); check(uiText.includes('张磊') && uiText.includes('01126'), '5.4 UI 替换为明文'); check(!uiText.includes('[NAME:'), '5.5 UI 无 token 残留'); check(mdWithToken.includes('[NAME:emp_test2]'), '5.6 AI prompt 保留 token'); } export function runAll() { _pass = 0; _fail = 0; console.log(' masking-mock-test v3.1 '); try { test_replacePlaceholders(); test_parseMeta(); test_validateCandidateSelection(); test_formatValidationFailure(); test_endToEndMock(); } catch (e) { err(`异常: ${e.message}`); console.error(e); } console.log(`\n ${_pass} pass, ${_fail} fail `); return { pass: _pass, fail: _fail }; } if (isBrowser) { window.__maskingMockTest = { runAll }; console.log('masking-mock-test 已加载,运行 __maskingMockTest.runAll()'); } if (!isBrowser && typeof require !== 'undefined' && require.main === module) { runAll(); }