/** * 验证「更多」→「政策列表」侧边栏的纯逻辑:去重、排序、关键词过滤。 * * 注:侧边栏列的是**本条回答里的政策**(用户 2026-09-18 调整;此前一度做成 * 「整个会话跨消息聚合」,那个采集函数已随之删除)。 * * 为什么要单独测:列表要经过「去重 → 排序 → 关键词过滤」三步, * 边界容易出错(精简格式的占位符 declaration_item、同分排序抖动、空关键词、 * 空/脏输入)。这些都是纯逻辑,用断言守住比在浏览器里点便宜得多。 * * 怎么跑(在项目根目录): * npx esbuild harness/tools/_entry-policy-cards.ts --bundle --format=esm \ * --outfile=harness/tools/_policy-cards.mjs * node harness/tools/verify-policy-card-list.mjs */ import { buildPolicyCardDedupKey, dedupePolicyItems, sortPolicyCardsByMatchScore, matchesPolicyCardKeyword, } from './_policy-cards.mjs'; 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); } }; /** 三条测试用卡片(后续各节共用) */ const cardA = { declaration_item: '政策甲 - 事项一', title: '政策甲', match_score: 90 }; const cardB = { declaration_item: '政策乙 - 事项一', title: '政策乙', match_score: 45 }; const cardC = { declaration_item: '政策丙 - 事项一', title: '政策丙', match_score: 60 }; console.log('【1】去重键(buildPolicyCardDedupKey)'); check( '同 declaration_item → 同键', buildPolicyCardDedupKey(cardA) === buildPolicyCardDedupKey({ ...cardA, match_score: 10 }), buildPolicyCardDedupKey(cardA) ); check('不同 declaration_item → 不同键', buildPolicyCardDedupKey(cardA) !== buildPolicyCardDedupKey(cardB)); // 精简格式:declaration_item 是占位符,不能把不同卡片并成一条 const minimalA = { declaration_item: '查看政策详情', title: '甲', match_reason: '理由A' }; const minimalB = { declaration_item: '查看政策详情', title: '乙', match_reason: '理由B' }; check('占位符「查看政策详情」不按它去重(不同卡片不同键)', buildPolicyCardDedupKey(minimalA) !== buildPolicyCardDedupKey(minimalB), buildPolicyCardDedupKey(minimalA)); check('占位符场景仍可用同内容命中同键', buildPolicyCardDedupKey(minimalA) === buildPolicyCardDedupKey({ ...minimalA })); console.log('\n【3】去重(dedupePolicyItems,首见优先)'); const withDup = [cardA, cardB, { ...cardA, match_score: 999 }, cardC, { ...cardB }]; const deduped = dedupePolicyItems(withDup, buildPolicyCardDedupKey); check('去重后 3 条', deduped.length === 3, `实际 ${deduped.length}`); check('保留的是**首见**那条(match_score=90 而非 999)', deduped[0].match_score === 90, String(deduped[0].match_score)); check('保持首见顺序', deduped.map((c) => c.title).join(',') === '政策甲,政策乙,政策丙'); check('空输入安全', dedupePolicyItems(null, buildPolicyCardDedupKey).length === 0); console.log('\n【4】排序(sortPolicyCardsByMatchScore)'); const sorted = sortPolicyCardsByMatchScore([cardB, cardA, cardC]); // 45, 90, 60 check('按 match_score 降序', sorted.map((c) => c.match_score).join(',') === '90,60,45', sorted.map((c) => c.match_score).join(',')); const tieA = { declaration_item: 'a', match_score: 90, tag2: 'first' }; const tieB = { declaration_item: 'b', match_score: 90, tag2: 'second' }; const tieSorted = sortPolicyCardsByMatchScore([tieA, tieB]); check('同分保持原相对顺序(稳定排序)', tieSorted[0].tag2 === 'first' && tieSorted[1].tag2 === 'second', tieSorted.map((c) => c.tag2).join(',')); check('缺 match_score 视为 0 排最后', sortPolicyCardsByMatchScore([{ declaration_item: 'x' }, cardB])[1].declaration_item === 'x'); check('空输入安全', sortPolicyCardsByMatchScore([]).length === 0); console.log('\n【5】关键词过滤(matchesPolicyCardKeyword)'); const searchable = { title: '关于创业扶持的通知', desc: '面向本区创业者', declaration_item: '创业开办费补贴', match_reason: '用户询问开办补贴', source: '区人社局', enterprise_benefit: '最高2万元补贴', }; check('空关键词恒真', matchesPolicyCardKeyword(searchable, '') === true && matchesPolicyCardKeyword(searchable, ' ') === true); check('命中 title', matchesPolicyCardKeyword(searchable, '创业扶持') === true); check('命中 desc', matchesPolicyCardKeyword(searchable, '本区创业者') === true); check('命中 declaration_item', matchesPolicyCardKeyword(searchable, '开办费') === true); check('命中 match_reason', matchesPolicyCardKeyword(searchable, '询问') === true); check('命中 source(部门)', matchesPolicyCardKeyword(searchable, '区人社局') === true); check('命中 enterprise_benefit', matchesPolicyCardKeyword(searchable, '2万元') === true); check('大小写不敏感', matchesPolicyCardKeyword({ title: 'ABC Policy' }, 'abc policy') === true); check('不命中返回 false', matchesPolicyCardKeyword(searchable, '不存在的词') === false); check('item 为 null 不抛错', matchesPolicyCardKeyword(null, 'x') === false); console.log('\n【6】端到端组合(去重 → 排序 → 过滤)'); const e2e = sortPolicyCardsByMatchScore( dedupePolicyItems([cardA, cardB, cardC], buildPolicyCardDedupKey) ); check('组合后 3 条且按分数降序', e2e.length === 3 && e2e[0].match_score === 90, e2e.map((c) => c.match_score).join(',')); const e2eFiltered = e2e.filter((c) => matchesPolicyCardKeyword(c, '政策乙')); check('组合后可按关键词筛出 1 条', e2eFiltered.length === 1 && e2eFiltered[0].title === '政策乙', String(e2eFiltered.length)); console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`); process.exit(fail ? 1 : 0);