根因:StatusBar/TopBar 只读 chat.currentMessage 的 pipeline 块 steps,生信对话 (sendBioflow)从不创建 pipeline 块——相位推进只写在 bioflow_progress.stages, 4 个点永远空态。 修复: - chat.js 生信消息派生 pipeline 块(derived:true):bioflowDotStepsFromStages 按「每 4 阶段一轮」把 stages 镜像为 4 相位 steps(running→running、 failed→error 单调保持、done/skipped→done);newBioflowSkeletonMessage 自带 派生块,patchBioflowEvent/patchBioflowSnapshot 每次推进后同步,error 分支 收口当前 running 相位;AssistantMessage 与 _persistableBlocks 跳过派生块 (不渲染、不落库,后端 Block union 不受影响)。 - 连带修复:web_search_started 相位 retrieval→evidence(sse_relay.py 语义 属证据补全环节,原映射会在联网检索时造成「证据→检索」倒流跳变); labels.js 兜底表补两条与 BFF 对齐。 验证:r3debug 88/88(新增 bioflowDots 14 + statusBarDots 6,含真实时序 mount→流到达→相位推进→终态)、主套件 373/373、sse_relay 29/29、dist 构建。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
182 lines
7.1 KiB
JavaScript
182 lines
7.1 KiB
JavaScript
// tests/r3debug/bioflowDots.test.js —— 底部状态栏 4 个 agent 点在生信对话中不随相位点亮
|
|
// 缺陷:StatusBar/TopBar 只读 pipeline 块 steps,生信对话(sendBioflow)从不创建
|
|
// pipeline 块 → 4 个点永远空态。修复:生信消息派生 pipeline 块(derived:true),
|
|
// 由进度块 stages 按「每 4 阶段一轮」镜像;渲染与落库均跳过派生块。
|
|
import { describe, it, expect } from 'vitest';
|
|
import { createPinia, setActivePinia } from 'pinia';
|
|
import {
|
|
useChatStore,
|
|
newBioflowSkeletonMessage,
|
|
patchBioflowEvent,
|
|
patchBioflowSnapshot,
|
|
bioflowDotStepsFromStages,
|
|
ensureBioflowDotBlock,
|
|
syncBioflowDotBlock,
|
|
} from '../../src/stores/chat.js';
|
|
|
|
const PHASES = ['planning', 'retrieval', 'evidence', 'generation'];
|
|
|
|
function dotStates(steps) {
|
|
return PHASES.map((p) => (steps.find((s) => s.phase === p) || {}).state);
|
|
}
|
|
|
|
describe('bioflowDotStepsFromStages 纯映射', () => {
|
|
it('空 stages → 四相位全 waiting', () => {
|
|
const steps = bioflowDotStepsFromStages([]);
|
|
expect(steps.map((s) => s.phase)).toEqual(PHASES);
|
|
expect(dotStates(steps)).toEqual(['waiting', 'waiting', 'waiting', 'waiting']);
|
|
});
|
|
|
|
it('第一轮阶段映射:running 点亮、done 收尾、waiting 保持', () => {
|
|
const steps = bioflowDotStepsFromStages([
|
|
{ stage: 'a', status: 'running' },
|
|
{ stage: 'b', status: 'done' },
|
|
{ stage: 'c', status: 'waiting' },
|
|
{ stage: 'd', status: 'waiting' },
|
|
]);
|
|
expect(dotStates(steps)).toEqual(['running', 'done', 'waiting', 'waiting']);
|
|
});
|
|
|
|
it('轮次回绕:第二轮 running 覆盖首轮 done;未触及相位沿用首轮 done', () => {
|
|
const steps = bioflowDotStepsFromStages([
|
|
{ stage: 'a', status: 'done' },
|
|
{ stage: 'b', status: 'done' },
|
|
{ stage: 'c', status: 'done' },
|
|
{ stage: 'd', status: 'done' },
|
|
{ stage: 'e', status: 'running' },
|
|
]);
|
|
// e = 第 5 阶段 → 相位 planning(running 覆盖 done);retrieval 沿用首轮 done
|
|
expect(dotStates(steps)).toEqual(['running', 'done', 'done', 'done']);
|
|
});
|
|
|
|
it('failed → error,后续轮次不覆盖(error 单调保持)', () => {
|
|
const steps = bioflowDotStepsFromStages([
|
|
{ stage: 'a', status: 'failed' },
|
|
{ stage: 'b', status: 'waiting' },
|
|
{ stage: 'c', status: 'waiting' },
|
|
{ stage: 'd', status: 'waiting' },
|
|
{ stage: 'e', status: 'done' }, // 第二轮又轮到 planning,error 不得回退为 done
|
|
]);
|
|
expect(dotStates(steps)).toEqual(['error', 'waiting', 'waiting', 'waiting']);
|
|
});
|
|
|
|
it('skipped → done(终态语义)', () => {
|
|
const steps = bioflowDotStepsFromStages([{ stage: 'a', status: 'skipped' }]);
|
|
expect(dotStates(steps)).toEqual(['done', 'waiting', 'waiting', 'waiting']);
|
|
});
|
|
|
|
it('阶段不足 4 个时其余相位保持 waiting', () => {
|
|
const steps = bioflowDotStepsFromStages([{ stage: 'a', status: 'done' }]);
|
|
expect(dotStates(steps)).toEqual(['done', 'waiting', 'waiting', 'waiting']);
|
|
});
|
|
});
|
|
|
|
describe('派生 pipeline 块同步', () => {
|
|
it('newBioflowSkeletonMessage 自带 derived pipeline 块(全 waiting)', () => {
|
|
const msg = newBioflowSkeletonMessage();
|
|
const dot = msg.blocks.find((b) => b.type === 'pipeline' && b.derived);
|
|
expect(dot).toBeTruthy();
|
|
expect(dotStates(dot.steps)).toEqual(['waiting', 'waiting', 'waiting', 'waiting']);
|
|
});
|
|
|
|
it('patchBioflowEvent(job_created) 建进度块并同步派生块', () => {
|
|
const aMsg = { blocks: [] };
|
|
patchBioflowEvent(aMsg, {
|
|
type: 'job_created',
|
|
data: {
|
|
job_id: 'j1',
|
|
stages: [
|
|
{ stage: 'prepare', status: 'running' },
|
|
{ stage: 'identify', status: 'waiting' },
|
|
],
|
|
},
|
|
});
|
|
const prog = aMsg.blocks.find((b) => b.type === 'bioflow_progress');
|
|
const dot = aMsg.blocks.find((b) => b.type === 'pipeline' && b.derived);
|
|
expect(prog.jobId).toBe('j1');
|
|
expect(dot).toBeTruthy();
|
|
expect(dotStates(dot.steps)).toEqual(['running', 'waiting', 'waiting', 'waiting']);
|
|
});
|
|
|
|
it('patchBioflowEvent(stage_done) 推进后派生块随动', () => {
|
|
const aMsg = { blocks: [] };
|
|
patchBioflowEvent(aMsg, {
|
|
type: 'job_created',
|
|
data: { job_id: 'j1', stages: [{ stage: 'prepare', status: 'running' }] },
|
|
});
|
|
patchBioflowEvent(aMsg, { type: 'stage_done', data: { job_id: 'j1', stage: 'prepare' } });
|
|
const dot = aMsg.blocks.find((b) => b.type === 'pipeline' && b.derived);
|
|
expect(dotStates(dot.steps)).toEqual(['done', 'waiting', 'waiting', 'waiting']);
|
|
});
|
|
|
|
it('patchBioflowEvent(stage_failed) 派生块 error', () => {
|
|
const aMsg = { blocks: [] };
|
|
patchBioflowEvent(aMsg, {
|
|
type: 'job_created',
|
|
data: { job_id: 'j1', stages: [{ stage: 'prepare', status: 'running' }] },
|
|
});
|
|
patchBioflowEvent(aMsg, { type: 'stage_failed', data: { job_id: 'j1', stage: 'prepare', reason: 'boom' } });
|
|
const dot = aMsg.blocks.find((b) => b.type === 'pipeline' && b.derived);
|
|
expect(dotStates(dot.steps)).toEqual(['error', 'waiting', 'waiting', 'waiting']);
|
|
});
|
|
|
|
it('patchBioflowSnapshot 整体替换后派生块重算(断线重连回放路径)', () => {
|
|
const aMsg = { blocks: [] };
|
|
patchBioflowSnapshot(aMsg, {
|
|
job_id: 'j1',
|
|
job: { status: 'running' },
|
|
stages: [
|
|
{ stage: 'a', status: 'done' },
|
|
{ stage: 'b', status: 'done' },
|
|
{ stage: 'c', status: 'done' },
|
|
{ stage: 'd', status: 'done' },
|
|
{ stage: 'e', status: 'running' },
|
|
],
|
|
last_seq: 7,
|
|
});
|
|
const dot = aMsg.blocks.find((b) => b.type === 'pipeline' && b.derived);
|
|
expect(dotStates(dot.steps)).toEqual(['running', 'done', 'done', 'done']);
|
|
});
|
|
|
|
it('ensureBioflowDotBlock 幂等:已存在则复用不新建', () => {
|
|
const aMsg = { blocks: [] };
|
|
const first = ensureBioflowDotBlock(aMsg);
|
|
const second = ensureBioflowDotBlock(aMsg);
|
|
expect(second).toBe(first);
|
|
expect(aMsg.blocks.filter((b) => b.type === 'pipeline').length).toBe(1);
|
|
});
|
|
|
|
it('syncBioflowDotBlock 无进度块时保持全 waiting', () => {
|
|
const aMsg = { blocks: [] };
|
|
syncBioflowDotBlock(aMsg);
|
|
const dot = aMsg.blocks.find((b) => b.type === 'pipeline' && b.derived);
|
|
expect(dotStates(dot.steps)).toEqual(['waiting', 'waiting', 'waiting', 'waiting']);
|
|
});
|
|
});
|
|
|
|
describe('落库隔离', () => {
|
|
it('_persistableBlocks 保留原生 pipeline 块、排除 derived 派生块', () => {
|
|
setActivePinia(createPinia());
|
|
const chat = useChatStore();
|
|
const aMsg = {
|
|
blocks: [
|
|
{
|
|
type: 'pipeline',
|
|
status: 'running',
|
|
steps: [
|
|
{ name: '规划', state: 'running', desc: '', phase: 'planning' },
|
|
{ name: '检索', state: 'waiting', desc: '', phase: 'retrieval' },
|
|
],
|
|
},
|
|
{ type: 'bioflow_path', status: 'done', candidates: [] },
|
|
{ type: 'pipeline', derived: true, status: 'running', steps: [] },
|
|
],
|
|
};
|
|
const out = chat._persistableBlocks(aMsg);
|
|
const pipelines = out.filter((b) => b.type === 'pipeline');
|
|
expect(pipelines.length).toBe(1);
|
|
expect(pipelines[0].derived).toBeUndefined();
|
|
expect(out.some((b) => b.type === 'bioflow_path')).toBe(true);
|
|
});
|
|
});
|