import { describe, expect, test } from "bun:test"; import { analyzeIncidentSequence, buildTrend, calculateAvailability, calculateCurrentStreak, calculatePercentile, type MetricCheckpoint, resolveAutoBucket, } from "../../src/server/metrics"; describe("后端指标计算", () => { test("可用率无数据返回 0,并保留两位精度", () => { expect(calculateAvailability(0, 0)).toBe(0); expect(calculateAvailability(2, 3)).toBe(66.67); }); test("百分位按 ceil(count * N / 100) - 1 取值", () => { const durations = Array.from({ length: 100 }, (_, index) => index + 1); expect(calculatePercentile([], 95)).toBeNull(); expect(calculatePercentile([40, 10, 30, 20], 95)).toBe(40); expect(calculatePercentile(durations, 95)).toBe(95); expect(calculatePercentile(durations, 99)).toBe(99); }); test("无检查数据时故障分析返回空口径", () => { const result = analyzeIncidentSequence([], "2025-01-01T00:00:00.000Z", "2025-01-01T01:00:00.000Z"); expect(result).toEqual({ incidentCount: 0, longestOutage: null, mttr: null }); expect(calculateCurrentStreak([])).toBeNull(); }); test("窗口起始即故障计入 incident 和最长故障,但不计入 MTTR", () => { const result = analyzeIncidentSequence( [ checkpoint("2025-01-01T00:05:00.000Z", false), checkpoint("2025-01-01T00:10:00.000Z", false), checkpoint("2025-01-01T00:20:00.000Z", true), ], "2025-01-01T00:00:00.000Z", "2025-01-01T01:00:00.000Z", ); expect(result.incidentCount).toBe(1); expect(result.longestOutage).toBe(20 * 60 * 1000); expect(result.mttr).toBeNull(); }); test("未恢复故障计算到窗口结束且不计入 MTTR", () => { const result = analyzeIncidentSequence( [checkpoint("2025-01-01T00:05:00.000Z", true), checkpoint("2025-01-01T00:20:00.000Z", false)], "2025-01-01T00:00:00.000Z", "2025-01-01T01:00:00.000Z", ); expect(result.incidentCount).toBe(1); expect(result.longestOutage).toBe(40 * 60 * 1000); expect(result.mttr).toBeNull(); }); test("连续异常只计一次 incident,恢复后纳入 MTTR", () => { const result = analyzeIncidentSequence( [ checkpoint("2025-01-01T00:00:00.000Z", true), checkpoint("2025-01-01T00:05:00.000Z", false), checkpoint("2025-01-01T00:10:00.000Z", false), checkpoint("2025-01-01T00:20:00.000Z", true), ], "2025-01-01T00:00:00.000Z", "2025-01-01T01:00:00.000Z", ); expect(result.incidentCount).toBe(1); expect(result.longestOutage).toBe(15 * 60 * 1000); expect(result.mttr).toBe(15 * 60 * 1000); }); test("连续状态支持 capped 标记", () => { expect( calculateCurrentStreak( [ checkpoint("2025-01-01T00:00:00.000Z", true), checkpoint("2025-01-01T00:01:00.000Z", false), checkpoint("2025-01-01T00:02:00.000Z", false), ], 2, ), ).toEqual({ capped: true, count: 2, up: false }); }); test("UTC 小时趋势分桶返回 up/down 和延迟范围", () => { const checkpoints = [ checkpoint("2025-01-01T00:10:00.000Z", true, 100), checkpoint("2025-01-01T00:40:00.000Z", false, null), checkpoint("2025-01-01T01:05:00.000Z", true, 300), ]; const trend = buildTrend(checkpoints, "2025-01-01T00:00:00.000Z", "2025-01-01T01:59:59.999Z", "1h"); expect(trend).toEqual([ { availability: 50, avgDurationMs: 100, bucketEnd: "2025-01-01T01:00:00.000Z", bucketStart: "2025-01-01T00:00:00.000Z", downChecks: 1, maxDurationMs: 100, minDurationMs: 100, p95DurationMs: 100, totalChecks: 2, upChecks: 1, }, { availability: 100, avgDurationMs: 300, bucketEnd: "2025-01-01T01:59:59.999Z", bucketStart: "2025-01-01T01:00:00.000Z", downChecks: 0, maxDurationMs: 300, minDurationMs: 300, p95DurationMs: 300, totalChecks: 1, upChecks: 1, }, ]); }); test("resolveAutoBucket 按窗口大小选择合适桶", () => { expect(resolveAutoBucket(30_000, 7 * 24 * 60 * 60 * 1000)).toBe("1h"); expect(resolveAutoBucket(30_000, 60 * 60 * 1000)).toBe("30s"); expect(resolveAutoBucket(30_000, 24 * 60 * 60 * 1000)).toBe("15m"); }); }); function checkpoint(timestamp: string, matched: boolean, durationMs: null | number = null): MetricCheckpoint { return { durationMs, matched, timestamp }; }