forked from admin/PlantAI-rag-new
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>
This commit is contained in:
@@ -74,7 +74,7 @@ STAGE_PHASE: dict[str, str] = {
|
||||
"subquestion_retry_started": "retrieval",
|
||||
"evidence_accepted": "evidence",
|
||||
"evidence_review_completed": "evidence",
|
||||
"web_search_started": "retrieval",
|
||||
"web_search_started": "evidence",
|
||||
"web_search_completed": "evidence",
|
||||
"answer_generation_started": "generation",
|
||||
"completed": "done",
|
||||
|
||||
@@ -59,6 +59,8 @@ def test_stage_phase_maps_all_13_known_stages():
|
||||
"subquestion_retry_started": ("retrieval", "子问题重试中"),
|
||||
"evidence_accepted": ("evidence", "证据已采纳"),
|
||||
"evidence_review_completed": ("evidence", "证据审核完成"),
|
||||
"web_search_started": ("evidence", "开始联网检索"),
|
||||
"web_search_completed": ("evidence", "联网检索完成"),
|
||||
"answer_generation_started": ("generation", "开始生成答案"),
|
||||
"completed": ("done", "检索完成"),
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ const degradedNote = computed(() => props.message.degradedNote || null);
|
||||
</div>
|
||||
|
||||
<!-- pipeline:检索流水线五相位卡 -->
|
||||
<PipelineCard v-if="b.type === 'pipeline'" :block="b" />
|
||||
<PipelineCard v-if="b.type === 'pipeline' && !b.derived" :block="b" />
|
||||
|
||||
<!-- P2 生信:路径选择卡(FR-BIO-002/003/004) -->
|
||||
<FlowPathCard
|
||||
|
||||
@@ -42,7 +42,9 @@ onMounted(async () => {
|
||||
try {
|
||||
const m = await import('../../stores/chat.js');
|
||||
chatStore.value = m.useChatStore();
|
||||
} catch {
|
||||
} catch (e) {
|
||||
// 失败可见:静默 null 会让底部 4 点永远空态且无从排查
|
||||
console.error('[StatusBar] chat store 挂载失败:', e);
|
||||
chatStore.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -48,6 +48,8 @@ export const STAGE_PHASE = {
|
||||
subquestion_retry_started: 'retrieval',
|
||||
evidence_accepted: 'evidence',
|
||||
evidence_review_completed: 'evidence',
|
||||
web_search_started: 'evidence',
|
||||
web_search_completed: 'evidence',
|
||||
answer_generation_started: 'generation',
|
||||
completed: 'done',
|
||||
};
|
||||
|
||||
+102
-11
@@ -113,16 +113,33 @@ export function newBioflowSkeletonMessage() {
|
||||
status: 'streaming',
|
||||
streamErr: null,
|
||||
interrupted: false,
|
||||
blocks: [{
|
||||
type: 'bioflow_path',
|
||||
status: 'running',
|
||||
candidates: [],
|
||||
selected: null,
|
||||
autoFilled: [],
|
||||
reuseArchive: null,
|
||||
reuseName: null,
|
||||
fallback: null,
|
||||
}],
|
||||
blocks: [
|
||||
{
|
||||
type: 'bioflow_path',
|
||||
status: 'running',
|
||||
candidates: [],
|
||||
selected: null,
|
||||
autoFilled: [],
|
||||
reuseArchive: null,
|
||||
reuseName: null,
|
||||
fallback: null,
|
||||
},
|
||||
// 派生 pipeline 块(derived:true):为底部状态栏 4 个 agent 点提供相位数据。
|
||||
// 生信对话从不建原生 pipeline 块,StatusBar/TopBar 只读 pipeline 块——派生块
|
||||
// 由进度块 stages 镜像而来,渲染(AssistantMessage)与落库(_persistableBlocks)
|
||||
// 均跳过,仅布局组件消费。
|
||||
{
|
||||
type: 'pipeline',
|
||||
derived: true,
|
||||
status: 'running',
|
||||
steps: AGENT_DOT_PHASES.map((phase) => ({
|
||||
name: PHASE_LABEL[phase],
|
||||
state: 'waiting',
|
||||
desc: PHASE_DESC[phase],
|
||||
phase,
|
||||
})),
|
||||
},
|
||||
],
|
||||
raw: null,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
@@ -194,6 +211,69 @@ export function patchBioflowSnapshot(aMsg, snap) {
|
||||
blk.stages = (snap && Array.isArray(snap.stages) ? snap.stages : [])
|
||||
.map((s) => progressStage(s.stage, s.status, s));
|
||||
blk.lastSeq = typeof snap.last_seq === 'number' ? snap.last_seq : blk.lastSeq;
|
||||
syncBioflowDotBlock(aMsg);
|
||||
return blk;
|
||||
}
|
||||
|
||||
/** 生信阶段状态 → 相位点状态(派生 pipeline 块;单调:error 与 done 不因后续阶段回退) */
|
||||
function dotStateOf(st) {
|
||||
const s = st && st.status;
|
||||
if (s === 'failed') return 'error';
|
||||
if (s === 'running') return 'running';
|
||||
if (s === 'done' || s === 'skipped') return 'done';
|
||||
return 'waiting';
|
||||
}
|
||||
|
||||
/**
|
||||
* 进度块 stages → 底部 4 个 agent 点的 steps。
|
||||
* 相位映射按「每 4 个连续阶段 = 一轮」循环(stages[r*4+i] → AGENT_DOT_PHASES[i]),
|
||||
* 与 FlowProgressCard 的相位色循环同源;waiting/缺失阶段沿用上一轮该相位状态,
|
||||
* 已 error 的点不因后续阶段回退。
|
||||
*/
|
||||
export function bioflowDotStepsFromStages(stages) {
|
||||
const arr = Array.isArray(stages) ? stages : [];
|
||||
const out = AGENT_DOT_PHASES.map((phase) => ({
|
||||
name: PHASE_LABEL[phase],
|
||||
state: 'waiting',
|
||||
desc: PHASE_DESC[phase],
|
||||
phase,
|
||||
}));
|
||||
for (let i = 0; i < arr.length; i += 1) {
|
||||
const phaseIdx = i % AGENT_DOT_PHASES.length;
|
||||
const step = out[phaseIdx];
|
||||
const st = dotStateOf(arr[i]);
|
||||
if (st === 'waiting') continue; // 沿用上一轮该相位状态
|
||||
if (step.state === 'error') continue; // error 不回退
|
||||
if (st === 'running') {
|
||||
step.state = 'running';
|
||||
} else if (step.state === 'waiting') {
|
||||
step.state = st;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 找到(或创建)生信消息的派生 pipeline 块并返回之 */
|
||||
export function ensureBioflowDotBlock(aMsg) {
|
||||
let blk = (aMsg.blocks || []).find((b) => b && b.type === 'pipeline' && b.derived);
|
||||
if (!blk) {
|
||||
blk = {
|
||||
type: 'pipeline',
|
||||
derived: true,
|
||||
status: 'running',
|
||||
steps: bioflowDotStepsFromStages([]),
|
||||
};
|
||||
aMsg.blocks.push(blk);
|
||||
}
|
||||
return blk;
|
||||
}
|
||||
|
||||
/** 用进度块 stages 重算派生 pipeline 块(实时事件与快照回放共用) */
|
||||
export function syncBioflowDotBlock(aMsg) {
|
||||
if (!aMsg || !Array.isArray(aMsg.blocks)) return null;
|
||||
const prog = aMsg.blocks.find((b) => b && b.type === 'bioflow_progress');
|
||||
const blk = ensureBioflowDotBlock(aMsg);
|
||||
blk.steps = bioflowDotStepsFromStages(prog && Array.isArray(prog.stages) ? prog.stages : []);
|
||||
return blk;
|
||||
}
|
||||
|
||||
@@ -226,6 +306,7 @@ export function patchBioflowEvent(aMsg, ev) {
|
||||
blk.autoContinue = !!data.auto_continue;
|
||||
blk.stages = (Array.isArray(data.stages) ? data.stages : [])
|
||||
.map((s) => progressStage(s.stage || s, s.status || 'waiting', s));
|
||||
syncBioflowDotBlock(aMsg);
|
||||
return blk;
|
||||
}
|
||||
if (type === 'snapshot') {
|
||||
@@ -272,6 +353,7 @@ export function patchBioflowEvent(aMsg, ev) {
|
||||
if (!blk.jobStatus || blk.jobStatus === 'created') {
|
||||
if (blk.stages.some((s) => s.status === 'running')) blk.jobStatus = 'running';
|
||||
}
|
||||
syncBioflowDotBlock(aMsg);
|
||||
return blk;
|
||||
}
|
||||
|
||||
@@ -1224,6 +1306,13 @@ export const useChatStore = defineStore('chat', {
|
||||
} else if (type === 'error') {
|
||||
// 生信服务错误帧(统一信封形态):标注可重试
|
||||
endTask();
|
||||
// 派生点收口:当前运行相位置 error(防点永久 running)
|
||||
const dotBlk = (aMsg.blocks || []).find((b) => b && b.type === 'pipeline' && b.derived);
|
||||
if (dotBlk) {
|
||||
for (const s of dotBlk.steps || []) {
|
||||
if (s.state === 'running') s.state = 'error';
|
||||
}
|
||||
}
|
||||
const code = (data && data.code) || 'STREAM_ERROR';
|
||||
const message = (data && data.message) || BF.bioflowRouteError;
|
||||
updateLocal(() => {
|
||||
@@ -2067,7 +2156,9 @@ export const useChatStore = defineStore('chat', {
|
||||
if (b.type === 'pipeline' || b.type === 'stream') continue;
|
||||
out.push({ ...b });
|
||||
}
|
||||
return out;
|
||||
// 派生 pipeline 块(derived:true)是布局消费的瞬态镜像,不落库
|
||||
// (后端 Block union 无此类型;历史恢复不生信点,与现状一致)
|
||||
return out.filter((b) => !(b.type === 'pipeline' && b.derived));
|
||||
},
|
||||
|
||||
async _persistFinal(aMsg, sessionId, status, { raw } = {}) {
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
// 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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,174 @@
|
||||
// tests/r3debug/statusBarDots.test.js —— 底部状态栏 4 个 agent 点端到端渲染
|
||||
// 缺陷:主界面底部状态栏左 4 个点在对话过程中不随相位点亮。
|
||||
// 修复:StatusBar 读取 chat.currentMessage 的 pipeline 块;生信对话无原生 pipeline
|
||||
// 块,由 chat.js 派生 derived pipeline 块(由 bioflow_progress.stages 镜像)。
|
||||
// 本测试真实挂载 StatusBar(health store mock),在 pinia 写入 chat 流上下文,
|
||||
// 断言 4 个点的 class 随数据点亮。
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
vi.mock('../../src/stores/health.js', () => ({
|
||||
useHealthStore: () => ({
|
||||
qaVersion: '1.1.73-test',
|
||||
sessionCount: 3,
|
||||
unknown: false,
|
||||
qaOk: true,
|
||||
neo4jOk: true,
|
||||
extractionOk: true,
|
||||
}),
|
||||
}));
|
||||
|
||||
import StatusBar from '../../src/components/layout/StatusBar.vue';
|
||||
import { useChatStore } from '../../src/stores/chat.js';
|
||||
|
||||
const PHASES = ['planning', 'retrieval', 'evidence', 'generation'];
|
||||
|
||||
function step(phase, state) {
|
||||
return { name: phase, state, desc: '', phase };
|
||||
}
|
||||
|
||||
function dotClasses(wrapper) {
|
||||
return wrapper.findAll('.agent-dot-item .ad')
|
||||
.map((d) => d.classes().filter((c) => c !== 'ad').join(','));
|
||||
}
|
||||
|
||||
function setCurrentMsg(chat, aMsg) {
|
||||
chat.sessionId = 's-dots';
|
||||
chat.messages = [aMsg];
|
||||
chat.sessionMsgs['s-dots'] = [aMsg];
|
||||
chat.streamCtx['s-dots'] = {
|
||||
sessionId: 's-dots',
|
||||
msgList: [aMsg],
|
||||
controller: null,
|
||||
completedOnce: false,
|
||||
events: [],
|
||||
aMsg,
|
||||
checkpointTimer: null,
|
||||
lastCheckpointAt: 0,
|
||||
reveal: null,
|
||||
abortAnn: null,
|
||||
};
|
||||
}
|
||||
|
||||
describe('StatusBar 4 个 agent 点', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
});
|
||||
|
||||
it('QA 流式:planning running → 点1 亮 running,其余空', async () => {
|
||||
const chat = useChatStore();
|
||||
const aMsg = {
|
||||
id: 'm1', role: 'assistant', kind: 'qa', status: 'streaming',
|
||||
blocks: [{
|
||||
type: 'pipeline', status: 'running',
|
||||
steps: PHASES.map((p) => step(p, p === 'planning' ? 'running' : 'waiting')),
|
||||
}],
|
||||
};
|
||||
setCurrentMsg(chat, aMsg);
|
||||
const wrapper = mount(StatusBar);
|
||||
await new Promise((r) => setTimeout(r, 30)); // onMounted 动态 import
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(dotClasses(wrapper)).toEqual(['running', '', '', '']);
|
||||
});
|
||||
|
||||
it('QA 流式:generation running(evidence 已 done)→ 点1/3 done、点4 running', async () => {
|
||||
const chat = useChatStore();
|
||||
const aMsg = {
|
||||
id: 'm2', role: 'assistant', kind: 'qa', status: 'streaming',
|
||||
blocks: [{
|
||||
type: 'pipeline', status: 'running',
|
||||
steps: PHASES.map((p) => step(p, p === 'generation' ? 'running' : (p === 'evidence' || p === 'planning' ? 'done' : 'waiting'))),
|
||||
}],
|
||||
};
|
||||
setCurrentMsg(chat, aMsg);
|
||||
const wrapper = mount(StatusBar);
|
||||
await new Promise((r) => setTimeout(r, 30));
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(dotClasses(wrapper)).toEqual(['done', '', 'done', 'running']);
|
||||
});
|
||||
|
||||
it('生信对话:bioflow_progress + 派生 pipeline 块 → 点随镜像点亮(本缺陷修复)', async () => {
|
||||
const chat = useChatStore();
|
||||
// 模拟 sendBioflow 消息:path 块 + 进度块(stages 3 阶段:第1 running) + 派生块
|
||||
const aMsg = {
|
||||
id: 'm3', role: 'assistant', kind: 'bioflow', status: 'streaming',
|
||||
blocks: [
|
||||
{ type: 'bioflow_path', status: 'done', candidates: [{ stages: ['a', 'b', 'c'] }], selected: 0 },
|
||||
{
|
||||
type: 'bioflow_progress', jobId: 'j1', jobStatus: 'running',
|
||||
stages: [
|
||||
{ stage: 'a', status: 'running' },
|
||||
{ stage: 'b', status: 'waiting' },
|
||||
{ stage: 'c', status: 'waiting' },
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'pipeline', derived: true, status: 'running',
|
||||
steps: PHASES.map((p) => step(p, p === 'planning' ? 'running' : 'waiting')),
|
||||
},
|
||||
],
|
||||
};
|
||||
setCurrentMsg(chat, aMsg);
|
||||
const wrapper = mount(StatusBar);
|
||||
await new Promise((r) => setTimeout(r, 30));
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(dotClasses(wrapper)).toEqual(['running', '', '', '']);
|
||||
});
|
||||
|
||||
it('生信对话:历史消息无派生块(落库过滤)→ 点空态不报错', async () => {
|
||||
const chat = useChatStore();
|
||||
const aMsg = {
|
||||
id: 'm4', role: 'assistant', kind: 'bioflow', status: 'done',
|
||||
blocks: [{ type: 'bioflow_path', status: 'done', candidates: [] }],
|
||||
};
|
||||
setCurrentMsg(chat, aMsg);
|
||||
const wrapper = mount(StatusBar);
|
||||
await new Promise((r) => setTimeout(r, 30));
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(dotClasses(wrapper)).toEqual(['', '', '', '']);
|
||||
});
|
||||
|
||||
it('无当前消息(空闲)→ 4 点全空态', async () => {
|
||||
const chat = useChatStore();
|
||||
chat.sessionId = 's-dots';
|
||||
chat.streamCtx['s-dots'] = null;
|
||||
const wrapper = mount(StatusBar);
|
||||
await new Promise((r) => setTimeout(r, 30));
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(dotClasses(wrapper)).toEqual(['', '', '', '']);
|
||||
});
|
||||
|
||||
it('真实时序:先 mount(挂载完成)后流到达 → computed 响应式点亮', async () => {
|
||||
const chat = useChatStore();
|
||||
// 1. 先 mount,onMounted 动态 import 完成(与真实 app 启动时序一致)
|
||||
const wrapper = mount(StatusBar);
|
||||
await new Promise((r) => setTimeout(r, 30));
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(dotClasses(wrapper)).toEqual(['', '', '', '']);
|
||||
// 2. 流在 mount 之后才到达(用户发送消息)
|
||||
const aMsg = {
|
||||
id: 'm5', role: 'assistant', kind: 'qa', status: 'streaming',
|
||||
blocks: [{
|
||||
type: 'pipeline', status: 'running',
|
||||
steps: PHASES.map((p) => step(p, p === 'planning' ? 'running' : 'waiting')),
|
||||
}],
|
||||
};
|
||||
setCurrentMsg(chat, aMsg);
|
||||
await wrapper.vm.$nextTick();
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
expect(dotClasses(wrapper)).toEqual(['running', '', '', '']);
|
||||
// 3. 相位推进:经 store 响应式代理调真实 action(_advancePipeline),
|
||||
// 与真实 SSE 链同路径(直接改 raw 对象不会触发渲染——chat.js 响应式断链陷阱)
|
||||
const proxyMsg = chat.streamCtx['s-dots'].aMsg;
|
||||
chat._advancePipeline(proxyMsg, 'retrieval', '多路检索');
|
||||
await wrapper.vm.$nextTick();
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
expect(dotClasses(wrapper)).toEqual(['done', 'running', '', '']);
|
||||
// 4. 终态:done 相位全置 done
|
||||
chat._advancePipeline(proxyMsg, 'done', '检索完成');
|
||||
await wrapper.vm.$nextTick();
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
expect(dotClasses(wrapper)).toEqual(['done', 'done', 'done', 'done']);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user