140 lines
6.6 KiB
JavaScript
140 lines
6.6 KiB
JavaScript
// @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' },
|
||
});
|
||
});
|
||
});
|