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

324 lines
14 KiB
JavaScript
Raw 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/panes.test.js —— P5 生信面板组件测试(L4)
// 覆盖 TC-BIO-JOB-001~016(BioJobsPane)与 TC-BIO-PANE-001~016(BioArchivePane):
// 任务列表 session-item 形态/阶段 DAG 取色/manifest+日志浏览/重跑/删除确认/running 禁用/
// 探活失败隐藏 TAB(bench store 断言由 benchpanel 层测试覆盖)/复用发起预填表单。
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createPinia, setActivePinia } from 'pinia';
vi.mock('../../../../ui/src/api/bioflow.js', () => ({
listBioflowJobs: vi.fn(async () => ({
ok: true,
data: {
items: [
{ id: 'job-1', title: '水稻 SPL 鉴定', status: 'running', progress: 33, stages: [{ stage: 'identify', status: 'done' }, { stage: 'align', status: 'running' }, { stage: 'phylogeny', status: 'waiting' }] },
{ id: 'job-2', title: '玉米 bZIP 分析', status: 'done', progress: 100, stages: [{ stage: 'identify', status: 'done' }] },
{ id: 'job-3', title: '小麦 WRKY 失败任务', status: 'failed', progress: 50, stages: [{ stage: 'identify', status: 'done' }, { stage: 'align', status: 'failed' }] },
],
total: 3,
},
})),
getBioflowJob: vi.fn(async () => ({
ok: true,
data: {
id: 'job-1', title: '水稻 SPL 鉴定', status: 'running',
stages: [{ stage: 'identify', status: 'done' }, { stage: 'align', status: 'running' }, { stage: 'phylogeny', status: 'waiting' }],
},
})),
deleteBioflowJob: vi.fn(async () => {}),
rerunBioflowStage: vi.fn(async () => ({ ok: true, data: {} })),
getBioflowStageManifest: vi.fn(async () => ({
ok: true,
data: { manifest: { status: 'success', artifacts: [] }, log_tail: 'log-line-1\nlog-line-2' },
})),
listBioflowArchives: vi.fn(async () => ({
ok: true,
data: {
items: [
{ id: 'arc-1', name: 'SPL 水稻鉴定', description: '描述', size_bytes: 2048, reused_count: 1, created_at: '2026-09-06T00:00:00Z', report_artifact: '/d/report.html' },
{ id: 'arc-2', name: 'bZIP 分析', size_bytes: 1024, created_at: '2026-09-05T00:00:00Z' },
],
total: 2,
},
})),
getBioflowArchive: vi.fn(async () => ({
ok: true,
data: {
id: 'arc-1', name: 'SPL 水稻鉴定', stage_chain: ['identify', 'align', 'phylogeny'],
params: { identify: { prefix: 'Ft.SPL' } },
products: [{ name: 'report.html', size: 2048 }, { name: 'final.zip', size: 1024 }],
report_artifact: '/d/report.html',
},
})),
deleteBioflowArchive: vi.fn(async () => {}),
runBioflowArchive: vi.fn(async () => ({ ok: true, data: { id: 'job-9', stages: [] } })),
bioflowHealth: vi.fn(async () => ({ ok: true })),
createBioflowJob: vi.fn(async () => ({ ok: true, data: { id: 'j1', stages: [] } })),
confirmBioflowStage: vi.fn(async () => ({ ok: true, data: {} })),
skipBioflowStage: vi.fn(async () => ({ ok: true, data: {} })),
pauseBioflowJob: vi.fn(async () => ({ ok: true, data: {} })),
resumeBioflowJob: vi.fn(async () => ({ ok: true, data: {} })),
createBioflowArchive: vi.fn(async () => ({ ok: true, data: {} })),
bioflowCatalog: vi.fn(async () => ({ ok: true, data: { stages: [] } })),
uploadBioflowFile: vi.fn(async () => ({ ok: true, data: {} })),
bioflowChatStream: vi.fn(async () => { throw new Error('unused'); }),
bioflowJobEvents: vi.fn(async () => { throw new Error('unused'); }),
bioflowAsk: vi.fn(async () => ({ ok: true, answer: { citations: [] } })),
reviseBioflowReport: vi.fn(async () => ({ ok: true, data: {} })),
registerBioflowReport: vi.fn(async () => ({ ok: true, data: {} })),
runBioflowArchiveByName: vi.fn(async () => ({ ok: true, data: {} })),
}));
vi.mock('../../../../ui/src/api/sessions.js', () => ({
createMessage: vi.fn(async () => ({ id: 'm1' })),
updateMessage: vi.fn(async () => ({ id: 'm1' })),
createSession: vi.fn(async () => ({ id: 'sess-1', title: 'x', status: 'idle', created_at: 'x', updated_at: 'x', message_count: 0, meta: {} })),
updateSession: vi.fn(async () => ({})),
deleteSession: vi.fn(async () => {}),
listSessions: vi.fn(async () => []),
getSession: vi.fn(),
getSessionSources: vi.fn(async () => ({ total: 0, sources: [] })),
}));
vi.mock('../../../../ui/src/api/qa.js', () => ({
chatStream: vi.fn(async () => { throw new Error('unused'); }),
search: vi.fn(async () => { throw new Error('unused'); }),
}));
vi.mock('../../../../ui/src/api/extraction.js', () => ({
listJobs: vi.fn(async () => ({ jobs: [], count: 0 })),
getJob: vi.fn(),
uploadPaper: vi.fn(),
getRunLog: vi.fn(async () => ({ text: '' })),
getProcessingStatus: vi.fn(),
getSummary: vi.fn(),
}));
vi.mock('../../../../ui/src/api/kg.js', () => ({
kgStatus: vi.fn(async () => ({ ok: true })),
kgOverview: vi.fn(),
searchEntities: vi.fn(),
getEntity: vi.fn(),
getNeighborhood: vi.fn(),
}));
import BioJobsPane from '../../../../ui/src/components/bench/BioJobsPane.vue';
import BioArchivePane from '../../../../ui/src/components/bench/BioArchivePane.vue';
import { useBioflowStore } from '../../../../ui/src/stores/bioflow.js';
import { useChatStore } from '../../../../ui/src/stores/chat.js';
import * as bioflowApi from '../../../../ui/src/api/bioflow.js';
function setup() {
setActivePinia(createPinia());
const bioflow = useBioflowStore();
const chat = useChatStore();
chat.sessionId = 'sess-1';
chat.messages = [{ id: 'a1', role: 'assistant', kind: 'bioflow', status: 'done', blocks: [], created_at: 'x', updated_at: 'x' }];
return { bioflow, chat };
}
describe('BioJobsPane —— TC-BIO-JOB-001~016(L4)', () => {
beforeEach(() => {
setup();
vi.clearAllMocks();
});
it('任务列表渲染 3 个 session-item 形态列表项(标题 13px + 状态徽章;TC-BIO-JOB-002)', async () => {
const w = mount(BioJobsPane);
await flushPromises();
const items = w.findAll('.bj-item');
expect(items.length).toBe(3);
expect(items[0].text()).toContain('水稻 SPL 鉴定');
expect(items[0].find('.badge').exists()).toBe(true);
expect(items[0].find('.bj-progress').text()).toBe('33%');
});
it('点击任务项:选中并展开详情(阶段 DAG 显示;TC-BIO-JOB-004/ACT-090)', async () => {
const w = mount(BioJobsPane);
await flushPromises();
await w.findAll('.bj-item')[0].trigger('click');
await flushPromises();
expect(w.find('.bj-detail').exists()).toBe(true);
const dag = w.findAll('.bj-dag-row');
expect(dag.length).toBe(3);
// DAG 状态取色(TC-BIO-JOB-005:done/running/waiting 三色点)
expect(dag[0].classes()).toContain('done');
expect(dag[1].classes()).toContain('running');
expect(dag[2].classes()).toContain('waiting');
});
it('DAG 节点 8px 圆点按阶段状态取色(TC-BIO-JOB-005 细节)', async () => {
const w = mount(BioJobsPane);
await flushPromises();
await w.findAll('.bj-item')[0].trigger('click');
await flushPromises();
const dots = w.findAll('.bj-dag-dot');
expect(dots[0].text()).toBe('✓');
expect(dots[1].text()).toBe('●');
expect(dots[2].text()).toBe('◌');
});
it('点击 DAG 阶段行:打开 manifest + 日志尾(code-block 渲染;TC-BIO-JOB-006/ACT-094)', async () => {
const w = mount(BioJobsPane);
await flushPromises();
await w.findAll('.bj-item')[0].trigger('click');
await flushPromises();
await w.findAll('.bj-dag-row')[0].trigger('click');
await flushPromises();
expect(bioflowApi.getBioflowStageManifest).toHaveBeenCalledWith('job-1', 'identify');
expect(w.find('.bj-manifest').exists()).toBe(true);
const code = w.find('.bj-manifest .code-block');
expect(code.text()).toContain('log-line-1');
expect(code.text()).toContain('manifest');
});
it('重跑按钮:选中含 failed 阶段任务时发出 rerun(TC-BIO-JOB-007/ACT-063)', async () => {
const w = mount(BioJobsPane);
await flushPromises();
// job-3 含 failed 阶段
await w.findAll('.bj-item')[2].trigger('click');
await flushPromises();
const rerunBtn = w.find('.bj-rerun');
expect(rerunBtn.exists()).toBe(true);
await rerunBtn.trigger('click');
await flushPromises();
expect(bioflowApi.rerunBioflowStage).toHaveBeenCalledWith('job-3', 'align', null);
});
it('删除:弹二次确认 → 确认后 DELETE 并移除(TC-BIO-JOB-008/ACT-064)', async () => {
const w = mount(BioJobsPane);
await flushPromises();
// job-2 状态 done 可删除
const delBtn = w.findAll('.bj-del')[1];
await delBtn.trigger('click');
expect(w.find('.bj-confirm').exists()).toBe(true);
await w.find('.pane-btn.danger').trigger('click');
await flushPromises();
expect(bioflowApi.deleteBioflowJob).toHaveBeenCalledWith('job-2');
});
it('删除二次确认取消:无 DELETE 请求(TC-BIO-JOB/ACT-067 语义)', async () => {
const w = mount(BioJobsPane);
await flushPromises();
await w.findAll('.bj-del')[1].trigger('click');
await w.find('.bj-confirm .pane-btn').trigger('click'); // 取消
expect(bioflowApi.deleteBioflowJob).not.toHaveBeenCalled();
});
it('running 任务删除按钮禁用(TC-BIO-JOB-009/ACT-068)', async () => {
const w = mount(BioJobsPane);
await flushPromises();
const delBtn = w.findAll('.bj-del')[0]; // job-1 running
expect(delBtn.attributes('disabled')).toBeDefined();
});
it('空任务列表:空态文案显示(TC-BIO-JOB-010)', async () => {
bioflowApi.listBioflowJobs.mockResolvedValueOnce({ ok: true, data: { items: [], total: 0 } });
const w = mount(BioJobsPane);
await flushPromises();
expect(w.find('.bj-empty').text()).toContain('暂无生信任务');
});
it('列表加载失败:错误态 + 重新加载(失败必须可见)', async () => {
bioflowApi.listBioflowJobs.mockRejectedValueOnce(new Error('down'));
const w = mount(BioJobsPane);
await flushPromises();
expect(w.find('.bj-state.error').exists()).toBe(true);
});
});
describe('BioArchivePane —— TC-BIO-PANE-001~016(L4)', () => {
beforeEach(() => {
setup();
vi.clearAllMocks();
});
it('档案列表渲染 2 项(标题+大小;TC-BIO-PANE-002)', async () => {
const w = mount(BioArchivePane);
await flushPromises();
const items = w.findAll('.bap-item');
expect(items.length).toBe(2);
expect(items[0].text()).toContain('SPL 水稻鉴定');
expect(items[0].text()).toContain('KB');
});
it('点击档案项:详情显示阶段链/参数/产物/报告(TC-BIO-PANE-003/004/005)', async () => {
const w = mount(BioArchivePane);
await flushPromises();
await w.findAll('.bap-item')[0].trigger('click');
await flushPromises();
expect(w.find('.bap-detail').exists()).toBe(true);
expect(w.find('.bap-stages').text()).toContain('identify');
expect(w.find('.bap-stages').text()).toContain('phylogeny');
expect(w.find('.bap-products').text()).toContain('report.html');
expect(w.find('.bap-report').exists()).toBe(false); // 未点查看报告
// 查看报告按钮存在(report_artifact 非空)
const btns = w.findAll('.bap-detail .pane-btn');
expect(btns.some((b) => b.text().includes('查看报告'))).toBe(true);
});
it('查看报告:iframe 打开归档报告(TC-BIO-PANE-004)', async () => {
const w = mount(BioArchivePane);
await flushPromises();
await w.findAll('.bap-item')[0].trigger('click');
await flushPromises();
const viewBtn = w.findAll('.bap-detail .pane-btn').find((b) => b.text().includes('查看报告'));
await viewBtn.trigger('click');
expect(w.find('.bap-report-frame').exists()).toBe(true);
expect(w.find('.bap-report-frame').attributes('src')).toContain('/api/bioflow/archives/arc-1/report');
});
it('删除归档:二次确认 → DELETE 并移除(TC-BIO-PANE-006)', async () => {
const w = mount(BioArchivePane);
await flushPromises();
await w.findAll('.bap-item')[0].trigger('click');
await flushPromises();
const delBtn = w.find('.bap-del');
await delBtn.trigger('click');
expect(w.find('.bap-confirm').exists()).toBe(true);
await w.find('.pane-btn.danger').trigger('click');
await flushPromises();
expect(bioflowApi.deleteBioflowArchive).toHaveBeenCalledWith('arc-1');
});
it('删除取消:无 DELETE 请求(TC-BIO-PANE-007)', async () => {
const w = mount(BioArchivePane);
await flushPromises();
await w.findAll('.bap-item')[0].trigger('click');
await flushPromises();
await w.find('.bap-del').trigger('click');
await w.find('.bap-confirm .pane-btn').trigger('click'); // 取消
expect(bioflowApi.deleteBioflowArchive).not.toHaveBeenCalled();
});
it('复用发起:装载预填表单并挂到消息流(TC-BIO-PANE-008/009/ACT-070)', async () => {
const w = mount(BioArchivePane);
await flushPromises();
await w.findAll('.bap-item')[0].trigger('click');
await flushPromises();
const reuseBtn = w.find('.bap-reuse');
await reuseBtn.trigger('click');
await flushPromises();
// chat.showReuseForm 把 bioflow_reuse 块挂到助手消息
const chat = useChatStore();
const blk = chat.messages.find((m) => m.role === 'assistant').blocks.find((b) => b.type === 'bioflow_reuse');
expect(blk).toBeTruthy();
expect(blk.archiveId).toBe('arc-1');
expect(blk.archiveName).toBe('SPL 水稻鉴定');
});
it('空档案列表:空态文案(TC-BIO-PANE-011)', async () => {
bioflowApi.listBioflowArchives.mockResolvedValueOnce({ ok: true, data: { items: [], total: 0 } });
const w = mount(BioArchivePane);
await flushPromises();
expect(w.find('.bap-empty').text()).toContain('暂无生信档案');
});
it('档案大小人类可读格式(TC-BIO-PANE-012)', async () => {
const w = mount(BioArchivePane);
await flushPromises();
const sizes = w.findAll('.bap-size');
expect(sizes[0].text()).toContain('KB');
});
});