- 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>
179 lines
6.2 KiB
JavaScript
179 lines
6.2 KiB
JavaScript
// 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();
|
||
});
|
||
});
|