192 lines
7.7 KiB
JavaScript
192 lines
7.7 KiB
JavaScript
// tests/r3debug/bioflowPathConfirm.test.js —— 生信路径卡确认创建后 UI 断链修复
|
|
// 缺陷:FlowPathCard.confirmCreate 创建任务成功后 emit('updated') 无人监听,
|
|
// UI 无进度卡/无事件订阅/不落库(刷新即丢)。修复后走 store 一站式 confirmBioflowPath。
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { setActivePinia, createPinia } from 'pinia';
|
|
|
|
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: [] })),
|
|
}));
|
|
|
|
const connectJobEventsSpy = vi.fn();
|
|
const createJobFromPlanSpy = vi.fn();
|
|
vi.mock('../../src/stores/bioflow.js', async () => {
|
|
const actual = await vi.importActual('../../src/stores/bioflow.js');
|
|
const { useBioflowStore: useReal } = actual;
|
|
return {
|
|
...actual,
|
|
useBioflowStore: () => {
|
|
const st = useReal();
|
|
st.connectJobEvents = connectJobEventsSpy;
|
|
st.createJobFromPlan = createJobFromPlanSpy;
|
|
return st;
|
|
},
|
|
};
|
|
});
|
|
|
|
import { useChatStore } from '../../src/stores/chat.js';
|
|
import { useSessionsStore } from '../../src/stores/sessions.js';
|
|
import { useBioflowStore } from '../../src/stores/bioflow.js';
|
|
import { updateMessage } from '../../src/api/sessions.js';
|
|
|
|
function freshStores() {
|
|
const pinia = createPinia();
|
|
setActivePinia(pinia);
|
|
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('confirmBioflowPath 一站式任务创建', () => {
|
|
beforeEach(() => {
|
|
connectJobEventsSpy.mockReset();
|
|
createJobFromPlanSpy.mockReset();
|
|
vi.mocked(updateMessage).mockClear();
|
|
});
|
|
|
|
it('创建成功 → 挂 job_created 进度块 + 订阅任务事件 + 落库', async () => {
|
|
const { chat } = freshStores();
|
|
createJobFromPlanSpy.mockResolvedValue({
|
|
id: 'job-1', title: '生信任务', status: 'created',
|
|
stages: [
|
|
{ stage: 'genome_prepare', status: 'waiting' },
|
|
{ stage: 'identify', status: 'waiting' },
|
|
],
|
|
});
|
|
const aMsg = {
|
|
id: 'am1', role: 'assistant', status: 'done',
|
|
blocks: [
|
|
{ type: 'bioflow_path', status: 'done', candidates: [{ stages: ['genome_prepare', 'identify'] }], selected: 0 },
|
|
],
|
|
raw: {},
|
|
};
|
|
chat.messages = [aMsg];
|
|
|
|
const detail = await chat.confirmBioflowPath({
|
|
title: '生信任务',
|
|
stages: ['genome_prepare', 'identify'],
|
|
aMsg,
|
|
});
|
|
|
|
// 1. 返回 detail 与创建入参一致
|
|
expect(detail.id).toBe('job-1');
|
|
// 2. path 块落定 jobId + selected 固化
|
|
const pathBlk = aMsg.blocks.find((b) => b.type === 'bioflow_path');
|
|
expect(pathBlk.jobId).toBe('job-1');
|
|
expect(pathBlk.selected).toBe(0);
|
|
// 3. 进度块挂上且 jobStatus=created、阶段 waiting
|
|
const progBlk = aMsg.blocks.find((b) => b.type === 'bioflow_progress');
|
|
expect(progBlk).toBeTruthy();
|
|
expect(progBlk.jobId).toBe('job-1');
|
|
expect(progBlk.jobStatus).toBe('created');
|
|
expect(progBlk.stages.length).toBe(2);
|
|
// 4. 订阅了任务事件流(带 onEvent 桥接)
|
|
expect(connectJobEventsSpy).toHaveBeenCalledTimes(1);
|
|
expect(connectJobEventsSpy.mock.calls[0][0]).toBe('job-1');
|
|
expect(typeof connectJobEventsSpy.mock.calls[0][2].onEvent).toBe('function');
|
|
// 5. 落库(updateMessage 调用)
|
|
expect(updateMessage).toHaveBeenCalled();
|
|
});
|
|
|
|
it('onEvent 桥接把任务事件 patch 进消息进度块(阶段推进)', async () => {
|
|
const { chat } = freshStores();
|
|
createJobFromPlanSpy.mockResolvedValue({
|
|
id: 'job-2', title: '生信任务', status: 'created', stages: [{ stage: 'identify', status: 'waiting' }],
|
|
});
|
|
const aMsg = {
|
|
id: 'am2', role: 'assistant', status: 'done',
|
|
blocks: [{ type: 'bioflow_path', candidates: [{ stages: ['identify'] }], selected: 0 }],
|
|
raw: {},
|
|
};
|
|
chat.messages = [aMsg];
|
|
await chat.confirmBioflowPath({ title: 't', stages: ['identify'], aMsg });
|
|
|
|
const onEvent = connectJobEventsSpy.mock.calls[0][2].onEvent;
|
|
onEvent('stage_started', { job_id: 'job-2', stage: 'identify' });
|
|
onEvent('stage_done', { job_id: 'job-2', stage: 'identify' });
|
|
|
|
const progBlk = aMsg.blocks.find((b) => b.type === 'bioflow_progress');
|
|
const st = progBlk.stages.find((s) => s.stage === 'identify');
|
|
expect(st.status).toBe('done'); // 终态不回退语义保持
|
|
});
|
|
|
|
it('job_done 事件 → 挂存档提示块', async () => {
|
|
const { chat } = freshStores();
|
|
createJobFromPlanSpy.mockResolvedValue({
|
|
id: 'job-3', title: '生信任务', status: 'created', stages: [{ stage: 'identify', status: 'waiting' }],
|
|
});
|
|
const aMsg = {
|
|
id: 'am3', role: 'assistant', status: 'done',
|
|
blocks: [{ type: 'bioflow_path', candidates: [{ stages: ['identify'] }], selected: 0 }],
|
|
raw: {},
|
|
};
|
|
chat.messages = [aMsg];
|
|
await chat.confirmBioflowPath({ title: 't', stages: ['identify'], aMsg });
|
|
|
|
connectJobEventsSpy.mock.calls[0][2].onEvent('job_done', { job_id: 'job-3', report_path: '/r.html', figures: [] });
|
|
|
|
const archBlk = aMsg.blocks.find((b) => b.type === 'bioflow_archive');
|
|
expect(archBlk).toBeTruthy();
|
|
expect(archBlk.jobId).toBe('job-3');
|
|
});
|
|
|
|
it('创建失败不挂块、不落库', async () => {
|
|
const { chat } = freshStores();
|
|
createJobFromPlanSpy.mockRejectedValue(new Error('创建失败'));
|
|
const aMsg = {
|
|
id: 'am4', role: 'assistant', status: 'done',
|
|
blocks: [{ type: 'bioflow_path', candidates: [{ stages: ['identify'] }], selected: 0 }],
|
|
raw: {},
|
|
};
|
|
chat.messages = [aMsg];
|
|
await expect(
|
|
chat.confirmBioflowPath({ title: 't', stages: ['identify'], aMsg }),
|
|
).rejects.toThrow('创建失败');
|
|
expect(aMsg.blocks.some((b) => b.type === 'bioflow_progress')).toBe(false);
|
|
expect(updateMessage).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('hydrateDetail 恢复订阅:含未终态进度块的消息重连事件流', async () => {
|
|
const { chat, sessions } = freshStores();
|
|
const progressBlk = { type: 'bioflow_progress', jobId: 'job-9', jobStatus: 'running', stages: [] };
|
|
sessions.fetchDetail = vi.fn(async () => ({
|
|
messages: [
|
|
{ id: 'u1', role: 'user', text: 'q' },
|
|
{ id: 'a1', role: 'assistant', kind: 'qa', status: 'done', blocks: [progressBlk], raw: {} },
|
|
],
|
|
}));
|
|
await chat.loadSession('s1');
|
|
|
|
expect(connectJobEventsSpy).toHaveBeenCalledWith('job-9', 's1', expect.anything());
|
|
// 终态不重连
|
|
connectJobEventsSpy.mockClear();
|
|
sessions.fetchDetail = vi.fn(async () => ({
|
|
messages: [
|
|
{ id: 'a2', role: 'assistant', kind: 'qa', status: 'done', blocks: [{ type: 'bioflow_progress', jobId: 'job-10', jobStatus: 'done', stages: [] }], raw: {} },
|
|
],
|
|
}));
|
|
await chat.loadSession('s1');
|
|
expect(connectJobEventsSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|