- 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>
142 lines
4.8 KiB
JavaScript
142 lines
4.8 KiB
JavaScript
// health store 单测(B1):轮询与降级 getter(mock fetch)(FR-HEALTH-001..004)
|
||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||
import { createPinia, setActivePinia } from 'pinia';
|
||
import { useHealthStore } from '../../../../ui/src/stores/health.js';
|
||
|
||
// mock fetch(http.js 的 fetch 封装走全局 fetch)
|
||
const fetchMock = vi.fn();
|
||
vi.stubGlobal('fetch', fetchMock);
|
||
|
||
function jsonRes(payload, status = 200) {
|
||
return new Response(JSON.stringify(payload), {
|
||
status,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
});
|
||
}
|
||
|
||
const HEALTH_OK = {
|
||
status: 'ok',
|
||
version: '0.1.0',
|
||
upstreams: {
|
||
qa: { ok: true, version: '1.1.73-assertion-anchored' },
|
||
rag: { ok: true },
|
||
ragAlt: { ok: true },
|
||
neo4j: { ok: true, active: 'neo4j-plant-srt-pc2025' },
|
||
},
|
||
db: { ok: true, sessions: 12 },
|
||
};
|
||
|
||
// 本文件所有 start() 过的 store,afterEach 统一 stop(防止 interval 泄漏到下一个测试)
|
||
let startedStores = [];
|
||
|
||
describe('health store', () => {
|
||
beforeEach(() => {
|
||
fetchMock.mockReset();
|
||
setActivePinia(createPinia());
|
||
vi.useFakeTimers();
|
||
Object.defineProperty(document, 'hidden', { configurable: true, value: false });
|
||
startedStores = [];
|
||
});
|
||
afterEach(() => {
|
||
startedStores.forEach((s) => s.stop());
|
||
startedStores = [];
|
||
vi.useRealTimers();
|
||
});
|
||
|
||
/** 启动并登记 store */
|
||
function startStore() {
|
||
const store = useHealthStore();
|
||
store.start();
|
||
startedStores.push(store);
|
||
return store;
|
||
}
|
||
|
||
it('启动轮询:立即请求一次 health,数据入 store,getter 全绿', async () => {
|
||
fetchMock.mockResolvedValue(jsonRes(HEALTH_OK));
|
||
const store = startStore();
|
||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||
// 让初始 fetch 的 microtask 完成
|
||
await vi.advanceTimersByTimeAsync(0);
|
||
expect(store.data).toEqual(HEALTH_OK);
|
||
expect(store.error).toBeNull();
|
||
expect(store.qaOk).toBe(true);
|
||
expect(store.neo4jOk).toBe(true);
|
||
expect(store.extractionOk).toBe(true);
|
||
expect(store.sessionCount).toBe(12);
|
||
expect(store.unknown).toBe(false);
|
||
});
|
||
|
||
it('30s 间隔轮询;页面 hidden 时跳过', async () => {
|
||
fetchMock.mockResolvedValue(jsonRes(HEALTH_OK));
|
||
const store = startStore();
|
||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||
await vi.advanceTimersByTimeAsync(30000);
|
||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||
|
||
// hidden:跳过本次 tick
|
||
Object.defineProperty(document, 'hidden', { configurable: true, value: true });
|
||
await vi.advanceTimersByTimeAsync(30000);
|
||
expect(fetchMock).toHaveBeenCalledTimes(2);
|
||
|
||
// 恢复可见:继续轮询
|
||
Object.defineProperty(document, 'hidden', { configurable: true, value: false });
|
||
await vi.advanceTimersByTimeAsync(30000);
|
||
expect(fetchMock).toHaveBeenCalledTimes(3);
|
||
expect(store.polling).toBe(true);
|
||
});
|
||
|
||
it('降级 getter:QA 不可用时 qaOk=false(不弹窗,仅标记)', async () => {
|
||
const degraded = {
|
||
status: 'degraded',
|
||
upstreams: {
|
||
qa: { ok: false },
|
||
rag: { ok: true },
|
||
ragAlt: { ok: false },
|
||
neo4j: { ok: true },
|
||
},
|
||
db: { ok: true, sessions: 3 },
|
||
};
|
||
fetchMock.mockResolvedValue(jsonRes(degraded));
|
||
const store = startStore();
|
||
await vi.advanceTimersByTimeAsync(0);
|
||
expect(store.qaOk).toBe(false);
|
||
expect(store.neo4jOk).toBe(true);
|
||
expect(store.extractionOk).toBe(true); // 8000 在线
|
||
expect(store.bffHealthy).toBe(false); // status degraded
|
||
});
|
||
|
||
it('网络失败:error 置位、data 保留旧值、unknown=false(不弹窗)', async () => {
|
||
fetchMock.mockResolvedValueOnce(jsonRes(HEALTH_OK));
|
||
const store = startStore();
|
||
await vi.advanceTimersByTimeAsync(0);
|
||
expect(store.data).toEqual(HEALTH_OK);
|
||
|
||
fetchMock.mockRejectedValueOnce(new TypeError('Failed to fetch'));
|
||
await vi.advanceTimersByTimeAsync(30000);
|
||
expect(store.error).not.toBeNull();
|
||
expect(store.error.name).toBe('ApiError');
|
||
expect(store.data).toEqual(HEALTH_OK); // 旧值保留供降级展示
|
||
expect(store.unknown).toBe(false);
|
||
});
|
||
|
||
it('fetchQaInfo:存版本号;失败置 null 不抛', async () => {
|
||
fetchMock.mockResolvedValueOnce(jsonRes({ name: 'QA', version: '1.1.73-assertion-anchored' }));
|
||
const store = useHealthStore();
|
||
await store.fetchQaInfo();
|
||
expect(store.qaVersion).toBe('1.1.73-assertion-anchored');
|
||
|
||
fetchMock.mockRejectedValueOnce(new TypeError('Failed to fetch'));
|
||
await store.fetchQaInfo();
|
||
expect(store.qaInfo).toBeNull();
|
||
});
|
||
|
||
it('stop 后不再轮询', async () => {
|
||
fetchMock.mockResolvedValue(jsonRes(HEALTH_OK));
|
||
const store = startStore();
|
||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||
store.stop();
|
||
await vi.advanceTimersByTimeAsync(120000);
|
||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||
});
|
||
});
|