- git mv 保留历史:ui/(Vue3+Vite)、sdk/(BFF 中间层)
- 路径语义修正:db_path/figures_cache 改相对 sdk/(BFF cwd),
历史库 sdk/backend/data 迁移至 sdk/data/(会话/图片缓存无损)
- 引用修复 ~15 处:config.py、deploy/*.sh、vitest/playwright 配置、
单测导入路径、docs、README、gitignore、.env.example
- 骨架:agent/{qa,methodology,legacy-rag,vendor}、knowledge/manifest.yaml、
tooling/{compile,kb,crypto,assemble,audit,tools,scripts} + README
- 门禁验证:sdk pytest 125 通过、ui vitest 158 通过、
生产 BFF 脚本重启四上游全绿 + 静态托管 + 会话库无损
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
177 lines
6.1 KiB
JavaScript
177 lines
6.1 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);
|
||
expect(store.sessionSourceItems).toHaveLength(2); // paper_id 去重
|
||
const byPaper = store.sessionSourceItems.find((s) => s.paper_id === 'PMID1');
|
||
expect(byPaper.count).toBe(3); // 累计出现次数
|
||
expect(store.sources).toHaveLength(2);
|
||
|
||
// 切回 current:仍为 null(无 B2)
|
||
store.setSourceScope('current');
|
||
expect(store.sources).toBeNull();
|
||
});
|
||
});
|