/** * 验证「惠企政策」判定与置顶逻辑。 * * 验证什么: * 1. 惠企政策判定——优先用接口字段 is_policy_library,缺失时回退到标题匹配, * 标题匹配要能容忍《》全半角等差异 * 2. 置顶——本轮匹配到的惠企政策排在最前,其余保持库内原顺序 * * 怎么跑(在项目根目录): * npm run build:tools # 或手工:npx esbuild <入口> --bundle ... * node harness/tools/verify-policy-library.mjs * * 说明:本脚本直接引用 src 里的同一份实现(policy-library-utils.ts), * 不是复制一份逻辑——复制会漂移,测的就不是真代码了。 */ import { createRequire } from 'node:module'; import { normPolicyName, buildLibraryNameSet, isPolicyLibraryItem, collectMatchedLibraryNames, pinMatchedFirst, buildPolicyDetailUrl, POLICY_DETAIL_URL_BASE, } from './_policy-library-utils.mjs'; const require = createRequire(import.meta.url); const library = require('../../public/merchant-agent/policies_public.v1.json'); let pass = 0; let fail = 0; const check = (name, cond, extra = '') => { if (cond) { pass++; console.log(' ok ' + name); } else { fail++; console.log(' FAIL ' + name + ' ' + extra); } }; console.log('惠企政策库:' + library.length + ' 条'); const nameSet = buildLibraryNameSet(library); console.log('去重后政策名:' + nameSet.size + ' 个\n'); console.log('=== 1. 名称归一化 ==='); check( '《》差异可忽略', normPolicyName('《上海市青浦区加强知识产权保护、促进质量提升和标准体系建设的政策措施》') === normPolicyName('上海市青浦区加强知识产权保护、促进质量提升和标准体系建设的政策措施') ); check('括号差异可忽略', normPolicyName('政策(试行)') === normPolicyName('政策(试行)')); check('空白差异可忽略', normPolicyName(' 政策 A ') === normPolicyName('政策A')); console.log('\n=== 2. 惠企政策判定 ==='); const sampleName = String(library[0].name).replace(/[《》]/g, ''); check( '标题能在库里找到 → 是惠企政策', isPolicyLibraryItem({ title: sampleName }, nameSet) ); check( '标题带《》也能找到 → 是惠企政策', isPolicyLibraryItem({ title: '《' + sampleName + '》' }, nameSet) ); check( '接口字段 is_policy_library=true → 是惠企政策', isPolicyLibraryItem({ title: '库里没有的名字', originalData: { is_policy_library: true } }, nameSet) ); check( '库中没有且无字段 → 不是惠企政策', !isPolicyLibraryItem({ title: '这个名字肯定不在惠企政策库里' }, nameSet) ); check('空标题 → 不是惠企政策', !isPolicyLibraryItem({ title: '' }, nameSet)); console.log('\n=== 3. 收集本轮匹配到的惠企政策 ==='); const cards = [ { title: sampleName }, // 命中 { title: '不在库里的政策' }, { title: '库里没有但后端标记了', originalData: { is_policy_library: true } }, ]; const matched = collectMatchedLibraryNames(cards, nameSet); check('收集到 2 条命中', matched.size === 2, '实际 ' + matched.size); console.log('\n=== 4. 置顶排序 ==='); const items = [ { title: 'A-未命中' }, { title: sampleName }, // 命中,应置顶 { title: 'B-未命中' }, { title: '库里没有但后端标记了', originalData: { is_policy_library: true } }, // 命中,应置顶 { title: 'C-未命中' }, ]; const ordered = pinMatchedFirst(items, matched, (i) => i.title); check('命中项排在最前', ordered.slice(0, 2).every((i) => matched.has(normPolicyName(i.title))), JSON.stringify(ordered.map((i) => i.title))); check('未命中项保持原顺序', JSON.stringify(ordered.slice(2).map((i) => i.title)) === JSON.stringify(['A-未命中', 'B-未命中', 'C-未命中']), JSON.stringify(ordered.slice(2).map((i) => i.title))); check('总数不变', ordered.length === items.length); check('无命中时顺序不变', JSON.stringify(pinMatchedFirst(items, new Set(), (i) => i.title).map((i) => i.title)) === JSON.stringify(items.map((i) => i.title))); console.log('\n=== 5. 真实场景抽样(库里前 3 条政策名)==='); library.slice(0, 3).forEach((p) => { const t = String(p.name).replace(/[《》]/g, ''); console.log(' ' + (isPolicyLibraryItem({ title: t }, nameSet) ? '✓' : '✗') + ' ' + t.slice(0, 40)); }); console.log('\n=== 6. 政策详情页地址(由 市级政策id 拼)==='); { const withId = { data: { '市级政策id': '676eb30861aee302a8607104' } }; check( '有 id → 拼出正确地址', buildPolicyDetailUrl(withId) === POLICY_DETAIL_URL_BASE + '?id=676eb30861aee302a8607104', buildPolicyDetailUrl(withId) ); check('无 id → 空串(不猜)', buildPolicyDetailUrl({ data: {} }) === ''); check('data 缺失 → 空串', buildPolicyDetailUrl({}) === ''); check('null → 空串', buildPolicyDetailUrl(null) === ''); check('id 为空白 → 空串', buildPolicyDetailUrl({ data: { '市级政策id': ' ' } }) === ''); check('id 前后空格 → 去掉', buildPolicyDetailUrl({ data: { '市级政策id': ' abc ' } }) === POLICY_DETAIL_URL_BASE + '?id=abc'); check('特殊字符 → 编码', buildPolicyDetailUrl({ data: { '市级政策id': 'a&b=c' } }) === POLICY_DETAIL_URL_BASE + '?id=a%26b%3Dc'); // 真实库覆盖率 const covered = library.filter((p) => buildPolicyDetailUrl(p)).length; console.log(' 真实库覆盖率: ' + covered + ' / ' + library.length); check('覆盖 261/262', covered === 261, '实际 ' + covered); const sample = library.find((p) => buildPolicyDetailUrl(p)); console.log(' 示例: ' + buildPolicyDetailUrl(sample)); } console.log('\n通过 ' + pass + ' 项,失败 ' + fail + ' 项'); process.exit(fail ? 1 : 0);