/** * SLOMonitor + HealthChecker 测试(electron/utils/slo.ts —— 此前零测试) * * SLOMonitor 契约: * 1. 滑动窗口内错误率统计(窗口外记录被清理/忽略) * 2. 吞吐量 = 窗口内请求数 / 窗口秒数 * 3. 延迟分位数 P50/P95/P99(空数据返回 0) * 4. 燃烧速率 = 错误率 / 错误预算;>1 触发 violated * 5. 低流量语义:窗口内少量请求也按真实比例计算 * 6. reset / getConfig * * HealthChecker 契约:database(DB ping / 文件存在)/ free_memory(>200MB)/ * memory_usage(heap < 512MB)三项检查聚合为 healthy。 */ import { describe, it, expect, vi, beforeEach, afterEach, beforeAll, afterAll } from 'vitest'; import { mkdtempSync, rmSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; // electron.app.getPath 指向一个真实存在的临时目录(checkFreeMemory 依赖) const mockState = vi.hoisted(() => ({ userDataDir: '' })); vi.mock('electron', async () => { const { mkdtempSync } = await import('fs'); const { tmpdir } = await import('os'); const { join } = await import('path'); mockState.userDataDir = mkdtempSync(join(tmpdir(), 'metona-slo-')); return { app: { getPath: () => mockState.userDataDir } }; }); import { SLOMonitor, HealthChecker } from '../slo'; describe('SLOMonitor — 基础统计', () => { let monitor: SLOMonitor; beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(1_000_000); monitor = new SLOMonitor({ windowMs: 60_000 }); }); afterEach(() => { vi.useRealTimers(); }); it('空监控器:errorRate=0、throughput=0、percentile=0、violated=false', () => { const status = monitor.getStatus(); expect(status.errorRate).toBe(0); expect(status.throughput).toBe(0); expect(status.avgLatencyMs).toBe(0); expect(status.percentiles).toEqual({ P50: 0, P95: 0, P99: 0 }); expect(status.burnRate).toBe(0); expect(status.violated).toBe(false); expect(status.totalRequests).toBe(0); expect(status.errorRequests).toBe(0); expect(status.target).toBe(0.999); }); it('全部成功:errorRate=0、violated=false、burnRate=0', () => { for (let i = 0; i < 10; i++) monitor.recordRequest(100, true); const status = monitor.getStatus(); expect(status.totalRequests).toBe(10); expect(status.errorRequests).toBe(0); expect(status.errorRate).toBe(0); expect(status.burnRate).toBe(0); expect(status.violated).toBe(false); }); it('全部失败:errorRate=1、burnRate=1/errorBudget、violated=true', () => { for (let i = 0; i < 5; i++) monitor.recordRequest(10, false); const status = monitor.getStatus(); expect(status.errorRate).toBe(1); expect(status.errorRequests).toBe(5); // errorBudget = 1 - 0.999 = 0.001 expect(status.errorBudget).toBeCloseTo(0.001); expect(status.burnRate).toBeCloseTo(1000); expect(status.violated).toBe(true); }); it('混合请求:errorRate = 错误数/总数', () => { monitor.recordRequest(10, true); monitor.recordRequest(20, true); monitor.recordRequest(30, false); monitor.recordRequest(40, true); const status = monitor.getStatus(); expect(status.totalRequests).toBe(4); expect(status.errorRate).toBeCloseTo(0.25); }); it('吞吐量 = 窗口内请求数 / 窗口秒数', () => { for (let i = 0; i < 30; i++) monitor.recordRequest(5, true); const status = monitor.getStatus(); expect(status.throughput).toBeCloseTo(30 / 60); // 0.5 req/s }); it('平均延迟 = 窗口内延迟总和 / 请求数', () => { monitor.recordRequest(100, true); monitor.recordRequest(300, true); monitor.recordRequest(200, true); expect(monitor.getStatus().avgLatencyMs).toBe(200); }); }); describe('SLOMonitor — 分位数', () => { let monitor: SLOMonitor; beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(1_000_000); monitor = new SLOMonitor({ windowMs: 60_000 }); }); afterEach(() => { vi.useRealTimers(); }); it('P50/P95/P99 边界:升序延迟的 ceil 索引取数', () => { // [1,2,3,4]:P50=2(idx1),P95=4(idx3),P99=4(idx3) for (const lat of [4, 1, 3, 2]) monitor.recordRequest(lat, true); const status = monitor.getStatus(); expect(status.percentiles).toEqual({ P50: 2, P95: 4, P99: 4 }); }); it('单元素:所有分位数均为该值', () => { monitor.recordRequest(77, true); expect(monitor.getStatus().percentiles).toEqual({ P50: 77, P95: 77, P99: 77 }); }); it('自定义分位数数组([0.5,0.9])生成对应 key', () => { const m = new SLOMonitor({ latencyPercentiles: [0.5, 0.9], windowMs: 60_000 }); m.recordRequest(1, true); m.recordRequest(2, true); m.recordRequest(3, true); m.recordRequest(4, true); m.recordRequest(5, true); const status = m.getStatus(); expect(status.percentiles).toEqual({ P50: 3, P90: 5 }); }); }); describe('SLOMonitor — 滑动窗口', () => { let monitor: SLOMonitor; beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(1_000_000); monitor = new SLOMonitor({ windowMs: 60_000 }); }); afterEach(() => { vi.useRealTimers(); }); it('窗口外旧记录被 getStatus 忽略(不参与统计)', () => { for (let i = 0; i < 3; i++) monitor.recordRequest(10, false); // 全部失败 // 推进 60 秒以上 —— 旧记录全部过期 vi.advanceTimersByTime(61_000); const status = monitor.getStatus(); expect(status.totalRequests).toBe(0); expect(status.errorRate).toBe(0); expect(status.violated).toBe(false); }); it('窗口内保留新记录:混合新旧后只统计新记录', () => { monitor.recordRequest(10, false); // 旧失败 vi.advanceTimersByTime(30_000); monitor.recordRequest(20, true); // 新成功(仍在窗口内) const status = monitor.getStatus(); expect(status.totalRequests).toBe(2); // 两条都还在窗口内 vi.advanceTimersByTime(30_001); // 第一条(30 秒前的)过期 const status2 = monitor.getStatus(); expect(status2.totalRequests).toBe(1); expect(status2.errorRate).toBe(0); }); it('recordRequest 内部清理:推进时间后旧记录被移除(内存不膨胀)', () => { for (let i = 0; i < 5; i++) monitor.recordRequest(1, true); vi.advanceTimersByTime(120_000); // 再次 recordRequest 触发 cleanup → 旧记录全清 monitor.recordRequest(2, true); expect(monitor.getStatus().totalRequests).toBe(1); }); it('窗口边界:恰在窗口起始点的记录被保留(>= 比较)', () => { monitor.recordRequest(1, true); vi.advanceTimersByTime(60_000); // 恰满窗口 const status = monitor.getStatus(); expect(status.totalRequests).toBe(1); }); it('低流量误报语义:少量请求也按真实比例(单条失败即 100% 错误率 → violated)', () => { // target 0.999 预算极低 —— 单条失败即触发 violated(设计语义:低流量不掩盖问题) monitor.recordRequest(5, false); const status = monitor.getStatus(); expect(status.errorRate).toBe(1); expect(status.violated).toBe(true); }); it('低流量不误报:单条成功请求 errorRate=0 不触发 violated', () => { monitor.recordRequest(5, true); const status = monitor.getStatus(); expect(status.errorRate).toBe(0); expect(status.violated).toBe(false); }); }); describe('SLOMonitor — 配置与重置', () => { it('自定义 target 影响错误预算与燃烧速率', () => { vi.useFakeTimers(); const m = new SLOMonitor({ target: 0.9, windowMs: 60_000 }); m.recordRequest(1, false); m.recordRequest(1, true); const status = m.getStatus(); expect(status.errorBudget).toBeCloseTo(0.1); expect(status.burnRate).toBeCloseTo(5); // 0.5 / 0.1 expect(status.violated).toBe(true); vi.useRealTimers(); }); it('目标 0.5 时 50% 错误率为边界(violated=false)', () => { vi.useFakeTimers(); const m = new SLOMonitor({ target: 0.5, windowMs: 60_000 }); m.recordRequest(1, true); m.recordRequest(1, false); const status = m.getStatus(); expect(status.errorRate).toBeCloseTo(0.5); expect(status.burnRate).toBeCloseTo(1); expect(status.violated).toBe(false); // burnRate > 1 才触发 vi.useRealTimers(); }); it('reset 清空全部记录', () => { vi.useFakeTimers(); const m = new SLOMonitor({ windowMs: 60_000 }); m.recordRequest(1, false); m.recordRequest(2, false); m.reset(); const status = m.getStatus(); expect(status.totalRequests).toBe(0); expect(status.errorRate).toBe(0); vi.useRealTimers(); }); it('getConfig 返回默认配置的副本(修改副本不影响实例)', () => { const m = new SLOMonitor(); const config = m.getConfig(); expect(config.target).toBe(0.999); expect(config.windowMs).toBe(5 * 60 * 1000); expect(config.latencyPercentiles).toEqual([0.5, 0.95, 0.99]); expect(config.latencyThresholdMs).toBe(5000); config.target = 0.5; expect(m.getConfig().target).toBe(0.999); }); it('getStatus 返回 timestamp 为 Date 实例', () => { vi.useFakeTimers(); vi.setSystemTime(1_234_567); const m = new SLOMonitor(); m.recordRequest(1, true); const status = m.getStatus(); expect(status.timestamp).toBeInstanceOf(Date); expect(status.timestamp.getTime()).toBe(1_234_567); vi.useRealTimers(); }); }); describe('HealthChecker — 健康检查聚合', () => { let dir: string; let db: any; // 模块级 ABI 探测(it.skipIf 在注册期求值,不能依赖 beforeAll) let dbAvailable = true; try { // eslint-disable-next-line @typescript-eslint/no-require-imports const Probe = require('better-sqlite3'); new Probe(':memory:').close(); } catch { dbAvailable = false; } beforeAll(() => { if (dbAvailable) { // eslint-disable-next-line @typescript-eslint/no-require-imports db = new (require('better-sqlite3'))(':memory:'); } }); afterAll(() => { try { db?.close(); } catch { /* ignore */ } }); beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'metona-health-')); }); afterEach(() => { try { rmSync(dir, { recursive: true, force: true }); } catch { /* ignore */ } }); it.skipIf(!dbAvailable)('database 检查:注入 DB 成功执行 ping → healthy', async () => { const checker = new HealthChecker(() => db); const report = await checker.check(); const dbCheck = report.checks.find((c) => c.name === 'database'); expect(dbCheck?.healthy).toBe(true); expect(typeof dbCheck?.latencyMs).toBe('number'); }); it('database 检查:DB ping 抛错 → unhealthy 且带错误信息', async () => { const broken = { prepare: () => { throw new Error('db closed'); }, }; const checker = new HealthChecker(() => broken as never); const report = await checker.check(); const dbCheck = report.checks.find((c) => c.name === 'database'); expect(dbCheck?.healthy).toBe(false); expect(dbCheck?.error).toContain('db closed'); }); it('database 检查:无 DB 注入但 dbPath 存在 → healthy(文件存在后备)', async () => { const filePath = join(dir, 'agent.db'); const { writeFileSync } = await import('fs'); writeFileSync(filePath, 'sqlite', 'utf-8'); const checker = new HealthChecker(undefined, filePath); const report = await checker.check(); const dbCheck = report.checks.find((c) => c.name === 'database'); expect(dbCheck?.healthy).toBe(true); }); it('database 检查:无 DB 且 dbPath 不存在 → unhealthy', async () => { const checker = new HealthChecker(undefined, join(dir, 'missing.db')); const report = await checker.check(); const dbCheck = report.checks.find((c) => c.name === 'database'); expect(dbCheck?.healthy).toBe(false); expect(dbCheck?.error).toBe('Database not available'); }); it('free_memory 检查:userData 存在 → 返回布尔健康态(v0.7.4 降级,不断言 true)', async () => { // 回归修复: 原断言 healthy===true 依赖测试机空闲内存 >200MB,低内存 CI 假失败。 // 降级为断言"检查存在 + healthy 为布尔"。 const checker = new HealthChecker(); const report = await checker.check(); const memCheck = report.checks.find((c) => c.name === 'free_memory'); expect(memCheck).toBeDefined(); expect(typeof memCheck?.healthy).toBe('boolean'); }); it('memory_usage 检查:返回布尔健康态(v0.7.4 降级,不断言 true)', async () => { const checker = new HealthChecker(); const report = await checker.check(); const usageCheck = report.checks.find((c) => c.name === 'memory_usage'); expect(usageCheck).toBeDefined(); expect(typeof usageCheck?.healthy).toBe('boolean'); }); it('check 返回三项检查且 healthy 为聚合结果(全健康 → true)', async () => { const checker = new HealthChecker(); const report = await checker.check(); expect(report.checks.map((c) => c.name)).toEqual(['database', 'free_memory', 'memory_usage']); // 无 DB 无路径 → database 不健康 → 聚合 unhealthy expect(report.healthy).toBe(false); expect(report.timestamp).toBeInstanceOf(Date); }); it('所有检查健康时聚合 healthy=true', async () => { const filePath = join(dir, 'agent.db'); const { writeFileSync } = await import('fs'); writeFileSync(filePath, 'sqlite', 'utf-8'); const checker = new HealthChecker(undefined, filePath); const report = await checker.check(); expect(report.healthy).toBe(true); }); });