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("第二次 snapshot 抛错返回 cpu/snapshot failure", async () => { const before = [makeCore(0, 0, 0, 10000, 0)]; let callCount = 0; const reader: SnapshotReader = () => { callCount++; if (callCount === 1) return before; throw new Error("second snapshot failed"); }; const checker = new CpuChecker(reader); const target: RawTargetConfig = { cpu: {}, 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.matched).toBe(false); expect(result.failure?.phase).toBe("cpu"); expect(result.failure?.path).toBe("snapshot"); expect(result.observation).toBeNull(); }); test("空 snapshot pair 返回 cpu/snapshot failure", async () => { const reader: SnapshotReader = () => []; const checker = new CpuChecker(reader); const target: RawTargetConfig = { cpu: {}, 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.matched).toBe(false); expect(result.failure?.phase).toBe("cpu"); expect(result.failure?.path).toBe("snapshot"); }); test("核心数不一致返回 cpu/snapshot failure", async () => { let callCount = 0; const snapshots = [[makeCore(0, 0, 0, 100, 0)], [makeCore(0, 0, 0, 100, 0), makeCore(0, 0, 0, 100, 0)]]; const reader: SnapshotReader = () => { const result = snapshots[Math.min(callCount, snapshots.length - 1)]!; callCount++; return result; }; const checker = new CpuChecker(reader); const target: RawTargetConfig = { cpu: {}, 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.matched).toBe(false); expect(result.failure?.phase).toBe("cpu"); expect(result.failure?.path).toBe("snapshot"); expect(result.failure?.message).toContain("核心数不一致"); }); test("非有限 CPU time 值返回 cpu/snapshot failure", async () => { let callCount = 0; const snapshots: CpuCoreSnapshot[][] = [ [makeCore(0, 0, 0, 100, 0)], [{ times: { idle: NaN, irq: 0, nice: 0, sys: 0, user: 100 } }], ]; const reader: SnapshotReader = () => { const result = snapshots[Math.min(callCount, snapshots.length - 1)]!; callCount++; return result; }; const checker = new CpuChecker(reader); const target: RawTargetConfig = { cpu: {}, 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.matched).toBe(false); expect(result.failure?.phase).toBe("cpu"); expect(result.failure?.path).toBe("snapshot"); expect(result.failure?.message).toContain("非有限值"); }); test("负数 CPU time delta 返回 cpu/snapshot failure", async () => { const before = [makeCore(1000, 0, 0, 0, 0)]; const after = [makeCore(100, 0, 0, 0, 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 })); const ctx = { signal: new AbortController().signal }; const result = await checker.execute(resolved, ctx); expect(result.matched).toBe(false); expect(result.failure?.phase).toBe("cpu"); expect(result.failure?.path).toBe("snapshot"); expect(result.failure?.message).toContain("负数 delta"); }); test("零 delta snapshot 返回稳定安全值", async () => { const before = [makeCore(100, 0, 0, 100, 0)]; const after = [makeCore(100, 0, 0, 100, 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 })); 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: 0, maxCoreUsagePercent: 0, minCoreUsagePercent: 0, usagePercent: 0, }); }); 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); }); });