272 lines
9.8 KiB
TypeScript
272 lines
9.8 KiB
TypeScript
// @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();
|
|
}
|