1
0

feat: 新增本机 CPU checker

- 新增 type: cpu checker,基于 os.cpus() 两次快照计算 CPU 使用率
- 配置项:sampleDuration(默认 1s)、includePerCore(默认 false)
- expect 字段:usagePercent、idlePercent、maxCoreUsagePercent、minCoreUsagePercent、durationMs
- idlePercent 与 usagePercent 互补恒等于 100,百分比范围 0-100
- logicalCoreCount 仅输出到 observation,不作为 expect 字段
- 不暴露 userPercent / systemPercent
- 语义校验禁止 sampleDuration >= timeout
- 支持 AbortSignal 超时取消
- 完整测试覆盖:schema、validate、normalize、resolve、calculate、execute、expect、config-loader
- 新增用户文档 docs/user/checkers/cpu.md
- 更新 checker 索引、配置类型列表、示例配置和 schema
This commit is contained in:
2026-05-26 22:34:57 +08:00
parent f38286d74d
commit c2dcfab80c
22 changed files with 1839 additions and 3 deletions

View File

@@ -2270,6 +2270,82 @@ targets:
);
});
test("解析最简 cpu 配置", async () => {
const configPath = join(tempDir, "minimal-cpu.yaml");
await writeFile(
configPath,
`targets:
- id: "local-cpu"
type: cpu
cpu: {}
`,
);
const config = await loadConfig(configPath);
expect(config.targets).toHaveLength(1);
const t = config.targets[0]! as Record<string, unknown>;
expect(t["type"]).toBe("cpu");
expect((t["cpu"] as Record<string, unknown>)["sampleDurationMs"]).toBe(1000);
expect((t["cpu"] as Record<string, unknown>)["includePerCore"]).toBe(false);
expect(t["group"]).toBe("default");
expect(t["intervalMs"]).toBe(30000);
expect(t["timeoutMs"]).toBe(10000);
});
test("解析 cpu expect 配置", async () => {
const configPath = join(tempDir, "cpu-expect.yaml");
await writeFile(
configPath,
`targets:
- id: "local-cpu"
type: cpu
cpu:
sampleDuration: "2s"
includePerCore: true
expect:
usagePercent: { lte: 85 }
idlePercent: { gte: 15 }
maxCoreUsagePercent: { lte: 95 }
durationMs: { lte: 3000 }
`,
);
const config = await loadConfig(configPath);
expect(config.targets).toHaveLength(1);
const t = config.targets[0]! as Record<string, unknown>;
expect((t["cpu"] as Record<string, unknown>)["sampleDurationMs"]).toBe(2000);
expect((t["cpu"] as Record<string, unknown>)["includePerCore"]).toBe(true);
expect((t["expect"] as Record<string, unknown>)["usagePercent"]).toEqual({ lte: 85 });
});
test("cpu expect 未知字段抛出错误", async () => {
await expectConfigError(
"cpu-unknown-expect.yaml",
`targets:
- id: "local-cpu"
type: cpu
cpu: {}
expect:
logicalCoreCount: { gte: 4 }
`,
"expect.logicalCoreCount 是未知字段",
);
});
test("cpu sampleDuration >= timeout 抛出错误", async () => {
await expectConfigError(
"cpu-sample-too-long.yaml",
`targets:
- id: "local-cpu"
type: cpu
timeout: "1s"
cpu:
sampleDuration: "5s"
`,
"sampleDuration 必须小于 timeout",
);
});
describe("logging 配置", () => {
test("logging 全部缺省时使用默认值", async () => {
const configPath = join(tempDir, "logging-default.yaml");

View File

@@ -0,0 +1,112 @@
import { describe, expect, test } from "bun:test";
import type { CpuCoreSnapshot } from "../../../../../src/server/checker/runner/cpu/types";
import { calculateCpuStats } from "../../../../../src/server/checker/runner/cpu/calculate";
function makeCore(user: number, nice: number, sys: number, idle: number, irq: number): CpuCoreSnapshot {
return { times: { idle, irq, nice, sys, user } };
}
describe("calculateCpuStats", () => {
test("单核心完全空闲", () => {
const before = [makeCore(0, 0, 0, 100, 0)];
const after = [makeCore(0, 0, 0, 200, 0)];
const stats = calculateCpuStats(before, after);
expect(stats.usagePercent).toBe(0);
expect(stats.idlePercent).toBe(100);
expect(stats.maxCoreUsagePercent).toBe(0);
expect(stats.minCoreUsagePercent).toBe(0);
expect(stats.logicalCoreCount).toBe(1);
});
test("单核心完全忙碌idle 不变)", () => {
const before = [makeCore(100, 0, 0, 100, 0)];
const after = [makeCore(200, 0, 0, 100, 0)];
// idle delta = 0, total delta = 100
// idlePercent = 0, usagePercent = 100
const stats = calculateCpuStats(before, after);
expect(stats.usagePercent).toBe(100);
expect(stats.idlePercent).toBe(0);
});
test("单核心部分使用", () => {
const before = [makeCore(100, 0, 0, 900, 0)];
const after = [makeCore(150, 0, 0, 950, 0)];
// idle delta = 50, total delta = 100
// idlePercent = 50, usagePercent = 50
const stats = calculateCpuStats(before, after);
expect(stats.usagePercent).toBe(50);
expect(stats.idlePercent).toBe(50);
});
test("多核心加权平均", () => {
// 核心 0: idle delta = 200, total delta = 1000 -> 80% usage
// 核心 1: idle delta = 800, total delta = 1000 -> 20% usage
const before = [makeCore(0, 0, 0, 1000, 0), makeCore(0, 0, 0, 1000, 0)];
const after = [makeCore(800, 0, 0, 1200, 0), makeCore(200, 0, 0, 1800, 0)];
const stats = calculateCpuStats(before, after);
// 总 idle = 200+800=1000, 总 delta = 1000+1000=2000
// idlePercent = 1000/2000*100 = 50
// usagePercent = 100 - 50 = 50
expect(stats.idlePercent).toBe(50);
expect(stats.usagePercent).toBe(50);
expect(stats.maxCoreUsagePercent).toBe(80);
expect(stats.minCoreUsagePercent).toBe(20);
expect(stats.logicalCoreCount).toBe(2);
expect(stats.perCoreUsagePercent).toEqual([80, 20]);
});
test("四核心各不相同", () => {
const bf = [
makeCore(1000, 0, 0, 9000, 0), // core 0 baseline
makeCore(1000, 0, 0, 9000, 0), // core 1
makeCore(1000, 0, 0, 9000, 0), // core 2
makeCore(1000, 0, 0, 9000, 0), // core 3
];
const af = [
makeCore(1900, 0, 0, 9100, 0), // delta: user=900, idle=100, total=1000 -> 90% usage, 10% idle
makeCore(1500, 0, 0, 9500, 0), // delta: user=500, idle=500, total=1000 -> 50% usage
makeCore(1200, 0, 0, 9800, 0), // delta: user=200, idle=800, total=1000 -> 20% usage
makeCore(1010, 0, 0, 9990, 0), // delta: user=10, idle=990, total=1000 -> 1% usage
];
const stats = calculateCpuStats(bf, af);
// 总 idle = 100+500+800+990 = 2390, 总 delta = 4000
// idlePercent = 2390/4000*100 = 59.75 -> 59.8
expect(stats.idlePercent).toBe(59.8);
expect(stats.usagePercent).toBe(40.2);
expect(stats.maxCoreUsagePercent).toBe(90);
expect(stats.minCoreUsagePercent).toBe(1);
expect(stats.perCoreUsagePercent).toEqual([90, 50, 20, 1]);
expect(stats.logicalCoreCount).toBe(4);
});
test("delta 为 0 时返回 0", () => {
const before = [makeCore(100, 0, 0, 100, 0)];
const after = [makeCore(100, 0, 0, 100, 0)];
const stats = calculateCpuStats(before, after);
expect(stats.usagePercent).toBe(0);
expect(stats.idlePercent).toBe(0);
});
test("保留 1 位小数", () => {
// 总 idle = 333, 总 delta = 1000 -> idlePercent = 33.3
const before = [makeCore(0, 0, 0, 1000, 0)];
const after = [makeCore(667, 0, 0, 1333, 0)];
const stats = calculateCpuStats(before, after);
// idle delta = 333, total delta = 1000
expect(stats.idlePercent).toBe(33.3);
expect(stats.usagePercent).toBe(66.7);
});
test("nice 和 irq 计入 total 但不影响 idle", () => {
const bf = [makeCore(0, 0, 0, 0, 0)];
const af = [makeCore(300, 100, 100, 400, 100)];
// total delta = 300+100+100+400+100 = 1000
// idle delta = 400
// idlePercent = 400/1000*100 = 40
const stats = calculateCpuStats(bf, af);
expect(stats.idlePercent).toBe(40);
expect(stats.usagePercent).toBe(60);
});
});

View File

@@ -0,0 +1,255 @@
import { describe, expect, test } from "bun:test";
import type { SnapshotReader } from "../../../../../src/server/checker/runner/cpu/execute";
import type { CpuCoreSnapshot } from "../../../../../src/server/checker/runner/cpu/types";
import type { RawTargetConfig } from "../../../../../src/server/checker/types";
import { CpuChecker } from "../../../../../src/server/checker/runner/cpu/execute";
function makeCore(user: number, nice: number, sys: number, idle: number, irq: number): CpuCoreSnapshot {
return { times: { idle, irq, nice, sys, user } };
}
function makeResolveContext(
overrides: Partial<{ configDir: string; defaultIntervalMs: number; defaultTimeoutMs: number }> = {},
) {
return {
configDir: "/test",
defaultIntervalMs: 30000,
defaultTimeoutMs: 10000,
...overrides,
};
}
describe("CpuChecker resolve", () => {
const checker = new CpuChecker();
test("默认值sampleDurationMs=1000, includePerCore=false", () => {
const target: RawTargetConfig = { cpu: {}, id: "cpu-test", type: "cpu" };
const resolved = checker.resolve(target, makeResolveContext());
expect(resolved.cpu.sampleDurationMs).toBe(1000);
expect(resolved.cpu.includePerCore).toBe(false);
});
test("显式配置覆盖默认值", () => {
const target: RawTargetConfig = {
cpu: { includePerCore: true, sampleDuration: "2s" },
id: "cpu-test",
type: "cpu",
};
const resolved = checker.resolve(target, makeResolveContext());
expect(resolved.cpu.sampleDurationMs).toBe(2000);
expect(resolved.cpu.includePerCore).toBe(true);
});
test("无 expect 时 expect 为 undefined", () => {
const target: RawTargetConfig = { cpu: {}, id: "cpu-test", type: "cpu" };
const resolved = checker.resolve(target, makeResolveContext());
expect(resolved.expect).toBeUndefined();
});
test("保留 expect 字段", () => {
const target: RawTargetConfig = {
cpu: {},
expect: { usagePercent: { lte: 85 } },
id: "cpu-test",
type: "cpu",
};
const resolved = checker.resolve(target, makeResolveContext());
expect(resolved.expect).toEqual({ usagePercent: { lte: 85 } });
});
test("type 为 cpu", () => {
const target: RawTargetConfig = { cpu: {}, id: "cpu-test", type: "cpu" };
const resolved = checker.resolve(target, makeResolveContext());
expect(resolved.type).toBe("cpu");
});
});
describe("CpuChecker execute", () => {
function makeSnapshotReader(_first: CpuCoreSnapshot[], _second: CpuCoreSnapshot[]): SnapshotReader {
let callCount = 0;
const snapshots = [_first, _second];
return () => {
const result = snapshots[Math.min(callCount, snapshots.length - 1)]!;
callCount++;
return result;
};
}
test("成功匹配", async () => {
// 50% usage, 50% idle
const before = [makeCore(1000, 0, 0, 9000, 0)];
const after = [makeCore(1500, 0, 0, 9500, 0)];
const reader = makeSnapshotReader(before, after);
const checker = new CpuChecker(reader);
const target: RawTargetConfig = { cpu: {}, id: "cpu-test", type: "cpu" };
const resolved = checker.resolve(target, makeResolveContext({ defaultTimeoutMs: 5000 }));
resolved.expect = { usagePercent: { lte: 85 } };
const ctx = { signal: new AbortController().signal };
const result = await checker.execute(resolved, ctx);
expect(result.matched).toBe(true);
expect(result.failure).toBeNull();
expect(result.observation).toMatchObject({
idlePercent: 50,
logicalCoreCount: 1,
usagePercent: 50,
});
// 默认不包含 perCoreUsagePercent
expect(result.observation!["perCoreUsagePercent"]).toBeUndefined();
});
test("usagePercent mismatch", async () => {
// 90% usage: before idle=0, after idle=1000, total=10000
const before = [makeCore(0, 0, 0, 0, 0)];
const after = [makeCore(9000, 0, 0, 1000, 0)];
const reader = makeSnapshotReader(before, after);
const checker = new CpuChecker(reader);
const target: RawTargetConfig = { cpu: {}, id: "cpu-test", type: "cpu" };
const resolved = checker.resolve(target, makeResolveContext({ defaultTimeoutMs: 5000 }));
resolved.expect = { usagePercent: { lte: 50 } };
const ctx = { signal: new AbortController().signal };
const result = await checker.execute(resolved, ctx);
expect(result.matched).toBe(false);
expect(result.failure?.phase).toBe("usage");
});
test("idlePercent mismatch", async () => {
// idle = 10%: before idle=0, after idle=1000, total=10000
const before = [makeCore(0, 0, 0, 0, 0)];
const after = [makeCore(9000, 0, 0, 1000, 0)];
const reader = makeSnapshotReader(before, after);
const checker = new CpuChecker(reader);
const target: RawTargetConfig = { cpu: {}, id: "cpu-test", type: "cpu" };
const resolved = checker.resolve(target, makeResolveContext({ defaultTimeoutMs: 5000 }));
resolved.expect = { idlePercent: { gte: 80 } };
const ctx = { signal: new AbortController().signal };
const result = await checker.execute(resolved, ctx);
expect(result.matched).toBe(false);
expect(result.failure?.phase).toBe("idle");
});
test("maxCoreUsagePercent mismatch", async () => {
// core 0: 95% usage, core 1: 10% usage
const bf = [makeCore(0, 0, 0, 0, 0), makeCore(0, 0, 0, 0, 0)];
const af = [makeCore(9500, 0, 0, 500, 0), makeCore(1000, 0, 0, 9000, 0)];
const reader = makeSnapshotReader(bf, af);
const checker = new CpuChecker(reader);
const target: RawTargetConfig = { cpu: {}, id: "cpu-test", type: "cpu" };
const resolved = checker.resolve(target, makeResolveContext({ defaultTimeoutMs: 5000 }));
resolved.expect = { maxCoreUsagePercent: { lte: 80 } };
const ctx = { signal: new AbortController().signal };
const result = await checker.execute(resolved, ctx);
expect(result.matched).toBe(false);
expect(result.failure?.phase).toBe("maxCoreUsage");
});
test("minCoreUsagePercent mismatch", async () => {
// core 0: 95% usage, core 1: 10% usage
const bf = [makeCore(0, 0, 0, 0, 0), makeCore(0, 0, 0, 0, 0)];
const af = [makeCore(9500, 0, 0, 500, 0), makeCore(1000, 0, 0, 9000, 0)];
const reader = makeSnapshotReader(bf, af);
const checker = new CpuChecker(reader);
const target: RawTargetConfig = { cpu: {}, id: "cpu-test", type: "cpu" };
const resolved = checker.resolve(target, makeResolveContext({ defaultTimeoutMs: 5000 }));
resolved.expect = { minCoreUsagePercent: { gte: 50 } };
const ctx = { signal: new AbortController().signal };
const result = await checker.execute(resolved, ctx);
expect(result.matched).toBe(false);
expect(result.failure?.phase).toBe("minCoreUsage");
});
test("durationMs mismatch", async () => {
const before = [makeCore(0, 0, 0, 10000, 0)];
const after = [makeCore(1000, 0, 0, 9000, 0)];
const reader = makeSnapshotReader(before, after);
const checker = new CpuChecker(reader);
const target: RawTargetConfig = { cpu: {}, id: "cpu-test", type: "cpu" };
const resolved = checker.resolve(target, makeResolveContext({ defaultTimeoutMs: 5000 }));
resolved.expect = { durationMs: { lte: 0 } };
const ctx = { signal: new AbortController().signal };
const result = await checker.execute(resolved, ctx);
expect(result.matched).toBe(false);
expect(result.failure?.phase).toBe("duration");
});
test("超时取消", async () => {
const before = [makeCore(0, 0, 0, 10000, 0)];
const after = [makeCore(1000, 0, 0, 9000, 0)];
const reader = makeSnapshotReader(before, after);
const checker = new CpuChecker(reader);
const target: RawTargetConfig = { cpu: { sampleDuration: "10s" }, id: "cpu-test", type: "cpu" };
const resolved = checker.resolve(target, makeResolveContext({ defaultTimeoutMs: 100 }));
const controller = new AbortController();
controller.abort();
const result = await checker.execute(resolved, { signal: controller.signal });
expect(result.matched).toBe(false);
expect(result.failure?.phase).toBe("cpu");
expect(result.failure?.path).toBe("timeout");
});
test("includePerCore=true 时输出 perCoreUsagePercent", async () => {
const before = [makeCore(0, 0, 0, 0, 0), makeCore(0, 0, 0, 0, 0)];
const after = [makeCore(8000, 0, 0, 2000, 0), makeCore(2000, 0, 0, 8000, 0)];
const reader = makeSnapshotReader(before, after);
const checker = new CpuChecker(reader);
const target: RawTargetConfig = { cpu: { includePerCore: true }, id: "cpu-test", type: "cpu" };
const resolved = checker.resolve(target, makeResolveContext({ defaultTimeoutMs: 5000 }));
const ctx = { signal: new AbortController().signal };
const result = await checker.execute(resolved, ctx);
expect(result.observation).toMatchObject({
perCoreUsagePercent: [80, 20],
});
});
});
describe("CpuChecker buildDetail", () => {
test("正常输出格式", () => {
const checker = new CpuChecker();
const detail = checker.buildDetail({
idlePercent: 40,
logicalCoreCount: 8,
maxCoreUsagePercent: 91.5,
minCoreUsagePercent: 8.2,
usagePercent: 60,
});
expect(detail).toBe("usage 60%, max core 91.5%, 8 cores");
});
});
describe("CpuChecker serialize", () => {
test("序列化输出", () => {
const checker = new CpuChecker();
const target: RawTargetConfig = { cpu: { sampleDuration: "1s" }, id: "cpu-test", type: "cpu" };
const resolved = checker.resolve(target, makeResolveContext());
const result = checker.serialize(resolved);
expect(result.target).toBe("cpu sample 1000ms");
const config = JSON.parse(result.config) as { sampleDurationMs: number };
expect(config.sampleDurationMs).toBe(1000);
});
});

View File

@@ -0,0 +1,55 @@
import { describe, expect, test } from "bun:test";
import {
checkIdlePercent,
checkMaxCoreUsage,
checkMinCoreUsage,
checkUsagePercent,
} from "../../../../../src/server/checker/runner/cpu/expect";
describe("CPU expect checks", () => {
test("checkUsagePercent 匹配", () => {
expect(checkUsagePercent(50, { lte: 85 }).matched).toBe(true);
});
test("checkUsagePercent 不匹配", () => {
const result = checkUsagePercent(90, { lte: 85 });
expect(result.matched).toBe(false);
expect(result.failure?.phase).toBe("usage");
});
test("checkIdlePercent 匹配", () => {
expect(checkIdlePercent(50, { gte: 15 }).matched).toBe(true);
});
test("checkIdlePercent 不匹配", () => {
const result = checkIdlePercent(10, { gte: 15 });
expect(result.matched).toBe(false);
expect(result.failure?.phase).toBe("idle");
});
test("checkMaxCoreUsage 匹配", () => {
expect(checkMaxCoreUsage(80, { lte: 95 }).matched).toBe(true);
});
test("checkMaxCoreUsage 不匹配", () => {
const result = checkMaxCoreUsage(96, { lte: 95 });
expect(result.matched).toBe(false);
expect(result.failure?.phase).toBe("maxCoreUsage");
});
test("checkMinCoreUsage 匹配", () => {
expect(checkMinCoreUsage(10, { gte: 5 }).matched).toBe(true);
});
test("checkMinCoreUsage 不匹配", () => {
const result = checkMinCoreUsage(3, { gte: 5 });
expect(result.matched).toBe(false);
expect(result.failure?.phase).toBe("minCoreUsage");
});
test("undefined matcher 直接通过", () => {
expect(checkUsagePercent(99.9, undefined).matched).toBe(true);
expect(checkIdlePercent(0, undefined).matched).toBe(true);
});
});

View File

@@ -0,0 +1,41 @@
import { describe, expect, test } from "bun:test";
import { normalizeTargetExpect } from "../../../../../src/server/checker/runner/cpu/normalize";
describe("normalizeTargetExpect (cpu)", () => {
test("无 expect 直接返回", () => {
const target = { cpu: {}, id: "test", type: "cpu" };
expect(normalizeTargetExpect(target)).toEqual(target);
});
test("expect 为非对象直接返回", () => {
const target = { cpu: {}, expect: "not-an-object", id: "test", type: "cpu" };
expect(normalizeTargetExpect(target)).toEqual(target);
});
test("ValueMatcher 简写展开", () => {
const target = { cpu: {}, expect: { usagePercent: 85 }, id: "test", type: "cpu" };
const result = normalizeTargetExpect(target);
expect((result.expect as Record<string, unknown>)["usagePercent"]).toEqual({ equals: 85 });
});
test("已经是 matcher 对象的不变", () => {
const target = { cpu: {}, expect: { usagePercent: { lte: 85 } }, id: "test", type: "cpu" };
const result = normalizeTargetExpect(target);
expect((result.expect as Record<string, unknown>)["usagePercent"]).toEqual({ lte: 85 });
});
test("多个字段同时展开", () => {
const target = {
cpu: {},
expect: { idlePercent: 15, maxCoreUsagePercent: { lte: 95 }, usagePercent: 85 },
id: "test",
type: "cpu",
};
const result = normalizeTargetExpect(target);
const expectObj = result.expect as Record<string, unknown>;
expect(expectObj["idlePercent"]).toEqual({ equals: 15 });
expect(expectObj["maxCoreUsagePercent"]).toEqual({ lte: 95 });
expect(expectObj["usagePercent"]).toEqual({ equals: 85 });
});
});

View File

@@ -0,0 +1,77 @@
import Ajv from "ajv";
import { describe, expect, test } from "bun:test";
import { cpuCheckerSchemas } from "../../../../../src/server/checker/runner/cpu/schema";
const ajv = new Ajv({ strict: false });
describe("CPU checker schema", () => {
test("authoring config 允许变量引用", () => {
const validate = ajv.compile(cpuCheckerSchemas.authoring.config);
expect(validate({ includePerCore: "${per_core|false}", sampleDuration: "${sample_dur|1s}" })).toBe(true);
});
test("normalized config 允许合法值", () => {
const validate = ajv.compile(cpuCheckerSchemas.normalized.config);
expect(validate({ includePerCore: true, sampleDuration: "1s" })).toBe(true);
});
test("normalized config 空配置通过", () => {
const validate = ajv.compile(cpuCheckerSchemas.normalized.config);
expect(validate({})).toBe(true);
});
test("config 拒绝额外字段", () => {
const validate = ajv.compile(cpuCheckerSchemas.authoring.config);
expect(validate({ extraField: true })).toBe(false);
});
test("authoring expect 允许 ValueMatcher 简写", () => {
const validate = ajv.compile(cpuCheckerSchemas.authoring.expect);
expect(validate({ usagePercent: 85 })).toBe(true);
expect(validate({ usagePercent: { lte: 85 } })).toBe(true);
});
test("normalized expect 允许 matcher 对象", () => {
const validate = ajv.compile(cpuCheckerSchemas.normalized.expect);
expect(validate({ idlePercent: { gte: 15 }, usagePercent: { lte: 85 } })).toBe(true);
});
test("expect 拒绝 logicalCoreCount 字段", () => {
const validate = ajv.compile(cpuCheckerSchemas.authoring.expect);
expect(validate({ logicalCoreCount: { gte: 4 } })).toBe(false);
});
test("expect 拒绝 userPercent 字段", () => {
const validate = ajv.compile(cpuCheckerSchemas.authoring.expect);
expect(validate({ userPercent: { lte: 50 } })).toBe(false);
});
test("expect 拒绝 systemPercent 字段", () => {
const validate = ajv.compile(cpuCheckerSchemas.authoring.expect);
expect(validate({ systemPercent: { lte: 50 } })).toBe(false);
});
test("expect 允许所有合法字段", () => {
const validate = ajv.compile(cpuCheckerSchemas.normalized.expect);
expect(
validate({
durationMs: { lte: 2000 },
idlePercent: { gte: 15 },
maxCoreUsagePercent: { lte: 95 },
minCoreUsagePercent: { gte: 0 },
usagePercent: { lte: 85 },
}),
).toBe(true);
});
test("expect 拒绝额外字段", () => {
const validate = ajv.compile(cpuCheckerSchemas.normalized.expect);
expect(validate({ unknownField: 1 })).toBe(false);
});
test("expect 空对象通过", () => {
const validate = ajv.compile(cpuCheckerSchemas.normalized.expect);
expect(validate({})).toBe(true);
});
});

View File

@@ -0,0 +1,84 @@
import { describe, expect, test } from "bun:test";
import type { RawTargetConfig } from "../../../../../src/server/checker/types";
import { validateCpuConfig } from "../../../../../src/server/checker/runner/cpu/validate";
function validate(target: RawTargetConfig) {
return validateCpuConfig({ targets: [target] });
}
describe("validateCpuConfig", () => {
test("有效配置无错误", () => {
expect(validate({ cpu: { sampleDuration: "1s" }, id: "cpu-test", type: "cpu" })).toEqual([]);
});
test("空 cpu 配置无错误", () => {
expect(validate({ cpu: {}, id: "cpu-test", type: "cpu" })).toEqual([]);
});
test("缺少 cpu 配置分组", () => {
const issues = validate({ id: "cpu-test", type: "cpu" });
expect(issues.some((i) => i.path.endsWith("cpu") && i.code === "required")).toBe(true);
});
test("无效 sampleDuration 格式", () => {
const issues = validate({ cpu: { sampleDuration: "abc" }, id: "cpu-test", type: "cpu" });
expect(issues.some((i) => i.path.endsWith("sampleDuration"))).toBe(true);
});
test("sampleDuration >= timeout 报错", () => {
const issues = validate({ cpu: { sampleDuration: "5s" }, id: "cpu-test", timeout: "5s", type: "cpu" });
expect(issues.some((i) => i.path.endsWith("sampleDuration") && i.message.includes("必须小于 timeout"))).toBe(true);
});
test("sampleDuration 大于默认 timeout (10s) 报错", () => {
const issues = validate({ cpu: { sampleDuration: "15s" }, id: "cpu-test", type: "cpu" });
expect(issues.some((i) => i.message.includes("默认 10s"))).toBe(true);
});
test("sampleDuration < timeout 通过", () => {
const issues = validate({ cpu: { sampleDuration: "1s" }, id: "cpu-test", timeout: "5s", type: "cpu" });
expect(issues.some((i) => i.path.includes("sampleDuration"))).toBe(false);
});
test("includePerCore 非布尔值报错", () => {
const issues = validate({ cpu: { includePerCore: "yes" }, id: "cpu-test", type: "cpu" });
expect(issues.some((i) => i.path.endsWith("includePerCore") && i.code === "invalid-type")).toBe(true);
});
test("cpu 未知字段报错", () => {
const issues = validate({ cpu: { extra: true }, id: "cpu-test", type: "cpu" });
expect(issues.some((i) => i.path.endsWith("extra") && i.code === "unknown-field")).toBe(true);
});
test("expect 未知字段报错", () => {
const issues = validate({ cpu: {}, expect: { logicalCoreCount: { gte: 4 } }, id: "cpu-test", type: "cpu" });
expect(issues.some((i) => i.path.endsWith("logicalCoreCount") && i.code === "unknown-field")).toBe(true);
});
test("expect userPercent 未知字段报错", () => {
const issues = validate({ cpu: {}, expect: { userPercent: { lte: 50 } }, id: "cpu-test", type: "cpu" });
expect(issues.some((i) => i.path.endsWith("userPercent") && i.code === "unknown-field")).toBe(true);
});
test("expect systemPercent 未知字段报错", () => {
const issues = validate({ cpu: {}, expect: { systemPercent: { lte: 50 } }, id: "cpu-test", type: "cpu" });
expect(issues.some((i) => i.path.endsWith("systemPercent") && i.code === "unknown-field")).toBe(true);
});
test("expect 合法 ValueMatcher 通过", () => {
const issues = validate({
cpu: {},
expect: { maxCoreUsagePercent: { lte: 95 }, usagePercent: { lte: 85 } },
id: "cpu-test",
type: "cpu",
});
expect(issues.filter((i) => i.path.includes("expect"))).toEqual([]);
});
test("expect 非法 ValueMatcher 报错", () => {
const issues = validate({ cpu: {}, expect: { usagePercent: [1, 2] }, id: "cpu-test", type: "cpu" });
expect(issues.some((i) => i.path.includes("usagePercent"))).toBe(true);
});
});

View File

@@ -73,8 +73,20 @@ describe("CheckerRegistry", () => {
const second = createDefaultCheckerRegistry();
first.register(createChecker("custom"));
expect(first.supportedTypes).toEqual(["http", "cmd", "db", "tcp", "icmp", "udp", "llm", "dns", "ws", "custom"]);
expect(second.supportedTypes).toEqual(["http", "cmd", "db", "tcp", "icmp", "udp", "llm", "dns", "ws"]);
expect(first.supportedTypes).toEqual([
"http",
"cmd",
"db",
"tcp",
"icmp",
"udp",
"llm",
"dns",
"ws",
"cpu",
"custom",
]);
expect(second.supportedTypes).toEqual(["http", "cmd", "db", "tcp", "icmp", "udp", "llm", "dns", "ws", "cpu"]);
expect(
first.definitions.every((checker) => checker.schemas.authoring.config && checker.schemas.normalized.expect),
).toBe(true);