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

140 lines
6.6 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/report.test.js —— P3 报告闭环单测(L3/L4)
// 覆盖:fig:// 前端渲染器兜底(替换/未知 id 降级)、报告登记(防重幂等)、
// revise 触发(FR-BIO-019)、job_done → 报告登记分发(chat store 集成)。
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createPinia, setActivePinia } from 'pinia';
vi.mock('../../../../ui/src/api/bioflow.js', () => ({
registerBioflowReport: vi.fn(async (payload) => ({ ok: true, data: { job_id: payload.job_id, figures: payload.figures } })),
reviseBioflowReport: vi.fn(async () => ({ ok: true, data: { rerun_stage: 'synteny' } })),
bioflowHealth: vi.fn(async () => ({ ok: true })),
listBioflowJobs: vi.fn(async () => ({ ok: true, data: { items: [], total: 0 } })),
listBioflowArchives: vi.fn(async () => ({ ok: true, data: { items: [], total: 0 } })),
getBioflowJob: vi.fn(async () => ({ ok: true, data: {} })),
getBioflowArchive: vi.fn(async () => ({ ok: true, data: {} })),
deleteBioflowJob: vi.fn(async () => {}),
deleteBioflowArchive: vi.fn(async () => {}),
runBioflowArchive: vi.fn(async () => ({ ok: true, data: {} })),
createBioflowArchive: vi.fn(async () => ({ ok: true, data: {} })),
createBioflowJob: vi.fn(async () => ({ ok: true, data: { id: 'j1', stages: [] } })),
confirmBioflowStage: vi.fn(async () => ({ ok: true, data: {} })),
rerunBioflowStage: 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: {} })),
getBioflowStageManifest: 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: [] } })),
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'); }),
}));
import { useBioflowStore } from '../../../../ui/src/stores/bioflow.js';
import * as bioflowApi from '../../../../ui/src/api/bioflow.js';
// 前端 fig:// 渲染兜底(与后端 resolve_fig_refs 同语义;未知 id 占位不报错)
function resolveFigRefs(html, figures) {
const data = figures || {};
return String(html || '')
.replace(/<img\s+src="fig:\/\/([a-zA-Z0-9_\-]+)"[^>]*\/?>/gi, (m, ref) => {
const uri = data[ref];
if (uri) return `<img src="${uri}" class="bio-figure" alt="${ref}" />`;
return `<span class="bio-fig-placeholder">[图引用缺失: ${ref}]</span>`;
})
.replace(/fig:\/\/([a-zA-Z0-9_\-]+)/gi, (m, ref) => {
const uri = data[ref];
return uri || `<span class="bio-fig-placeholder">[图引用缺失: ${ref}]</span>`;
});
}
describe('fig:// 图引用渲染兜底(TC-BIO-FLOW-005/006)', () => {
it('已知 id 替换为 data URI;无 fig:// 残留(TC-BIO-FLOW-005)', () => {
const html = '<img src="fig://fig1" alt="系统发育树" />';
const out = resolveFigRefs(html, { fig1: 'data:image/png;base64,AAAA' });
expect(out).not.toContain('fig://');
expect(out).toContain('data:image/png;base64,AAAA');
expect(out).toContain('bio-figure');
});
it('未知 id 占位降级不报错(TC-BIO-FLOW-006)', () => {
const out = resolveFigRefs('<img src="fig://nope" />', {});
expect(out).toContain('bio-fig-placeholder');
expect(out).not.toContain('fig://');
});
it('多图混合:已知替换 + 未知占位', () => {
const out = resolveFigRefs('<img src="fig://a" alt="1" /><img src="fig://b" alt="2" />', { a: 'data:x' });
expect(out).toContain('data:x');
expect(out).toContain('bio-fig-placeholder');
expect(out).not.toContain('fig://');
});
});
describe('bioflow store —— 报告登记与 revise(FR-BIO-016/019)', () => {
beforeEach(() => {
setActivePinia(createPinia());
vi.clearAllMocks();
});
it('registerReport 发出登记请求(job_id/标题/图清单字段齐全)', async () => {
const store = useBioflowStore();
store._registeredSet = new Set(); // 重置防重
const data = await store.registerReport({
jobId: 'j1', sessionId: 'sess-1', title: 'SPL 报告',
reportPath: '/d/final/report.html', figures: ['fig1', 'fig2'],
});
expect(bioflowApi.registerBioflowReport).toHaveBeenCalledWith(
expect.objectContaining({
job_id: 'j1', session_id: 'sess-1', title: 'SPL 报告',
md_path: '/d/final/report.html', figures: ['fig1', 'fig2'],
}),
);
expect(data).toBeTruthy();
});
it('同一 job 只登记一次(防重幂等)', async () => {
const store = useBioflowStore();
store._registeredSet = new Set();
await store.registerReport({ jobId: 'j1', sessionId: 's1', title: 't' });
await store.registerReport({ jobId: 'j1', sessionId: 's1', title: 't' });
expect(bioflowApi.registerBioflowReport).toHaveBeenCalledTimes(1);
});
it('登记失败不抛(降级:报告仍可直连下载)', async () => {
bioflowApi.registerBioflowReport.mockRejectedValueOnce(new Error('down'));
const store = useBioflowStore();
store._registeredSet = new Set();
const data = await store.registerReport({ jobId: 'j2', sessionId: 's1', title: 't' });
expect(data).toBeNull();
});
it('reviseReport 发出 revise 请求(FR-BIO-019 图表调整)', async () => {
const store = useBioflowStore();
await store.reviseReport({
jobId: 'j1', target: 'synteny 图1', instruction: '颜色换成红色', params: { color: 'red' },
});
expect(bioflowApi.reviseBioflowReport).toHaveBeenCalledWith('j1', {
target: 'synteny 图1', instruction: '颜色换成红色', params: { color: 'red' },
});
});
});