feat: 配置变量系统与 target id/name 双字段标识
- 新增顶层 variables 段支持 string/number/boolean 字面量
- target 字符串字段支持 、、{...} 转义语法
- 变量解析优先级: variables -> process.env -> 默认值 -> 报错
- 完整引用保留原始类型,部分引用拼接为字符串
- 变量替换在 YAML 解析后、AJV 校验前执行
- 替换仅作用于 targets,跳过 id/type 字段
- target 新增必填 id 字段作为唯一标识,name 改为可选展示名称
- 数据库存储/API/前端全面迁移到 id 标识
- 统一 checker 运行时类型检查为 es-toolkit predicates
- 同步 delta specs 到主 specs,归档 config-variables 变更
This commit is contained in:
@@ -24,6 +24,10 @@ beforeAll(() => {
|
||||
ensureRegistered();
|
||||
});
|
||||
|
||||
function targetId(store: ProbeStore, name: string): string {
|
||||
return store.getTargets().find((target) => target.name === name)!.id;
|
||||
}
|
||||
|
||||
const httpTarget: ResolvedHttpTarget = {
|
||||
expect: { maxDurationMs: 3000, status: [200] },
|
||||
group: "default",
|
||||
@@ -35,6 +39,7 @@ const httpTarget: ResolvedHttpTarget = {
|
||||
method: "GET",
|
||||
url: "https://example.com/health",
|
||||
},
|
||||
id: "test-http",
|
||||
intervalMs: 30000,
|
||||
name: "test-http",
|
||||
timeoutMs: 10000,
|
||||
@@ -50,6 +55,7 @@ const commandTarget: ResolvedCommandTarget = {
|
||||
maxOutputBytes: 104857600,
|
||||
},
|
||||
group: "default",
|
||||
id: "test-cmd",
|
||||
intervalMs: 60000,
|
||||
name: "test-cmd",
|
||||
timeoutMs: 5000,
|
||||
@@ -79,8 +85,7 @@ describe("ProbeStore", () => {
|
||||
store.syncTargets([httpTarget, commandTarget]);
|
||||
const targets = store.getTargets();
|
||||
expect(targets).toHaveLength(2);
|
||||
expect(targets[0]!.name).toBe("test-http");
|
||||
expect(targets[1]!.name).toBe("test-cmd");
|
||||
expect(targets.map((target) => target.name).sort()).toEqual(["test-cmd", "test-http"]);
|
||||
});
|
||||
|
||||
test("http target 字段正确", () => {
|
||||
@@ -144,20 +149,18 @@ describe("ProbeStore", () => {
|
||||
});
|
||||
|
||||
test("getTargetById", () => {
|
||||
const targets = store.getTargets();
|
||||
const found = store.getTargetById(targets[0]!.id);
|
||||
const found = store.getTargetById(targetId(store, "test-http"));
|
||||
expect(found).toBeDefined();
|
||||
expect(found!.name).toBe("test-http");
|
||||
});
|
||||
|
||||
test("getTargetById 不存在", () => {
|
||||
expect(store.getTargetById(99999)).toBeNull();
|
||||
expect(store.getTargetById("missing-target")).toBeNull();
|
||||
});
|
||||
|
||||
test("写入 check result 并查询", () => {
|
||||
store.syncTargets([httpTarget, commandTarget]);
|
||||
const targets = store.getTargets();
|
||||
const t1Id = targets[0]!.id;
|
||||
const t1Id = targetId(store, "test-http");
|
||||
|
||||
store.insertCheckResult({
|
||||
durationMs: 150.5,
|
||||
@@ -209,8 +212,7 @@ describe("ProbeStore", () => {
|
||||
});
|
||||
|
||||
test("getHistory 默认 limit=20", () => {
|
||||
const targets = store.getTargets();
|
||||
const t1Id = targets[0]!.id;
|
||||
const t1Id = targetId(store, "test-http");
|
||||
|
||||
for (let i = 0; i < 25; i++) {
|
||||
store.insertCheckResult({
|
||||
@@ -228,8 +230,7 @@ describe("ProbeStore", () => {
|
||||
});
|
||||
|
||||
test("getTargetWindowStats 按时间窗口计算基础计数", () => {
|
||||
const targets = store.getTargets();
|
||||
const t1Id = targets[0]!.id;
|
||||
const t1Id = targetId(store, "test-http");
|
||||
|
||||
const stats = store.getTargetWindowStats(t1Id, "2025-01-01T00:00:00.000Z", "2025-01-01T00:01:00.000Z");
|
||||
expect(stats.totalChecks).toBeGreaterThan(0);
|
||||
@@ -239,8 +240,7 @@ describe("ProbeStore", () => {
|
||||
});
|
||||
|
||||
test("无记录目标的窗口 stats", () => {
|
||||
const targets = store.getTargets();
|
||||
const t2Id = targets.find((t) => t.name === "test-cmd")!.id;
|
||||
const t2Id = targetId(store, "test-cmd");
|
||||
|
||||
const stats = store.getTargetWindowStats(t2Id, "2025-01-01T00:00:00.000Z", "2025-01-01T00:01:00.000Z");
|
||||
expect(stats.totalChecks).toBe(0);
|
||||
@@ -251,16 +251,14 @@ describe("ProbeStore", () => {
|
||||
|
||||
test("getLatestChecksMap 支持 Dashboard 组装当前状态", () => {
|
||||
const latestChecksMap = store.getLatestChecksMap();
|
||||
const targets = store.getTargets();
|
||||
const latest = latestChecksMap.get(targets[0]!.id);
|
||||
const latest = latestChecksMap.get(targetId(store, "test-http"));
|
||||
|
||||
expect(latest).toBeDefined();
|
||||
expect(latest!.timestamp).toBe("2025-01-01T01:24:00.000Z");
|
||||
});
|
||||
|
||||
test("getTargetCheckpoints 返回窗口内升序检查点", () => {
|
||||
const targets = store.getTargets();
|
||||
const t1Id = targets[0]!.id;
|
||||
const t1Id = targetId(store, "test-http");
|
||||
|
||||
const checkpoints = store.getTargetCheckpoints(t1Id, "2025-01-01T00:00:00.000Z", "2025-01-01T00:01:00.000Z");
|
||||
expect(checkpoints).toEqual([
|
||||
@@ -271,16 +269,14 @@ describe("ProbeStore", () => {
|
||||
});
|
||||
|
||||
test("getTargetDurations 返回成功检查耗时升序数组", () => {
|
||||
const targets = store.getTargets();
|
||||
const t1Id = targets[0]!.id;
|
||||
const t1Id = targetId(store, "test-http");
|
||||
|
||||
const durations = store.getTargetDurations(t1Id, "2025-01-01T00:00:00.000Z", "2025-01-01T00:01:00.000Z");
|
||||
expect(durations).toEqual([150.5, 300]);
|
||||
});
|
||||
|
||||
test("getRecentSamples 返回最近采样数据", () => {
|
||||
const targets = store.getTargets();
|
||||
const t1Id = targets[0]!.id;
|
||||
const t1Id = targetId(store, "test-http");
|
||||
|
||||
const samples = store.getRecentSamples(t1Id, 10);
|
||||
expect(Array.isArray(samples)).toBe(true);
|
||||
@@ -293,15 +289,17 @@ describe("ProbeStore", () => {
|
||||
|
||||
test("getAllRecentSamples 返回每个 target 的最近采样数据", () => {
|
||||
const sampleStore = new ProbeStore(join(tempDir, "all-samples.db"));
|
||||
const httpA: ResolvedHttpTarget = { ...httpTarget, name: "sample-http-a" };
|
||||
const httpA: ResolvedHttpTarget = { ...httpTarget, id: "sample-http-a", name: "sample-http-a" };
|
||||
const httpB: ResolvedHttpTarget = {
|
||||
...httpTarget,
|
||||
http: { ...httpTarget.http, url: "https://example.com/other" },
|
||||
id: "sample-http-b",
|
||||
name: "sample-http-b",
|
||||
};
|
||||
const httpEmpty: ResolvedHttpTarget = {
|
||||
...httpTarget,
|
||||
http: { ...httpTarget.http, url: "https://example.com/empty" },
|
||||
id: "sample-http-empty",
|
||||
name: "sample-http-empty",
|
||||
};
|
||||
sampleStore.syncTargets([httpA, httpB, httpEmpty]);
|
||||
@@ -360,7 +358,7 @@ describe("ProbeStore", () => {
|
||||
const closedStore = new ProbeStore(join(tempDir, "closed.db"));
|
||||
closedStore.close();
|
||||
expect(closedStore.getTargets()).toHaveLength(0);
|
||||
expect(closedStore.getTargetById(1)).toBeNull();
|
||||
expect(closedStore.getTargetById("closed-target")).toBeNull();
|
||||
});
|
||||
|
||||
test("删除 target 级联删除 check_results", () => {
|
||||
@@ -375,6 +373,7 @@ describe("ProbeStore", () => {
|
||||
method: "GET",
|
||||
url: "http://cascade.test",
|
||||
},
|
||||
id: "cascade-test",
|
||||
intervalMs: 30000,
|
||||
name: "cascade-test",
|
||||
timeoutMs: 10000,
|
||||
@@ -437,6 +436,7 @@ describe("ProbeStore", () => {
|
||||
method: "GET",
|
||||
url: "http://no.records",
|
||||
},
|
||||
id: "no-records",
|
||||
intervalMs: 30000,
|
||||
name: "no-records",
|
||||
timeoutMs: 10000,
|
||||
@@ -451,9 +451,8 @@ describe("ProbeStore", () => {
|
||||
});
|
||||
|
||||
test("getAllTargetWindowStats 返回所有 target 的窗口聚合统计", () => {
|
||||
const targets = store.getTargets();
|
||||
const t1Id = targets[0]!.id;
|
||||
const t2Id = targets[1]!.id;
|
||||
const t1Id = targetId(store, "test-http");
|
||||
const t2Id = targetId(store, "test-cmd");
|
||||
|
||||
const stats = store.getAllTargetWindowStats("2024-01-01T00:00:00.000Z", "2026-12-31T23:59:59.999Z");
|
||||
expect(stats).toBeInstanceOf(Map);
|
||||
@@ -484,6 +483,7 @@ describe("ProbeStore", () => {
|
||||
method: "GET",
|
||||
url: "http://no.stats",
|
||||
},
|
||||
id: "no-stats",
|
||||
intervalMs: 30000,
|
||||
name: "no-stats",
|
||||
timeoutMs: 10000,
|
||||
@@ -499,7 +499,7 @@ describe("ProbeStore", () => {
|
||||
|
||||
test("getAllTargetWindowStats 与 getTargetWindowStats 的 availability 精度一致", () => {
|
||||
const statsStore = new ProbeStore(join(tempDir, "stats-precision.db"));
|
||||
const target: ResolvedHttpTarget = { ...httpTarget, name: "stats-precision" };
|
||||
const target: ResolvedHttpTarget = { ...httpTarget, id: "stats-precision", name: "stats-precision" };
|
||||
statsStore.syncTargets([target]);
|
||||
const targetId = statsStore.getTargets()[0]!.id;
|
||||
|
||||
@@ -534,10 +534,11 @@ describe("ProbeStore", () => {
|
||||
|
||||
test("getDashboardIncidentStates 返回按 target 和 timestamp 升序排列的状态序列", () => {
|
||||
const incidentStore = new ProbeStore(join(tempDir, "dashboard-incidents.db"));
|
||||
const httpA: ResolvedHttpTarget = { ...httpTarget, name: "incident-http-a" };
|
||||
const httpA: ResolvedHttpTarget = { ...httpTarget, id: "incident-http-a", name: "incident-http-a" };
|
||||
const httpB: ResolvedHttpTarget = {
|
||||
...httpTarget,
|
||||
http: { ...httpTarget.http, url: "https://example.com/incident-b" },
|
||||
id: "incident-http-b",
|
||||
name: "incident-http-b",
|
||||
};
|
||||
incidentStore.syncTargets([httpA, httpB]);
|
||||
|
||||
Reference in New Issue
Block a user