Files
PlantAI-rag-new/tests/frontend/unit/b1/health.test.js
T
jerryandClaude Fable 5 0875bb036c P0/M1: 四部分重构——frontend→ui、backend→sdk 改名 + 引用修复 + 骨架
- 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>
2026-08-31 12:56:47 +08:00

142 lines
4.8 KiB
JavaScript
Raw 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.
// 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);
});
});