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:
141
tests/server/bootstrap.test.ts
Normal file
141
tests/server/bootstrap.test.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import type { StaticAssets } from "../../src/server/app";
|
||||
import type { ResolvedConfig } from "../../src/server/checker/config-loader";
|
||||
import type { ProbeEngine } from "../../src/server/checker/engine";
|
||||
import type { ProbeStore } from "../../src/server/checker/store";
|
||||
import type { ResolvedTargetBase } from "../../src/server/checker/types";
|
||||
|
||||
import { bootstrap, type BootstrapDependencies } from "../../src/server/bootstrap";
|
||||
|
||||
type ShutdownSignal = "SIGINT" | "SIGTERM";
|
||||
|
||||
const target: ResolvedTargetBase = {
|
||||
group: "default",
|
||||
intervalMs: 30000,
|
||||
name: "test",
|
||||
timeoutMs: 5000,
|
||||
type: "command",
|
||||
};
|
||||
|
||||
function createHarness(overrides: BootstrapDependencies = {}) {
|
||||
const calls: string[] = [];
|
||||
const shutdownHandlers = new Map<ShutdownSignal, () => void>();
|
||||
const config: ResolvedConfig = {
|
||||
configDir: "/tmp",
|
||||
dataDir: "/tmp/dial-data",
|
||||
host: "127.0.0.1",
|
||||
maxConcurrentChecks: 3,
|
||||
port: 3000,
|
||||
retentionMs: 1000,
|
||||
targets: [target],
|
||||
};
|
||||
const store = {
|
||||
close() {
|
||||
calls.push("store.close");
|
||||
},
|
||||
syncTargets(targets: ResolvedTargetBase[]) {
|
||||
calls.push(`syncTargets:${targets.length}`);
|
||||
},
|
||||
} as unknown as ProbeStore;
|
||||
const engine = {
|
||||
start() {
|
||||
calls.push("engine.start");
|
||||
},
|
||||
stop() {
|
||||
calls.push("engine.stop");
|
||||
},
|
||||
} as unknown as ProbeEngine;
|
||||
|
||||
const dependencies: BootstrapDependencies = {
|
||||
createEngine(actualStore, targets, maxConcurrentChecks, retentionMs) {
|
||||
expect(actualStore).toBe(store);
|
||||
calls.push(`createEngine:${targets.length}:${maxConcurrentChecks}:${retentionMs}`);
|
||||
return engine;
|
||||
},
|
||||
createStore(dbPath) {
|
||||
calls.push(`createStore:${dbPath}`);
|
||||
return store;
|
||||
},
|
||||
exit(code) {
|
||||
calls.push(`exit:${code}`);
|
||||
throw new Error(`exit:${code}`);
|
||||
},
|
||||
loadConfig(configPath) {
|
||||
calls.push(`loadConfig:${configPath}`);
|
||||
return Promise.resolve(config);
|
||||
},
|
||||
logError(...data) {
|
||||
calls.push(`logError:${String(data[1])}`);
|
||||
},
|
||||
onSignal(signal, handler) {
|
||||
calls.push(`onSignal:${signal}`);
|
||||
shutdownHandlers.set(signal, handler);
|
||||
},
|
||||
startServer(options) {
|
||||
expect(options.config).toEqual({ host: config.host, port: config.port });
|
||||
expect(options.store).toBe(store);
|
||||
calls.push(`startServer:${options.mode}:${options.staticAssets ? "static" : "no-static"}`);
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
|
||||
return { calls, dependencies, shutdownHandlers };
|
||||
}
|
||||
|
||||
describe("bootstrap", () => {
|
||||
test("开发模式执行完整启动序列", async () => {
|
||||
const { calls, dependencies } = createHarness();
|
||||
|
||||
await bootstrap({ configPath: "/tmp/probes.yaml", mode: "development" }, dependencies);
|
||||
|
||||
expect(calls).toEqual([
|
||||
"loadConfig:/tmp/probes.yaml",
|
||||
"createStore:/tmp/dial-data/probe.db",
|
||||
"syncTargets:1",
|
||||
"createEngine:1:3:1000",
|
||||
"engine.start",
|
||||
"onSignal:SIGINT",
|
||||
"onSignal:SIGTERM",
|
||||
"startServer:development:no-static",
|
||||
]);
|
||||
});
|
||||
|
||||
test("生产模式传递 staticAssets", async () => {
|
||||
const { calls, dependencies } = createHarness();
|
||||
const staticAssets: StaticAssets = { files: {}, indexHtml: new Blob(["ok"]) };
|
||||
|
||||
await bootstrap({ configPath: "/tmp/probes.yaml", mode: "production", staticAssets }, dependencies);
|
||||
|
||||
expect(calls.at(-1)).toBe("startServer:production:static");
|
||||
});
|
||||
|
||||
test("收到退出信号时停止 engine 并关闭 store", async () => {
|
||||
const { calls, dependencies, shutdownHandlers } = createHarness();
|
||||
|
||||
await bootstrap({ configPath: "/tmp/probes.yaml", mode: "development" }, dependencies);
|
||||
|
||||
expect(() => shutdownHandlers.get("SIGINT")!()).toThrow("exit:0");
|
||||
|
||||
expect(calls.slice(-3)).toEqual(["engine.stop", "store.close", "exit:0"]);
|
||||
});
|
||||
|
||||
test("启动失败时输出错误并以非零退出", async () => {
|
||||
const { calls, dependencies } = createHarness({
|
||||
loadConfig() {
|
||||
return Promise.reject(new Error("bad config"));
|
||||
},
|
||||
});
|
||||
|
||||
let error: unknown;
|
||||
try {
|
||||
await bootstrap({ configPath: "/tmp/probes.yaml", mode: "development" }, dependencies);
|
||||
} catch (caught) {
|
||||
error = caught;
|
||||
}
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect((error as Error).message).toBe("exit:1");
|
||||
expect(calls).toEqual(["logError:bad config", "exit:1"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user