Files
PlantAI-rag-new/tests/frontend/unit/bioflow/intent.test.js

129 lines
7.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @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);
});
});