Files
PlantAI-rag-new/ui/tests/r3debug/bioflowDots.test.js
adminandClaude Fable 5 f74b27daa1 fix(statusbar): 生信对话 4 个 agent 点不随相位点亮
根因: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>
2026-09-12 16:32:21 +08:00

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);
});
});