1
0

feat: 结构化 observation 替代 statusDetail,API 层动态构造 detail

- CheckResult: statusDetail -> observation (持久化) + detail (API 动态派生)
- 存储: status_detail 列 -> observation TEXT (JSON)
- CheckerDefinition: 新增 buildDetail(observation) 方法
- 各 checker 返回结构化 observation,API 层通过 registry 调用 buildDetail
- HTTP: bodyPreview 在 status/header 失败时也提前采集
- UDP: observation 包含 durationMs,未响应归为 error failure
- CMD: 超时/输出超限时保留已收集 observation
- TCP: connectTimeMs 仅含连接建立耗时,不含 banner 等待
- 新增 buildDetail 单测和 mapCheckResult 覆盖测试
- 同步 openspec 主规范,归档 checker-observation 变更
This commit is contained in:
2026-05-19 22:49:00 +08:00
parent 22c06820fa
commit 375dd3492b
64 changed files with 915 additions and 965 deletions

View File

@@ -184,7 +184,7 @@ describe("HttpChecker", () => {
test("成功请求 200", async () => {
const result = await checker.execute(makeTarget({ url: `${baseUrl}/ok` }), makeCtx());
expect(result.matched).toBe(true);
expect(result.statusDetail).toBe("HTTP 200");
expect(result.observation).toMatchObject({ statusCode: 200 });
expect(result.durationMs).not.toBeNull();
expect(result.failure).toBeNull();
});
@@ -192,7 +192,7 @@ describe("HttpChecker", () => {
test("404 不匹配默认 status [200]", async () => {
const result = await checker.execute(makeTarget({ url: `${baseUrl}/notfound` }), makeCtx());
expect(result.matched).toBe(false);
expect(result.statusDetail).toBe("HTTP 404");
expect(result.observation).toMatchObject({ bodyPreview: "not found", statusCode: 404 });
expect(result.failure!.phase).toBe("status");
});
@@ -218,6 +218,7 @@ describe("HttpChecker", () => {
makeCtx(),
);
expect(result.matched).toBe(false);
expect(result.observation).toMatchObject({ bodyPreview: "hello world", statusCode: 200 });
expect(result.failure!.phase).toBe("headers");
});
@@ -328,13 +329,13 @@ describe("HttpChecker", () => {
test("maxRedirects=0 不跟随重定向", async () => {
const result = await checker.execute(makeTarget({ maxRedirects: 0, url: `${baseUrl}/redirect` }), makeCtx());
expect(result.matched).toBe(false);
expect(result.statusDetail).toBe("HTTP 301");
expect(result.observation).toMatchObject({ statusCode: 301 });
});
test("maxRedirects>0 跟随重定向", async () => {
const result = await checker.execute(makeTarget({ maxRedirects: 5, url: `${baseUrl}/redirect` }), makeCtx());
expect(result.matched).toBe(true);
expect(result.statusDetail).toBe("HTTP 200");
expect(result.observation).toMatchObject({ statusCode: 200 });
});
test("maxRedirects 精确限制跟随次数", async () => {
@@ -343,7 +344,7 @@ describe("HttpChecker", () => {
makeCtx(),
);
expect(result.matched).toBe(false);
expect(result.statusDetail).toBe("HTTP 302");
expect(result.observation).toMatchObject({ statusCode: 302 });
});
test("maxRedirects 允许足够次数时到达最终目标", async () => {
@@ -352,7 +353,7 @@ describe("HttpChecker", () => {
makeCtx(),
);
expect(result.matched).toBe(true);
expect(result.statusDetail).toBe("HTTP 200");
expect(result.observation).toMatchObject({ statusCode: 200 });
});
test("ignoreSSL 跳过自签名证书校验", async () => {
@@ -370,14 +371,14 @@ describe("HttpChecker", () => {
makeCtx(),
);
expect(strictResult.matched).toBe(false);
expect(strictResult.statusDetail).toBeNull();
expect(strictResult.observation).toBeNull();
const ignoredResult = await checker.execute(
makeTarget({ ignoreSSL: true, url: `https://localhost:${httpsServer.port}/` }),
makeCtx(),
);
expect(ignoredResult.matched).toBe(true);
expect(ignoredResult.statusDetail).toBe("HTTP 200");
expect(ignoredResult.observation).toMatchObject({ statusCode: 200 });
} finally {
void httpsServer.stop();
}
@@ -594,7 +595,7 @@ describe("HttpChecker", () => {
makeCtx(),
);
expect(result.matched).toBe(true);
expect(result.statusDetail).toBe("HTTP 200");
expect(result.observation).toMatchObject({ statusCode: 200 });
});
test("混合 body rules 集成检查", async () => {