#27 消息级来源 title 空值串行:SourceRow 标题链空值 fallback(非空 title >paper_id>id>「(无标题)」,绝不渲染空文本或正文片段);bench store 新增 sourcesStale 失效标记,chat.js QA 流 onComplete 与检索模式完成终态置标记, SourcesPane 监听自动 force 重拉(终态一次,跨会话后台完成不打扰当前视图)。 后端 normalize title 回填由 Dev-B1 负责。 #32 KG 搜索「水稻」无结果无反馈:KgPane 成功空结果新增 kgNoResult 「未找到相关实体」提示(与 kgDown 服务离线、kgEmpty 初始空态三态文案区分、 触发条件互斥),搜索按钮空结果后保持可用,arginine 命中路径不回归。 #47b 关闭文档→返回→再关闭看不到文档:docgen store 状态机加固——open/ adoptArtifact 自含复位 viewMode='doc';close() 作废在途 content/html 拉取序号 (迟到响应不再复活已清空的 html/css/templateKey/letterhead);fetchHtml 追加 contentSeq 归属守卫。反复关闭/资料库往返/会话切换无残留。 #52a「总结论文」路由 QA:WRITE_VERB 补「总结/概括/归纳」;「总结」自身在 DOC_TYPE 中会误拉无附件普通总结句,规则 3 细化——纯总结类动词句须命中 其他文档类型词(如「论文」)才进 docgen,带附件走规则 4 兜底。后端 intent.py 词典同步由 Dev-B1 负责,请 TL 校验双端一致。 单测:tests/r3debug/r4fixes.test.js 新增 31 例(含 SourceRow fallback、 sourcesStale 链路、KgPane 三态、docgen 状态机迟到响应作废、意图路由), r3debug 全套 68/68 通过。
469 lines
19 KiB
JavaScript
469 lines
19 KiB
JavaScript
// @vitest-environment jsdom
|
||
// tests/r4debug/r4fixes.test.js —— r4 前端四项缺陷单测(Dev-F 归属)
|
||
// #27 消息级来源 title 空值 fallback + 新回答完成后会话来源强制重拉
|
||
// #32 知识图谱搜索成功但空结果的明确提示(与 kgDown/kgEmpty 三态互斥)
|
||
// #47b DocPane 视图状态机:关闭→重开 / 资料库↔文档往返 / 会话切换
|
||
// #52a 意图路由 WRITE_VERB 补「总结/概括/归纳」(带附件进 docgen,普通问答不误伤)
|
||
//
|
||
// 运行方式(worktree 内):cd ui && npx vitest run --config tests/r3debug/vitest.config.js tests/r4debug/r4fixes.test.js
|
||
// (r3debug/vitest.config.js 的 include 覆盖 tests/r3debug/**,显式指定文件路径即可跑本文件)
|
||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||
import { mount, flushPromises } from '@vue/test-utils';
|
||
import { setActivePinia, createPinia, getActivePinia } from 'pinia';
|
||
|
||
// jsdom 无 scrollTo:polyfill(SourceRow 高亮滚动 / 组件挂载用)
|
||
if (typeof window !== 'undefined' && typeof window.Element !== 'undefined' && !window.Element.prototype.scrollTo) {
|
||
window.Element.prototype.scrollTo = vi.fn();
|
||
}
|
||
|
||
// ---- mock 依赖链(与 r3debug/docgenPoll.test.js 同款)----
|
||
vi.mock('../../src/api/sessions.js', () => ({
|
||
createMessage: vi.fn(async () => ({ id: 'm' + Math.random().toString(36).slice(2, 6) })),
|
||
updateMessage: vi.fn(async () => ({ updated_at: new Date().toISOString() })),
|
||
}));
|
||
vi.mock('../../src/api/qa.js', () => ({
|
||
chatStream: vi.fn(async () => {}),
|
||
search: vi.fn(async () => ({ answer: '' })),
|
||
}));
|
||
vi.mock('../../src/api/private.js', () => ({
|
||
askPrivate: vi.fn(async () => ({ hit: false })),
|
||
}));
|
||
vi.mock('../../src/api/docgen.js', () => ({
|
||
generate: vi.fn(),
|
||
getTask: vi.fn(),
|
||
getArtifactContent: vi.fn(async () => ({ markdown: '' })),
|
||
getArtifactHtml: vi.fn(async () => ({ ok: true, html: '', css: '' })),
|
||
saveArtifactContent: vi.fn(async () => ({})),
|
||
editArtifact: vi.fn(async () => ({})),
|
||
exportArtifact: vi.fn(async () => ({})),
|
||
uploadLibrary: vi.fn(),
|
||
listLibrary: vi.fn(async () => ({ items: [] })),
|
||
removeLibrary: vi.fn(async () => ({ ok: true })),
|
||
listTasks: vi.fn(async () => ({ tasks: [] })),
|
||
}));
|
||
vi.mock('../../src/api/kg.js', () => ({
|
||
searchEntities: vi.fn(),
|
||
getNeighborhood: vi.fn(),
|
||
kgStatus: vi.fn(async () => ({ ok: true, active: true })),
|
||
}));
|
||
|
||
// ---- 被测模块 ----
|
||
import { useChatStore } from '../../src/stores/chat.js';
|
||
import { useSessionsStore } from '../../src/stores/sessions.js';
|
||
import { useBenchStore, __setHttpGet } from '../../src/stores/bench.js';
|
||
import { useDocgenStore } from '../../src/stores/docgen.js';
|
||
import SourceRow from '../../src/components/bench/SourceRow.vue';
|
||
import KgPane from '../../src/components/bench/KgPane.vue';
|
||
import { route } from '../../src/intent.js';
|
||
|
||
function freshPinia() {
|
||
const pinia = createPinia();
|
||
setActivePinia(pinia);
|
||
return pinia;
|
||
}
|
||
|
||
/** 预置会话(跳过 create 网络路径) */
|
||
function seedSession() {
|
||
const chat = useChatStore();
|
||
const sessions = useSessionsStore();
|
||
chat.sessionId = 's1';
|
||
sessions.activeId = 's1';
|
||
sessions.items = [{ id: 's1', title: 't', status: 'done', created_at: '', updated_at: '' }];
|
||
return { chat, sessions };
|
||
}
|
||
|
||
describe('#27 SourceRow title 空值 fallback(消息级来源条目)', () => {
|
||
afterEach(() => {
|
||
vi.restoreAllMocks();
|
||
});
|
||
|
||
function mountRow(source) {
|
||
return mount(SourceRow, {
|
||
props: { source, index: 0, count: null },
|
||
global: { stubs: { teleport: true } },
|
||
});
|
||
}
|
||
|
||
it('title 为空 → 显示 paper_id(绝不渲染空文本或正文片段)', () => {
|
||
const w = mountRow({ paper_id: 'PMC12345678', title: '', snippet: 'RESEARCH ARTICLE 正文片段…', score: 0.21 });
|
||
const name = w.find('.ft-name');
|
||
expect(name.text()).toBe('PMC12345678');
|
||
// tooltip 与显示文本一致(完整 paper_id)
|
||
expect(name.attributes('title')).toBe('PMC12345678');
|
||
// 正文片段绝不进入标题行
|
||
expect(name.text()).not.toContain('RESEARCH ARTICLE');
|
||
});
|
||
|
||
it('title 与 paper_id 都空 → 显示 id', () => {
|
||
const w = mountRow({ id: 'src-9', title: null, snippet: 'x' });
|
||
expect(w.find('.ft-name').text()).toBe('src-9');
|
||
});
|
||
|
||
it('title/paper_id/id 全空 → 显示 i18n 占位「(无标题)」', () => {
|
||
const w = mountRow({ title: ' ', snippet: 'x' });
|
||
expect(w.find('.ft-name').text()).toBe('(无标题)');
|
||
});
|
||
|
||
it('正常 title 显示不受影响(r3 契约不回归)', () => {
|
||
const w = mountRow({ paper_id: 'P1', title: '水稻抗病基因研究进展', score: 0.5 });
|
||
expect(w.find('.ft-name').text()).toBe('水稻抗病基因研究进展');
|
||
// 徽标真实 paper_id 契约不回归
|
||
expect(w.find('.ft-ico.idx').text()).toBe('P1');
|
||
});
|
||
});
|
||
|
||
describe('#27 新回答完成后会话来源强制重拉(sourcesStale 链路)', () => {
|
||
beforeEach(() => {
|
||
vi.useFakeTimers();
|
||
__setHttpGet(null);
|
||
});
|
||
afterEach(() => {
|
||
vi.useRealTimers();
|
||
vi.restoreAllMocks();
|
||
__setHttpGet(null);
|
||
});
|
||
|
||
it('有缓存且无失效标记 → 缓存短路不重拉', async () => {
|
||
freshPinia();
|
||
seedSession();
|
||
const bench = useBenchStore();
|
||
bench.sessionSources = { sources: [{ paper_id: 'P1', count: 1 }] };
|
||
const http = vi.fn(async () => ({ sources: [] }));
|
||
__setHttpGet(http);
|
||
const out = await bench.loadSessionSources();
|
||
expect(out.sources[0].paper_id).toBe('P1');
|
||
expect(http).not.toHaveBeenCalled();
|
||
});
|
||
|
||
it('chat 检索模式完成 → sourcesStale 置真 → 自动绕过缓存重拉,成功后标记清除', async () => {
|
||
freshPinia();
|
||
const { chat } = seedSession();
|
||
const bench = useBenchStore();
|
||
bench.sessionSources = { sources: [{ paper_id: 'P1', count: 1 }] };
|
||
const http = vi.fn(async () => ({ sources: [{ paper_id: 'P2', count: 2 }] }));
|
||
__setHttpGet(http);
|
||
|
||
const { search } = await import('../../src/api/qa.js');
|
||
vi.mocked(search).mockResolvedValue({ answer: 'ok', sources: [{ paper_id: 'P2', title: 't' }], degraded: false });
|
||
await chat.send('问题', { mode: 'search' });
|
||
// 终态后失效标记已置
|
||
expect(bench.sourcesStale).toBe(true);
|
||
|
||
const out = await bench.loadSessionSources();
|
||
expect(http).toHaveBeenCalledWith('/api/sessions/s1/sources', undefined);
|
||
expect(out.sources[0].paper_id).toBe('P2');
|
||
expect(bench.sourcesStale).toBe(false); // 成功后清除,不会反复重拉
|
||
});
|
||
|
||
it('QA 流 onComplete 终态 → sourcesStale 置真(终态才一次,不在流式中触发)', async () => {
|
||
freshPinia();
|
||
const { chat } = seedSession();
|
||
const bench = useBenchStore();
|
||
|
||
const { chatStream } = await import('../../src/api/qa.js');
|
||
let captured = null;
|
||
vi.mocked(chatStream).mockImplementation(async (_body, handlers) => {
|
||
captured = handlers;
|
||
handlers.onProgress({ payload: { phase: 'retrieval', label: 'r' } });
|
||
handlers.onProgress({ payload: { phase: 'generation', label: 'g' } });
|
||
handlers.onComplete({ payload: { answer: '答案', sources: [] } });
|
||
});
|
||
const p = chat.send('问题', { mode: 'ask' });
|
||
await p;
|
||
expect(captured).not.toBeNull();
|
||
// 完成前(流式过程中)不触发失效:终态后才一次
|
||
expect(chat.messages.find((m) => m.role === 'assistant')?.status).toBe('done');
|
||
expect(bench.sourcesStale).toBe(true);
|
||
});
|
||
|
||
it('跨会话后台完成不打扰当前视图(sessionId 不匹配时失效标记不置真)', async () => {
|
||
freshPinia();
|
||
const { chat } = seedSession();
|
||
const bench = useBenchStore();
|
||
// 当前视图在 s2;后台流属于 s1
|
||
chat.sessionId = 's2';
|
||
chat._markSourcesStaleSafe('s1');
|
||
expect(bench.sourcesStale).toBe(false);
|
||
chat._markSourcesStaleSafe('s2');
|
||
expect(bench.sourcesStale).toBe(true);
|
||
});
|
||
|
||
it('resetSessionData 清理 sourcesStale(会话切换不残留失效标记)', () => {
|
||
freshPinia();
|
||
const bench = useBenchStore();
|
||
bench.sourcesStale = true;
|
||
bench.resetSessionData();
|
||
expect(bench.sourcesStale).toBe(false);
|
||
expect(bench.sessionSources).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('#32 知识图谱搜索空结果提示(kgNoResult)', () => {
|
||
afterEach(() => {
|
||
vi.restoreAllMocks();
|
||
});
|
||
|
||
function mountKg() {
|
||
return mount(KgPane, {
|
||
global: { plugins: [getActivePinia()], stubs: { teleport: true, KgGraphView: true, KgEntityDetail: true } },
|
||
});
|
||
}
|
||
|
||
it('搜索成功但 results 空 → 显示「未找到相关实体」,搜索按钮恢复可用', async () => {
|
||
freshPinia();
|
||
const bench = useBenchStore();
|
||
bench.kgReady = true;
|
||
bench.kgStatus = { ok: true };
|
||
|
||
const { searchEntities } = await import('../../src/api/kg.js');
|
||
vi.mocked(searchEntities).mockResolvedValue({ results: [] });
|
||
|
||
const w = mountKg();
|
||
await w.find('.search-box').setValue('水稻');
|
||
await w.find('.search-btn').trigger('click');
|
||
await flushPromises();
|
||
|
||
expect(w.text()).toContain('未找到相关实体');
|
||
// 与 kgDown(服务不可用)互斥:不出现离线文案
|
||
expect(w.text()).not.toContain('知识图谱服务离线');
|
||
// 搜索按钮恢复可用(空结果不锁按钮)
|
||
const btn = w.find('.search-btn');
|
||
expect(btn.attributes('disabled')).toBeUndefined();
|
||
});
|
||
|
||
it('搜索失败(kgDown)不出现空结果提示(触发条件互斥)', async () => {
|
||
freshPinia();
|
||
const bench = useBenchStore();
|
||
bench.kgReady = true;
|
||
bench.kgStatus = { ok: true };
|
||
|
||
const { searchEntities } = await import('../../src/api/kg.js');
|
||
vi.mocked(searchEntities).mockRejectedValue(new Error('down'));
|
||
|
||
const w = mountKg();
|
||
await w.find('.search-box').setValue('水稻');
|
||
await w.find('.search-btn').trigger('click');
|
||
await flushPromises();
|
||
|
||
expect(w.text()).toContain('知识图谱服务离线');
|
||
expect(w.text()).not.toContain('未找到相关实体');
|
||
});
|
||
|
||
it('正常命中路径不回归:arginine 命中渲染结果列表、无「未找到」提示', async () => {
|
||
freshPinia();
|
||
const bench = useBenchStore();
|
||
bench.kgReady = true;
|
||
bench.kgStatus = { ok: true };
|
||
|
||
const { searchEntities } = await import('../../src/api/kg.js');
|
||
vi.mocked(searchEntities).mockResolvedValue({
|
||
results: [{ id: 'e1', name: 'arginine', type: 'Compound', support_count: 42 }],
|
||
});
|
||
|
||
const w = mountKg();
|
||
await w.find('.search-box').setValue('arginine');
|
||
await w.find('.search-btn').trigger('click');
|
||
await flushPromises();
|
||
|
||
expect(w.find('.result-item').exists()).toBe(true);
|
||
expect(w.find('.rs-name').text()).toBe('arginine');
|
||
expect(w.text()).not.toContain('未找到相关实体');
|
||
});
|
||
|
||
it('初始空态 kgEmpty 与空结果提示不同文案(三态区分)', () => {
|
||
freshPinia();
|
||
const bench = useBenchStore();
|
||
bench.kgReady = true;
|
||
const w = mountKg();
|
||
expect(w.find('.empty-note').text()).toContain('本次检索未产生图谱关系');
|
||
expect(w.find('.empty-note').text()).not.toContain('未找到相关实体');
|
||
});
|
||
});
|
||
|
||
describe('#47b DocPane 视图状态机(close→重开 / 资料库往返 / 会话切换)', () => {
|
||
beforeEach(() => {
|
||
freshPinia();
|
||
vi.useFakeTimers();
|
||
});
|
||
afterEach(() => {
|
||
vi.useRealTimers();
|
||
vi.restoreAllMocks();
|
||
});
|
||
|
||
it('打开文档 A → 关闭 → 重开:viewMode 复位为 doc、内容完整渲染', async () => {
|
||
const docgen = useDocgenStore();
|
||
await docgen.open({ artifact_id: 'a1', title: 'A', template_name: 't', status: 'done' });
|
||
expect(docgen.viewMode).toBe('doc');
|
||
expect(docgen.activeArtifact?.artifact_id).toBe('a1');
|
||
|
||
docgen.close();
|
||
expect(docgen.viewMode).toBe('doc');
|
||
expect(docgen.activeArtifact).toBeNull();
|
||
expect(docgen.md).toBe('');
|
||
expect(docgen.html).toBe('');
|
||
|
||
await docgen.open({ artifact_id: 'a1', title: 'A', template_name: 't', status: 'done' });
|
||
expect(docgen.viewMode).toBe('doc');
|
||
expect(docgen.activeArtifact?.artifact_id).toBe('a1');
|
||
expect(docgen.loading).toBe(false);
|
||
expect(docgen.loadError).toBeNull();
|
||
});
|
||
|
||
it('反复关闭 3 次:状态干净、无残留(html/css/templateKey/letterhead 全空)', async () => {
|
||
const docgen = useDocgenStore();
|
||
for (let i = 0; i < 3; i++) {
|
||
await docgen.open({ artifact_id: 'a1', title: 'A', status: 'done' });
|
||
docgen.close();
|
||
expect(docgen.activeArtifact).toBeNull();
|
||
expect(docgen.md).toBe('');
|
||
expect(docgen.html).toBe('');
|
||
expect(docgen.css).toBe('');
|
||
expect(docgen.templateKey).toBe('');
|
||
expect(docgen.letterhead).toBe('');
|
||
expect(docgen.viewMode).toBe('doc');
|
||
expect(docgen.loading).toBe(false);
|
||
expect(docgen.loadError).toBeNull();
|
||
}
|
||
});
|
||
|
||
it('资料库↔文档往返:资料库→返回文档→再资料库,viewMode 切换无残留', async () => {
|
||
const docgen = useDocgenStore();
|
||
const { getArtifactContent } = await import('../../src/api/docgen.js');
|
||
vi.mocked(getArtifactContent).mockResolvedValue({ markdown: '# 文档 A', title: 'A' });
|
||
await docgen.open({ artifact_id: 'a1', title: 'A', status: 'done' });
|
||
|
||
for (let i = 0; i < 3; i++) {
|
||
docgen.viewMode = 'library';
|
||
expect(docgen.viewMode).toBe('library');
|
||
docgen.viewMode = 'doc';
|
||
expect(docgen.viewMode).toBe('doc');
|
||
expect(docgen.activeArtifact?.artifact_id).toBe('a1');
|
||
expect(docgen.md).toBe('# 文档 A');
|
||
}
|
||
// 资料库视图下重开同一文档 → open 自含复位回 doc
|
||
docgen.viewMode = 'library';
|
||
await docgen.open({ artifact_id: 'a1', title: 'A', status: 'done' });
|
||
expect(docgen.viewMode).toBe('doc');
|
||
});
|
||
|
||
it('文档 A 关闭后迟到的 html 响应作废:不得复活 A 的状态(#47b 根因)', async () => {
|
||
const docgen = useDocgenStore();
|
||
const { getArtifactHtml } = await import('../../src/api/docgen.js');
|
||
|
||
// A 的 html 响应悬挂 10s:模拟打开过程中用户点击「关闭文档」(缺陷场景)
|
||
vi.mocked(getArtifactHtml).mockImplementation(async (id) => {
|
||
await new Promise((r) => setTimeout(r, 10000));
|
||
return { ok: true, html: `<h1>${id}</h1>`, css: 'body{}', template_key: 'official', letterhead: '' };
|
||
});
|
||
|
||
const openA = docgen.open({ artifact_id: 'a1', title: 'A', status: 'done' });
|
||
await vi.advanceTimersByTimeAsync(0); // content 微任务排干,html 已发出(悬挂中)
|
||
// 打开未完成时关闭:close 必须作废在途拉取
|
||
docgen.close();
|
||
expect(docgen.activeArtifact).toBeNull();
|
||
|
||
// 放行悬挂响应
|
||
await vi.advanceTimersByTimeAsync(10000);
|
||
await flushPromises();
|
||
await openA.catch(() => {});
|
||
|
||
// 关闭后迟到响应被序号守卫作废:A 的 html/css/templateKey 不复活
|
||
expect(docgen.html).toBe('');
|
||
expect(docgen.css).toBe('');
|
||
expect(docgen.templateKey).toBe('');
|
||
expect(docgen.activeArtifact).toBeNull();
|
||
|
||
// 重开文档 B:完整渲染不受 A 残留影响
|
||
vi.mocked(getArtifactHtml).mockResolvedValue({ ok: true, html: '<h1>B</h1>', css: 'b{}', template_key: 'report', letterhead: '' });
|
||
await docgen.open({ artifact_id: 'a2', title: 'B', status: 'done' });
|
||
expect(docgen.activeArtifact?.artifact_id).toBe('a2');
|
||
expect(docgen.html).toBe('<h1>B</h1>');
|
||
expect(docgen.viewMode).toBe('doc');
|
||
});
|
||
|
||
it('adoptArtifact 自含复位视图并清空旧内容(生成接管)', async () => {
|
||
const docgen = useDocgenStore();
|
||
docgen.viewMode = 'library';
|
||
docgen.html = '<h1>old</h1>';
|
||
docgen.css = 'old{}';
|
||
docgen.templateKey = 'old';
|
||
docgen.letterhead = 'old';
|
||
await docgen.adoptArtifact({ artifact_id: 'a1', title: 'A', status: 'done' });
|
||
expect(docgen.viewMode).toBe('doc');
|
||
expect(docgen.html).toBe('');
|
||
expect(docgen.css).toBe('');
|
||
expect(docgen.templateKey).toBe('');
|
||
expect(docgen.letterhead).toBe('');
|
||
expect(docgen.activeArtifact?.artifact_id).toBe('a1');
|
||
});
|
||
|
||
it('会话切换 resetSession 清空文档域:A 不串挂到 B', async () => {
|
||
const docgen = useDocgenStore();
|
||
await docgen.open({ artifact_id: 'a1', title: 'A', status: 'done' });
|
||
expect(docgen.hasActiveArtifact).toBe(true);
|
||
|
||
docgen.resetSession();
|
||
expect(docgen.activeArtifact).toBeNull();
|
||
expect(docgen.md).toBe('');
|
||
expect(docgen.html).toBe('');
|
||
expect(docgen.viewMode).toBe('doc');
|
||
expect(docgen.templateCandidates).toEqual([]);
|
||
});
|
||
});
|
||
|
||
describe('#52a 意图路由 WRITE_VERB 补总结/概括/归纳', () => {
|
||
it('「帮我总结论文」+ 附件 → docgen(规则 4 附件兜底)', () => {
|
||
expect(route('帮我总结论文', true, false)).toBe('docgen');
|
||
});
|
||
|
||
it('「帮我概括这篇文章」+ 附件 → docgen', () => {
|
||
expect(route('帮我概括这篇文章', true, false)).toBe('docgen');
|
||
});
|
||
|
||
it('「帮我归纳资料」+ 附件 → docgen', () => {
|
||
expect(route('帮我归纳资料', true, false)).toBe('docgen');
|
||
});
|
||
|
||
it('无附件「总结一下植物光合作用的过程」→ qa(无其他 DOC_TYPE 证据)', () => {
|
||
expect(route('总结一下植物光合作用的过程', false, false)).toBe('qa');
|
||
});
|
||
|
||
it('无附件「概括一下 OsSPL14 的功能」→ qa(无其他 DOC_TYPE 证据)', () => {
|
||
expect(route('概括一下 OsSPL14 的功能', false, false)).toBe('qa');
|
||
});
|
||
|
||
it('无附件「总结这篇论文」→ docgen(TL 裁决:DOC_TYPE 含「论文」,接受该行为变化)', () => {
|
||
expect(route('总结这篇论文', false, false)).toBe('docgen');
|
||
});
|
||
|
||
it('无附件「帮我概括这篇文章」→ docgen(DOC_TYPE 含「文章」类材料词之外,按 DOC_TYPE 命中)', () => {
|
||
// 「文章」不在 DOC_TYPE:按漏判安全应走 qa(附规则 4 附件兜底覆盖带附件场景)
|
||
expect(route('帮我概括这篇文章', false, false)).toBe('qa');
|
||
expect(route('帮我概括这篇文章', true, false)).toBe('docgen');
|
||
});
|
||
|
||
it('普通问答不误伤:你好 / 介绍自己 → qa', () => {
|
||
expect(route('你好,介绍一下你自己', false, false)).toBe('qa');
|
||
expect(route('你是谁', true, false)).toBe('qa');
|
||
});
|
||
|
||
it('既有写类动词回归:写/撰写 + 论文 → docgen;无附件无 DOC_TYPE 写类仍 qa', () => {
|
||
expect(route('帮我写一份水稻研究报告', false, false)).toBe('docgen');
|
||
expect(route('撰写论文', false, false)).toBe('docgen');
|
||
expect(route('写一首诗', false, false)).toBe('qa'); // DOC_TYPE_EXCLUDE 优先
|
||
});
|
||
|
||
it('「写一篇总结」仍进 docgen(DOC_TYPE 含「总结」,行为不变)', () => {
|
||
expect(route('写一篇总结', false, false)).toBe('docgen');
|
||
});
|
||
|
||
it('既有写类动词 + 总结类文档词不回归:「写一份工作总结」→ docgen', () => {
|
||
expect(route('写一份工作总结', false, false)).toBe('docgen');
|
||
});
|
||
|
||
it('规则 4 附件兜底:总结类动词带附件无 DOC_TYPE 也进 docgen', () => {
|
||
expect(route('总结一下这份资料', true, false)).toBe('docgen');
|
||
expect(route('归纳一下内容', true, false)).toBe('docgen');
|
||
});
|
||
});
|