140 lines
4.5 KiB
TypeScript
140 lines
4.5 KiB
TypeScript
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 };
|
||
}
|