1
0

feat: 重构为多类型 checker 通用框架,支持 HTTP 与命令检查

- 引入 typed target 判别联合,支持 http 与 command 两种 checker
- expect 重构为有序规则数组,按配置顺序快速失败并生成结构化 failure
- 新增 command runner,支持 exec + args 本地命令执行
- 引入全局并发限制 maxConcurrentChecks 和 size 解析 (KB/MB/GB)
- HTTP/command 各自独立 expect pipeline,应用领域默认成功语义
- SQLite schema、API、Dashboard 全链路调整为 checker 通用契约
- 补充完整测试覆盖(192 tests),更新 README 与示例配置
This commit is contained in:
2026-05-10 22:25:21 +08:00
parent 599d973cbd
commit b8810f1182
46 changed files with 3562 additions and 1062 deletions

View File

@@ -26,18 +26,27 @@ describe("API 路由", () => {
store = new ProbeStore(join(tempDir, "test.db"));
store.syncTargets([
{
type: "http",
name: "test-a",
url: "http://a.com",
method: "GET",
headers: {},
http: {
url: "http://a.com",
method: "GET",
headers: {},
maxBodyBytes: 104857600,
},
intervalMs: 30000,
timeoutMs: 10000,
},
{
type: "command",
name: "test-b",
url: "http://b.com",
method: "POST",
headers: {},
command: {
exec: "echo",
args: ["hello"],
cwd: "/tmp",
env: {},
maxOutputBytes: 104857600,
},
intervalMs: 60000,
timeoutMs: 5000,
},
@@ -48,19 +57,26 @@ describe("API 路由", () => {
targetId: targets[0]!.id,
timestamp: "2025-01-01T00:00:00.000Z",
success: true,
statusCode: 200,
latencyMs: 150,
error: null,
matched: true,
durationMs: 150,
statusDetail: "200 OK",
failure: null,
});
store.insertCheckResult({
targetId: targets[0]!.id,
timestamp: "2025-01-01T00:00:30.000Z",
success: false,
statusCode: null,
latencyMs: null,
error: "timeout",
matched: false,
durationMs: null,
statusDetail: null,
failure: {
kind: "error",
phase: "status",
path: "$.status",
expected: 200,
actual: 500,
message: "状态码不匹配",
},
});
fetchHandler = createFetchHandler({ mode: "test", staticAssets, store });
@@ -88,6 +104,7 @@ describe("API 路由", () => {
expect(body.up).toBeGreaterThanOrEqual(0);
expect(body.down).toBeGreaterThanOrEqual(0);
expect(body.up + body.down).toBe(2);
expect(body.avgDurationMs).toBeDefined();
});
test("/api/targets 返回目标列表", async () => {
@@ -96,12 +113,23 @@ describe("API 路由", () => {
expect(response.status).toBe(200);
expect(body).toHaveLength(2);
expect(body[0]!.name).toBe("test-a");
expect(body[0]!.latestCheck).not.toBeNull();
expect(body[0]!.latestCheck!.success).toBe(false);
expect(body[0]!.sparkline).toBeDefined();
expect(Array.isArray(body[0]!.sparkline)).toBe(true);
expect(body[1]!.latestCheck).toBeNull();
const tA = body.find((t) => t.name === "test-a")!;
expect(tA.type).toBe("http");
expect(tA.target).toBe("http://a.com");
expect(tA.latestCheck).not.toBeNull();
expect(tA.latestCheck!.success).toBe(false);
expect(tA.latestCheck!.matched).toBe(false);
expect(tA.latestCheck!.failure).not.toBeNull();
expect(tA.sparkline).toBeDefined();
expect(Array.isArray(tA.sparkline)).toBe(true);
expect(tA.stats.avgDurationMs).toBeDefined();
expect(tA.stats.p99DurationMs).toBeDefined();
const tB = body.find((t) => t.name === "test-b")!;
expect(tB.type).toBe("command");
expect(tB.target).toBe("exec echo hello");
expect(tB.latestCheck).toBeNull();
});
test("/api/targets/:id/history 返回历史记录", async () => {
@@ -111,6 +139,8 @@ describe("API 路由", () => {
expect(response.status).toBe(200);
expect(body).toHaveLength(2);
expect(body[0].failure).not.toBeNull();
expect(body[0].failure.kind).toBe("error");
});
test("/api/targets/:id/history 支持 limit 参数", async () => {