1
0

refactor: 后端架构加固 — 泛型化、批量查询、bootstrap 统一、路径修复与 pageSize 上限

- CheckerDefinition 泛型化,HTTP/Command checker 移除 resolved target 断言
- 新增 ProbeStore.getAllRecentSamples 消除 targets 路由 N+1 查询
- 统一 getAllTargetStats 与 getTargetStats 的 availability 精度
- Engine rejected 结果写入 internal error 记录,提升可观测性
- 新增 bootstrap.ts 统一 dev/production 启动序列
- dataDir 相对路径改为基于配置文件目录解析
- validatePagination 增加 pageSize 上限 200 校验
- 修复 ErrorBoundary override 标记
- 更新 README/DEVELOPMENT 文档,新增完整测试覆盖
This commit is contained in:
2026-05-13 18:15:46 +08:00
parent 6ea185315f
commit 147a2559ae
30 changed files with 930 additions and 129 deletions

View File

@@ -116,7 +116,7 @@ describe("loadConfig", () => {
const config = await loadConfig(configPath);
expect(config.host).toBe("127.0.0.1");
expect(config.port).toBe(3000);
expect(config.dataDir).toBe("./data");
expect(config.dataDir).toBe(join(tempDir, "data"));
expect(config.maxConcurrentChecks).toBe(20);
expect(config.targets).toHaveLength(1);
const t = config.targets[0]! as ResolvedHttpTarget;
@@ -205,7 +205,7 @@ targets:
const config = await loadConfig(configPath);
expect(config.host).toBe("0.0.0.0");
expect(config.port).toBe(8080);
expect(config.dataDir).toBe("./my-data");
expect(config.dataDir).toBe(join(tempDir, "my-data"));
expect(config.maxConcurrentChecks).toBe(5);
expect(config.targets).toHaveLength(2);
@@ -228,6 +228,25 @@ targets:
expect(cmd.command.maxOutputBytes).toBe(10485760);
});
test("绝对 dataDir 保持不变", async () => {
const dataDir = join(tempDir, "absolute-data");
const configPath = join(tempDir, "absolute-data-dir.yaml");
await writeFile(
configPath,
`server:
dataDir: "${dataDir}"
targets:
- name: "test"
type: http
http:
url: "http://example.com"
`,
);
const config = await loadConfig(configPath);
expect(config.dataDir).toBe(dataDir);
});
test("per-target 覆盖 defaults", async () => {
const configPath = join(tempDir, "override.yaml");
await writeFile(

View File

@@ -131,6 +131,48 @@ describe("ProbeEngine", () => {
expect(goodResult).toBeDefined();
});
test("checker rejected 时写入 internal error 结果", async () => {
ensureRegistered();
const checker = checkerRegistry.get("command");
const originalExecute = checker.execute.bind(checker);
checker.execute = async (target, ctx) => {
if (target.name === "reject-cmd") {
throw new Error("boom");
}
return originalExecute(target, ctx);
};
try {
const rejectTarget = makeCommandTarget("reject-cmd");
const goodTarget = makeCommandTarget("good-cmd");
const mockStore = createMockStore(["reject-cmd", "good-cmd"]) as unknown as ProbeStore;
const engine = new ProbeEngine(mockStore, [rejectTarget, goodTarget]);
const probeGroup = (
engine as unknown as { probeGroup: (t: ResolvedTargetBase[]) => Promise<void> }
).probeGroup.bind(engine);
await probeGroup([rejectTarget, goodTarget]);
const results = (mockStore as unknown as { _results: Array<Record<string, unknown>> })._results;
expect(results.length).toBe(2);
expect(results[0]!["targetId"]).toBe(1);
expect(results[0]!["matched"]).toBe(false);
expect(results[0]!["durationMs"]).toBeNull();
expect(results[0]!["statusDetail"]).toBeNull();
expect(results[0]!["failure"]).toEqual({
kind: "error",
message: "boom",
path: "engine",
phase: "internal",
});
expect(typeof results[0]!["timestamp"]).toBe("string");
expect(results[1]!["targetId"]).toBe(2);
expect(results[1]!["matched"]).toBe(true);
} finally {
checker.execute = originalExecute;
}
});
test("并发限制 maxConcurrentChecks", async () => {
const targets = Array.from({ length: 5 }, (_, i) =>
makeCommandTarget(`cmd-${i}`, {

View File

@@ -757,7 +757,7 @@ describe("HttpChecker.resolve", () => {
{ http: { url: "https://example.com" }, name: "test", type: "http" },
makeResolveContext(),
);
expect((result as ResolvedHttpTarget).http.ignoreSSL).toBe(false);
expect(result.http.ignoreSSL).toBe(false);
});
test("maxRedirects 默认值为 0", () => {
@@ -765,7 +765,7 @@ describe("HttpChecker.resolve", () => {
{ http: { url: "https://example.com" }, name: "test", type: "http" },
makeResolveContext(),
);
expect((result as ResolvedHttpTarget).http.maxRedirects).toBe(0);
expect(result.http.maxRedirects).toBe(0);
});
test("合法 status 范围模式通过校验", () => {
@@ -773,7 +773,7 @@ describe("HttpChecker.resolve", () => {
{ expect: { status: ["2xx", 301] }, http: { url: "https://example.com" }, name: "test", type: "http" },
makeResolveContext(),
);
expect((result as ResolvedHttpTarget).expect?.status).toEqual(["2xx", 301]);
expect(result.expect?.status).toEqual(["2xx", 301]);
});
test("显式 ignoreSSL 和 maxRedirects 正确解析", () => {
@@ -781,7 +781,7 @@ describe("HttpChecker.resolve", () => {
{ http: { ignoreSSL: true, maxRedirects: 3, url: "https://example.com" }, name: "test", type: "http" },
makeResolveContext(),
);
expect((result as ResolvedHttpTarget).http.ignoreSSL).toBe(true);
expect((result as ResolvedHttpTarget).http.maxRedirects).toBe(3);
expect(result.http.ignoreSSL).toBe(true);
expect(result.http.maxRedirects).toBe(3);
});
});

View File

@@ -280,6 +280,71 @@ describe("ProbeStore", () => {
}
});
test("getAllRecentSamples 返回每个 target 的最近采样数据", () => {
const sampleStore = new ProbeStore(join(tempDir, "all-samples.db"));
const httpA: ResolvedHttpTarget = { ...httpTarget, name: "sample-http-a" };
const httpB: ResolvedHttpTarget = {
...httpTarget,
http: { ...httpTarget.http, url: "https://example.com/other" },
name: "sample-http-b",
};
const httpEmpty: ResolvedHttpTarget = {
...httpTarget,
http: { ...httpTarget.http, url: "https://example.com/empty" },
name: "sample-http-empty",
};
sampleStore.syncTargets([httpA, httpB, httpEmpty]);
const targets = sampleStore.getTargets();
const targetAId = targets.find((t) => t.name === "sample-http-a")!.id;
const targetBId = targets.find((t) => t.name === "sample-http-b")!.id;
const emptyTargetId = targets.find((t) => t.name === "sample-http-empty")!.id;
for (const [index, timestamp] of [
"2025-01-01T00:00:00.000Z",
"2025-01-01T00:01:00.000Z",
"2025-01-01T00:02:00.000Z",
].entries()) {
sampleStore.insertCheckResult({
durationMs: 100 + index,
failure: null,
matched: index !== 1,
statusDetail: "200 OK",
targetId: targetAId,
timestamp,
});
}
sampleStore.insertCheckResult({
durationMs: 200,
failure: null,
matched: true,
statusDetail: "200 OK",
targetId: targetBId,
timestamp: "2025-01-01T00:03:00.000Z",
});
sampleStore.insertCheckResult({
durationMs: null,
failure: { kind: "error", message: "fail", path: "request", phase: "request" },
matched: false,
statusDetail: null,
targetId: targetBId,
timestamp: "2025-01-01T00:04:00.000Z",
});
const samples = sampleStore.getAllRecentSamples(2);
expect(samples.get(targetAId)).toEqual([
{ duration_ms: 102, matched: 1, timestamp: "2025-01-01T00:02:00.000Z" },
{ duration_ms: 101, matched: 0, timestamp: "2025-01-01T00:01:00.000Z" },
]);
expect(samples.get(targetBId)).toEqual([
{ duration_ms: null, matched: 0, timestamp: "2025-01-01T00:04:00.000Z" },
{ duration_ms: 200, matched: 1, timestamp: "2025-01-01T00:03:00.000Z" },
]);
expect(samples.has(emptyTargetId)).toBe(false);
sampleStore.close();
});
test("关闭后操作不报错", () => {
const closedStore = new ProbeStore(join(tempDir, "closed.db"));
closedStore.close();
@@ -420,6 +485,33 @@ describe("ProbeStore", () => {
freshStore.close();
});
test("getAllTargetStats 与 getTargetStats 的 availability 精度一致", () => {
const statsStore = new ProbeStore(join(tempDir, "stats-precision.db"));
const target: ResolvedHttpTarget = { ...httpTarget, name: "stats-precision" };
statsStore.syncTargets([target]);
const targetId = statsStore.getTargets()[0]!.id;
for (const [index, matched] of [true, true, false].entries()) {
statsStore.insertCheckResult({
durationMs: 100,
failure: null,
matched,
statusDetail: matched ? "200 OK" : "500 ERROR",
targetId,
timestamp: `2025-01-01T00:0${index}:00.000Z`,
});
}
const targetStats = statsStore.getTargetStats(targetId);
const allStats = statsStore.getAllTargetStats().get(targetId)!;
expect(targetStats.availability).toBe(66.67);
expect(allStats.availability).toBe(66.67);
expect(allStats.availability).toBe(targetStats.availability);
statsStore.close();
});
test("prune 删除过期数据", () => {
const pruneStore = new ProbeStore(join(tempDir, "prune.db"));
pruneStore.syncTargets([httpTarget]);