// @vitest-environment jsdom // tests/frontend/unit/bioflow/intent.test.js —— 生信意图分流(L3) // 与 docs/team/cases_bioflow_intent.md 冻结词典 40 用例表同表双跑 // (TC-BIO-INTENT-001~047;后端 intent.py 同词典同规则,L2 侧由 T 执行同表校验)。 import { describe, expect, it } from 'vitest'; import { route, extractReuseName, hasAnyWord } from '../../../../ui/src/utils/intent.js'; /** 词典用例表(cases_bioflow_intent.md §4):[text, attachments, activeArtifact, bioflowContext, expected] */ const DICT_CASES = [ // # text 附件 活动文档 生信上下文 期望 ['帮我跑水稻 SPL 家族鉴定', false, false, false, 'bioflow'], ['跑一下拟南芥 MADS-box 家族分析', false, false, false, 'bioflow'], ['请帮我鉴定水稻 SPL 家族', false, false, false, 'bioflow'], ['识别玉米 bZIP 家族成员', false, false, false, 'bioflow'], ['帮我研究一下水稻 SPL 基因家族', false, false, false, 'bioflow'], ['分析小麦 WRKY 转录因子家族', false, false, false, 'bioflow'], ['做个大豆 NAC 家族分析', false, false, false, 'bioflow'], ['跑 SPL 家族', false, false, false, 'bioflow'], ['水稻 SPL 家族,帮我跑', false, false, false, 'bioflow'], ['基因家族鉴定:水稻 SPL', false, false, false, 'bioflow'], ['帮我跑水稻叶片 RNA-seq 差异表达分析', false, false, false, 'bioflow'], ['帮我做水稻全基因组变异检测', false, false, false, 'bioflow'], ['帮我跑水稻产量性状 GWAS 关联分析', false, false, false, 'bioflow'], ['什么是基因家族?', false, false, false, 'qa'], ['帮我跑一下', false, false, false, 'qa'], ['SPL 家族', false, false, false, 'qa'], ['今天天气不错', false, false, false, 'qa'], ['跑 SPL 家族鉴定需要准备什么输入?', false, false, false, 'qa'], // 无生信上下文漏判安全 ['帮我跑一个数据分析', false, false, false, 'qa'], ['你好呀', false, false, false, 'qa'], ['', false, false, false, 'qa'], // 空字符串 ['帮我跑香蕉 XYK 家族鉴定', false, false, false, 'qa'], // XYK 不在家族词典 ['跑水稻 spl 家族鉴定', false, false, false, 'bioflow'], // 家族名大小写不敏感 ['Please run rice SPL family identification', false, false, false, 'bioflow'], ['帮我跑(水稻 SPL 家族)鉴定!', false, false, false, 'bioflow'], ['用 XXX 存档任务的方法帮我跑水稻 SPL 家族鉴定', false, false, false, 'bioflow'], // 复用意图 ['根据 XXX 存档任务的方法帮我跑一下玉米 bZIP', false, false, false, 'bioflow'], ['参考 XXX 存档的方法再跑一个水稻 SPL', false, false, false, 'bioflow'], ['用《XXX》存档任务的方法帮我跑小麦 WRKY 家族', false, false, false, 'bioflow'], ['用 XXX 存档任务的方法', false, false, false, 'qa'], // 无目标任务词 ['用 XXX 存档任务的方法帮我跑 yyy', false, false, false, 'qa'], // yyy 无家族/对象匹配 ['帮我生成一份水稻 SPL 家族分析报告', false, false, false, 'docgen'], // docgen 优先于 bioflow ['帮我写一篇水稻基因家族研究论文', false, false, false, 'docgen'], ['导出成 PDF', false, true, false, 'export'], ['把这句话改成红色', false, true, false, 'edit'], // 无图表词 ['xxx 图表颜色换成红色', false, true, false, 'bioflow'], // 图表词走 FR-BIO-019 ['identify 需要什么输入', false, false, true, 'bioflow_qa'], // 不新建任务 ['帮我总结一下这个问题的要点', false, false, false, 'qa'], ['写一段水稻基因的介绍', false, false, false, 'qa'], // 介绍在 docgen 排除表 ['跑水稻 SPL 家族鉴定', false, true, false, 'bioflow'], // 活动文档不改判定 ]; describe('intent route —— 词典 40 用例同表双跑', () => { DICT_CASES.forEach(([text, att, artifact, ctx, expected], i) => { const label = text ? text.slice(0, 24) : '(空字符串)'; it(`#${i + 1} 「${label}」 → ${expected}`, () => { const { intent } = route(text, att, artifact, ctx); expect(intent).toBe(expected); }); }); }); describe('intent route —— 边界与实现注意(词典 §5)', () => { it('文本长度 ≤1 或纯空白 → qa', () => { expect(route('a').intent).toBe('qa'); expect(route(' ').intent).toBe('qa'); expect(route(null).intent).toBe('qa'); expect(route(undefined).intent).toBe('qa'); }); it('>2000 字符按截断前 2000 判定(长前缀不淹没意图)', () => { const long = '啊'.repeat(3000) + ' 帮我跑水稻 SPL 家族鉴定'; expect(route(long).intent).toBe('qa'); // 意图词在 2000 之后被截掉 const short = '帮我跑水稻 SPL 家族鉴定' + '啊'.repeat(3000); expect(route(short).intent).toBe('bioflow'); }); it('bioflow_qa 仅在 has_bioflow_context=true 生效', () => { expect(route('identify 需要什么输入', false, false, true).intent).toBe('bioflow_qa'); expect(route('identify 需要什么输入', false, false, false).intent).toBe('qa'); }); it('家族名后须跟语境词或位于句尾(SPL 会议不命中)', () => { expect(route('跑 SPL 会议').intent).toBe('qa'); expect(route('帮我跑玉米 bZIP').intent).toBe('bioflow'); // 句尾命中 }); it('「做」动词需对象组合(分析对象词命中)', () => { expect(route('帮我做水稻全基因组变异检测').intent).toBe('bioflow'); expect(route('帮我做个分析').intent).toBe('qa'); // 无对象词 }); it('附件 + 写类动词 → docgen(兜底信号,词典规则 4)', () => { expect(route('帮我整理一下', true, false, false).intent).toBe('docgen'); }); it('导出需 has_active_artifact 才生效', () => { expect(route('导出成 PDF', false, false, false).intent).toBe('qa'); }); it('命中句不打乱后续 QA 判定(无状态残留)', () => { expect(route('什么是 SPL 家族').intent).toBe('qa'); expect(route('帮我跑水稻 SPL 家族鉴定').intent).toBe('bioflow'); expect(route('什么是 SPL 家族').intent).toBe('qa'); }); }); describe('extractReuseName —— 复用方法名提取(词典实现注意 2)', () => { it('标准句式:方法名 = REUSE_PHRASE 之前最近的 3-20 字符片段', () => { expect(extractReuseName('用 XXX 存档任务的方法帮我跑水稻 SPL 家族鉴定')).toBe('XXX'); expect(extractReuseName('根据 XXX 存档任务的方法帮我跑一下玉米 bZIP')).toBe('XXX'); expect(extractReuseName('参考 XXX 存档的方法再跑一个水稻 SPL')).toBe('XXX'); expect(extractReuseName('用《XXX》存档任务的方法帮我跑小麦 WRKY 家族')).toBe('XXX'); }); it('非复用句式返回 null', () => { expect(extractReuseName('帮我跑水稻 SPL 家族鉴定')).toBeNull(); expect(extractReuseName('')).toBeNull(); }); it('片段长度不足 3 或超 20 返回 null', () => { expect(extractReuseName('用 AB 存档任务的方法帮我跑水稻 SPL 家族鉴定')).toBeNull(); // 21 字符片段超上限 expect(extractReuseName('用 一二三四五六七八九十一二三四五六七八九十一 存档任务的方法帮我跑水稻 SPL')).toBeNull(); }); }); describe('hasAnyWord —— 子串匹配工具', () => { it('大小写不敏感(英文词典词)', () => { expect(hasAnyWord('Run SPL', ['run'])).toBe(true); expect(hasAnyWord('RUN', ['run'])).toBe(true); }); it('null/undefined 容错', () => { expect(hasAnyWord(null, ['x'])).toBe(false); expect(hasAnyWord(undefined, ['x'])).toBe(false); }); });