|
@@ -0,0 +1,99 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * 验证适配层产出的政策卡片字段:**同政策的多个申报事项必须能区分开**。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 背景(2026-09-17 用户报的 bug):
|
|
|
|
|
+ * 适配层原先把卡片的 `title` 与 `declaration_item` 都取自 `card.name.text`(只有政策名),
|
|
|
|
|
+ * 而后端不下发 `id`/`policy_id`(实测恒为 undefined)——同政策的多个申报事项
|
|
|
|
|
+ * 在字段上完全无法区分:详情面板标题一样、「查看详情」还会跳到同一条。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 验证的不变量:
|
|
|
|
|
+ * ① `declaration_item` 取后端每条唯一的 `data.title`(含申报事项后缀),同政策两条必须不同
|
|
|
|
|
+ * ② 卡片标题 `title` 仍是政策名(`card.name.text`)—— 卡片区的观感不变
|
|
|
|
|
+ * ③ 后端没给后缀时(单事项政策),`declaration_item` 退化为政策名,不出现空值
|
|
|
|
|
+ *
|
|
|
|
|
+ * 数据来源:`probe-policy-cards.mjs` 抓到的真实载荷形状(这里用合成数据,保证可重复;
|
|
|
|
|
+ * 真实载荷每次问法不同、LLM 输出会变,不适合做断言基准)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 怎么跑(在项目根目录):
|
|
|
|
|
+ * npx esbuild harness/tools/_entry-coordinator.ts --bundle --format=esm \
|
|
|
|
|
+ * --outfile=harness/tools/_coordinator.mjs --alias:@=./src --define:import.meta.env='{}'
|
|
|
|
|
+ * node harness/tools/verify-policy-card-fields.mjs
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+import { buildPolicyTableContent } from './_coordinator.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 POLICY_NAME = '关于进一步完善本区创业扶持政策有关事项的通知';
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 按真实 `item` 事件的形状造数据(字段名与 probe 抓到的载荷一致)。
|
|
|
|
|
+ * `cardName` 与 `title` 分开传:有申报事项后缀时两者不同,单事项时两者相同。
|
|
|
|
|
+ */
|
|
|
|
|
+const makeItem = (title, sourceId, supportText, cardName = POLICY_NAME) => ({
|
|
|
|
|
+ source_id: sourceId,
|
|
|
|
|
+ title,
|
|
|
|
|
+ knowledge_type: 'policy',
|
|
|
|
|
+ is_policy_library: true,
|
|
|
|
|
+ card: {
|
|
|
|
|
+ kind: 'policy',
|
|
|
|
|
+ name: { text: cardName, citations: [] },
|
|
|
|
|
+ department: { text: '区人社局' },
|
|
|
|
|
+ support_objects: { text: '对象' },
|
|
|
|
|
+ support_methods_standards: { text: supportText },
|
|
|
|
|
+ application_conditions: { text: '条件' },
|
|
|
|
|
+ match_reason: { text: '原因' },
|
|
|
|
|
+ },
|
|
|
|
|
+ priority: { rank: 1 },
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const items = [
|
|
|
|
|
+ makeItem(`${POLICY_NAME} - 创业开办费补贴`, 'k_ff8fe476c2708c93b0d7803e', '一次性创业开办费补贴2万元,最高2万元补贴。'),
|
|
|
|
|
+ makeItem(`${POLICY_NAME} - 青浦区级创业孵化基地的补贴和奖励`, 'k_6dc752bd7d30b42d430ca7fc', '分别给予40万/30万/20万运作经费补贴,最高40万元补贴'),
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+const content = buildPolicyTableContent(items, []);
|
|
|
|
|
+const table = JSON.parse(content.replace(/^<!-- POLICY_TABLE /, '').replace(/ POLICY_TABLE -->$/, ''));
|
|
|
|
|
+const [a, b] = table.data;
|
|
|
|
|
+
|
|
|
|
|
+console.log('场景:同一政策下的两个申报事项\n');
|
|
|
|
|
+
|
|
|
|
|
+console.log('【1】详情面板的申报事项必须能区分');
|
|
|
|
|
+check('两条的 declaration_item 不同', a.declaration_item !== b.declaration_item, `${a.declaration_item} / ${b.declaration_item}`);
|
|
|
|
|
+check('第 1 条含「创业开办费补贴」', a.declaration_item === `${POLICY_NAME} - 创业开办费补贴`, a.declaration_item);
|
|
|
|
|
+check('第 2 条含「创业孵化基地」', b.declaration_item === `${POLICY_NAME} - 青浦区级创业孵化基地的补贴和奖励`, b.declaration_item);
|
|
|
|
|
+
|
|
|
|
|
+console.log('\n【2】卡片标题保持政策名不变(卡片区观感不动)');
|
|
|
|
|
+check('两条的 title 都是政策名', a.title === POLICY_NAME && b.title === POLICY_NAME, `${a.title}`);
|
|
|
|
|
+check('title 与 declaration_item 不再相同', a.title !== a.declaration_item);
|
|
|
|
|
+
|
|
|
|
|
+console.log('\n【3】没有后缀时退化为政策名,不出现空值');
|
|
|
|
|
+// 真实形态:单事项政策的 title 与 card.name.text 相同(见 probe 抓到的 item[1])
|
|
|
|
|
+const SOLO_NAME = '青浦区关于促进技能人才发展的实施办法';
|
|
|
|
|
+const single = buildPolicyTableContent([makeItem(SOLO_NAME, 'k_x', '补贴', SOLO_NAME)], []);
|
|
|
|
|
+const singleRow = JSON.parse(single.replace(/^<!-- POLICY_TABLE /, '').replace(/ POLICY_TABLE -->$/, '')).data[0];
|
|
|
|
|
+check('declaration_item 非空', !!singleRow.declaration_item, JSON.stringify(singleRow.declaration_item));
|
|
|
|
|
+check('无后缀时等于政策名(与 title 一致)', singleRow.declaration_item === SOLO_NAME && singleRow.title === SOLO_NAME, `${singleRow.declaration_item} / ${singleRow.title}`);
|
|
|
|
|
+
|
|
|
|
|
+console.log('\n【4】详情下标反查:字段可区分后能直接命中(与 resolvePolicyDetailIndex 配合)');
|
|
|
|
|
+// 模拟 policies 数组(normalizePolicy 之后),declaration_item 不同 → 唯一命中
|
|
|
|
|
+const policies = table.data.map((d) => ({ declaration_item: d.declaration_item, originalData: d }));
|
|
|
|
|
+const { resolvePolicyDetailIndex } = await import('./_policy-match.mjs');
|
|
|
|
|
+check(
|
|
|
|
|
+ '用「同政策第 2 条」的字段反查 → 命中下标 1',
|
|
|
|
|
+ resolvePolicyDetailIndex(policies, { originalData: { declaration_item: b.declaration_item } }) === 1,
|
|
|
|
|
+ String(resolvePolicyDetailIndex(policies, { originalData: { declaration_item: b.declaration_item } }))
|
|
|
|
|
+);
|
|
|
|
|
+
|
|
|
|
|
+console.log(`\n===== 通过 ${pass} 项,失败 ${fail} 项 =====`);
|
|
|
|
|
+process.exit(fail ? 1 : 0);
|