Files
PlantAI-rag-new/tests/frontend/unit/b3/bench.store.test.js
adminandClaude Fable 5 53069d4940 fix(ui): 复测缺陷 r3-07-01/r3-52-02 + b3 测试债
- r3-07-01 「跳过」无效:补齐事件转发链 MessageList(新增
  skip-reveal emit)→ ChatPanel(@skip-reveal 监听)→ store.skipReveal();
  新增 skipReveal.test.js 三层断言(点击定格终态/组件转发/ChatPanel 监听)
- r3-52-02 终态文案残留:docgen 轮询 tick 的 done 分支把 generation 步
  desc 还原 DOCGEN_STEPS 默认文案「按大纲逐节生成正文」;docgenPoll
  done 用例改两阶段 mock(running 进度文案 → done 还原)复现原缺陷
- 测试债:b3 bench.store.test.js 删除已下线 sessionSourceItems 引用,
  按 #27 修复后语义改断言(后端去重结果原样透出)

自测:r3debug 32 项 + 全套 373 项 + private_docs 23 项全绿;
playwright 实测(mock SSE/docgen 202):跳过按钮点击后全文定格、
skip-btn/cursor 消失;docgen done 后第 4 步 desc 还原默认文案;无 JS 错误。
未 push。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-10 18:01:01 +08:00

179 lines
6.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// bench.store.test.js —— B3 单测:bench store
// 覆盖:tab 切换、相位筛选、轮询启停(fake timers)、sources 两种来源切换、会话级去重。
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { createPinia, setActivePinia, getActivePinia } from 'pinia';
import { useBenchStore, normalizeStageEvent, __setHttpGet } from '../../../../ui/src/stores/bench.js';
const extractionApi = () => import('../../../../ui/src/api/extraction.js');
// http.js(B1 归属)尚未交付:用注入式 get 模拟统一信封,不依赖文件存在
const httpGet = vi.fn();
__setHttpGet(httpGet);
vi.mock('../../../../ui/src/api/extraction.js', () => ({
listJobs: vi.fn(),
getJob: vi.fn(),
uploadPaper: vi.fn(),
getRunLog: vi.fn(),
getProcessingStatus: vi.fn(),
getSummary: vi.fn(),
}));
vi.mock('../../../../ui/src/api/kg.js', () => ({
kgStatus: vi.fn(),
kgOverview: vi.fn(),
searchEntities: vi.fn(),
getEntity: vi.fn(),
getNeighborhood: vi.fn(),
}));
const stageEvent = (stage, phase, label, details = {}, at = '2026-08-25T09:41:00Z') => ({
stage,
phase,
label,
details,
at,
});
const jobs = [
{ job_id: 'j1', title: 'a.pdf', status: 'completed', stage: 'completed', progress_pct: 100 },
{ job_id: 'j2', title: 'b.pdf', status: 'running', stage: 'llm', progress_pct: 75 },
{ job_id: 'j3', title: 'c.pdf', status: 'failed', stage: 'failed', progress_pct: null },
{ job_id: 'j4', title: 'd.pdf', status: 'queued', stage: 'queued', progress_pct: 5 },
];
describe('bench store', () => {
let store;
beforeEach(() => {
setActivePinia(createPinia());
store = useBenchStore();
vi.clearAllMocks();
});
afterEach(() => {
store.stopPolling();
});
it('tab 切换:默认 retrieval;切到 extract 会暂停轮询', () => {
expect(store.activeTab).toBe('retrieval');
store.pollPaused = false;
store.setActiveTab('extract');
expect(store.activeTab).toBe('extract');
store.pollPaused = true; // 模拟曾启动
store.setActiveTab('sources');
expect(store.pollPaused).toBe(true);
});
it('相位筛选:默认 all,切到 retrieval 后 filteredStageEvents 仅含检索相位', () => {
store.recordStageEvent(stageEvent('understanding_started', 'planning', '开始理解问题'));
store.recordStageEvent(stageEvent('concepts_found', 'retrieval', '发现相关概念'));
store.recordStageEvent(stageEvent('evidence_accepted', 'evidence', '证据已采纳'));
expect(store.filteredStageEvents).toHaveLength(3);
store.setPhaseFilter('retrieval');
expect(store.filteredStageEvents).toHaveLength(1);
expect(store.filteredStageEvents[0].stage).toBe('concepts_found');
});
it('normalizeStageEvent:字段缺失时兜底(phase 默认 retrieval)', () => {
const n = normalizeStageEvent({ stage: 'x' });
expect(n.phase).toBe('retrieval');
expect(n.label).toBe('x');
expect(n.details).toEqual({});
expect(normalizeStageEvent(null)).toBeNull();
});
it('轮询启停(fake timers):有 running 任务且 extract 激活才启动;终态后停止', async () => {
vi.useFakeTimers();
const { listJobs } = await extractionApi();
listJobs.mockResolvedValue({ jobs, count: 4 });
store.setActiveTab('extract');
await store.loadJobs({ force: true });
expect(store.hasActiveJobs).toBe(true);
const started = store.startPolling();
expect(started).toBe(true);
expect(store.isPolling()).toBe(true);
// 模拟一轮轮询返回全部终态 → 不再排下一轮
listJobs.mockResolvedValue({
jobs: jobs.map((j) => ({ ...j, status: 'completed', progress_pct: 100 })),
count: 4,
});
await vi.runOnlyPendingTimersAsync();
expect(store.isPolling()).toBe(false);
vi.useRealTimers();
});
it('轮询条件:tab 非 extract 或 pollPaused 时不启动', async () => {
vi.useFakeTimers();
const { listJobs } = await extractionApi();
listJobs.mockResolvedValue({ jobs, count: 4 });
// 未激活 extract tab
await store.loadJobs({ force: true });
expect(store.startPolling()).toBe(false);
// 激活后启动成功
store.setActiveTab('extract');
expect(store.startPolling()).toBe(true);
// 暂停后再启动被拒
store.pausePolling();
expect(store.isPolling()).toBe(false);
expect(store.startPolling()).toBe(false);
vi.useRealTimers();
});
it('无 running/queued 任务时不空转(FR-EXTRACT-005)', async () => {
const { listJobs } = await extractionApi();
listJobs.mockResolvedValue({
jobs: jobs.filter((j) => j.status === 'completed' || j.status === 'failed'),
count: 2,
});
store.setActiveTab('extract');
await store.loadJobs({ force: true });
expect(store.hasActiveJobs).toBe(false);
expect(store.startPolling()).toBe(false);
});
it('sources 两种来源切换:current 取消息 sources 块,session 取聚合接口', async () => {
// 无 B2 chat store → current 为 null
expect(store.currentSources).toBeNull();
expect(store.sources).toBeNull();
// session 聚合(无活动会话 → null,不发请求)
store.setSourceScope('session');
expect(store.sources).toBeNull();
expect(httpGet).not.toHaveBeenCalled();
// 有活动会话时拉聚合
const sessionsStore = { activeId: 'sess-1' };
getActivePinia()._s.set('sessions', sessionsStore);
httpGet.mockResolvedValue({
session_id: 'sess-1',
total: 3,
sources: [
{ id: 's1', paper_id: 'PMID1', title: 't1', count: 2 },
{ id: 's2', paper_id: 'PMID1', title: 't1', count: 1 },
{ id: 's3', paper_id: 'PMID2', title: 't2', count: 1 },
],
});
await store.loadSessionSources();
expect(httpGet).toHaveBeenCalledWith('/api/sessions/sess-1/sources', undefined);
// 缺陷 #27:前端不再二次聚合——后端去重结果原样透出
expect(store.sources).toHaveLength(3);
expect(store.sources[0].src.paper_id).toBe('PMID1');
expect(store.sources[0].label).toBe(2); // 后端给的第一条 count
expect(store.sources[1].label).toBe(1);
expect(store.sources[2].label).toBe(1);
// 切回 current:仍为 null(无 B2)
store.setSourceScope('current');
expect(store.sources).toBeNull();
});
});