- 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 文档,新增完整测试覆盖
142 lines
4.3 KiB
TypeScript
142 lines
4.3 KiB
TypeScript
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"]);
|
|
});
|
|
});
|