refactor: 统一 target name/description 可空语义,前端展示 fallback 到 id
- schema: name/description 允许省略或显式 null,TypeBox Union([Null, String]) - 类型: RawTargetConfig/ResolvedTargetBase/子类型/StoredTarget/TargetStatus name 改为 string | null - checker resolve: name: t.name ?? null,不再 fallback 到 id - 语义校验: 拒绝空字符串和纯空白 name - SQLite: targets.name 列改为可空 TEXT - 前端: 新增 getTargetDisplayName(target) 展示 name ?? id - 测试: 覆盖 name/description null 全场景,查找改为按 id - 文档: 更新 README/DEVELOPMENT 和 6 个 openspec specs
This commit is contained in:
@@ -24,10 +24,6 @@ beforeAll(() => {
|
||||
ensureRegistered();
|
||||
});
|
||||
|
||||
function targetId(store: ProbeStore, name: string): string {
|
||||
return store.getTargets().find((target) => target.name === name)!.id;
|
||||
}
|
||||
|
||||
const httpTarget: ResolvedHttpTarget = {
|
||||
description: null,
|
||||
expect: { maxDurationMs: 3000, status: [200] },
|
||||
@@ -87,11 +83,11 @@ describe("ProbeStore", () => {
|
||||
store.syncTargets([httpTarget, commandTarget]);
|
||||
const targets = store.getTargets();
|
||||
expect(targets).toHaveLength(2);
|
||||
expect(targets.map((target) => target.name).sort()).toEqual(["test-cmd", "test-http"]);
|
||||
expect(targets.map((target) => target.id).sort()).toEqual(["test-cmd", "test-http"]);
|
||||
});
|
||||
|
||||
test("http target 字段正确", () => {
|
||||
const t = store.getTargets().find((t) => t.name === "test-http")!;
|
||||
const t = store.getTargets().find((t) => t.id === "test-http")!;
|
||||
expect(t.type).toBe("http");
|
||||
expect(t.target).toBe("https://example.com/health");
|
||||
const config = JSON.parse(t.config) as {
|
||||
@@ -114,7 +110,7 @@ describe("ProbeStore", () => {
|
||||
});
|
||||
|
||||
test("cmd target 字段正确", () => {
|
||||
const t = store.getTargets().find((t) => t.name === "test-cmd")!;
|
||||
const t = store.getTargets().find((t) => t.id === "test-cmd")!;
|
||||
expect(t.type).toBe("cmd");
|
||||
expect(t.target).toBe("exec ping -c 1 localhost");
|
||||
const config = JSON.parse(t.config) as { args: string[]; cwd: string; exec: string; maxOutputBytes: number };
|
||||
@@ -133,7 +129,7 @@ describe("ProbeStore", () => {
|
||||
http: { ...httpTarget.http, url: "https://example.com/v2" },
|
||||
};
|
||||
store.syncTargets([updated, commandTarget]);
|
||||
const t = store.getTargets().find((t) => t.name === "test-http")!;
|
||||
const t = store.getTargets().find((t) => t.id === "test-http")!;
|
||||
expect(t.target).toBe("https://example.com/v2");
|
||||
expect(store.getTargets()).toHaveLength(2);
|
||||
});
|
||||
@@ -151,7 +147,7 @@ describe("ProbeStore", () => {
|
||||
});
|
||||
|
||||
test("getTargetById", () => {
|
||||
const found = store.getTargetById(targetId(store, "test-http"));
|
||||
const found = store.getTargetById("test-http");
|
||||
expect(found).toBeDefined();
|
||||
expect(found!.name).toBe("test-http");
|
||||
});
|
||||
@@ -162,14 +158,13 @@ describe("ProbeStore", () => {
|
||||
|
||||
test("写入 check result 并查询", () => {
|
||||
store.syncTargets([httpTarget, commandTarget]);
|
||||
const t1Id = targetId(store, "test-http");
|
||||
|
||||
store.insertCheckResult({
|
||||
durationMs: 150.5,
|
||||
failure: null,
|
||||
matched: true,
|
||||
statusDetail: "200 OK",
|
||||
targetId: t1Id,
|
||||
targetId: "test-http",
|
||||
timestamp: "2025-01-01T00:00:00.000Z",
|
||||
});
|
||||
|
||||
@@ -178,7 +173,7 @@ describe("ProbeStore", () => {
|
||||
failure: null,
|
||||
matched: true,
|
||||
statusDetail: "200 OK",
|
||||
targetId: t1Id,
|
||||
targetId: "test-http",
|
||||
timestamp: "2025-01-01T00:00:30.000Z",
|
||||
});
|
||||
|
||||
@@ -196,15 +191,15 @@ describe("ProbeStore", () => {
|
||||
failure,
|
||||
matched: false,
|
||||
statusDetail: null,
|
||||
targetId: t1Id,
|
||||
targetId: "test-http",
|
||||
timestamp: "2025-01-01T00:01:00.000Z",
|
||||
});
|
||||
|
||||
const history = store.getHistory(t1Id, "2025-01-01T00:00:00.000Z", "2025-12-31T23:59:59.999Z", 1, 10);
|
||||
const history = store.getHistory("test-http", "2025-01-01T00:00:00.000Z", "2025-12-31T23:59:59.999Z", 1, 10);
|
||||
expect(history.items).toHaveLength(3);
|
||||
expect(history.items[0]!.timestamp).toBe("2025-01-01T00:01:00.000Z");
|
||||
|
||||
const latest = store.getLatestCheck(t1Id)!;
|
||||
const latest = store.getLatestCheck("test-http")!;
|
||||
expect(latest.matched).toBe(0);
|
||||
expect(latest.failure).not.toBeNull();
|
||||
const parsedFailure = JSON.parse(latest.failure!) as CheckFailure;
|
||||
@@ -214,27 +209,23 @@ describe("ProbeStore", () => {
|
||||
});
|
||||
|
||||
test("getHistory 默认 limit=20", () => {
|
||||
const t1Id = targetId(store, "test-http");
|
||||
|
||||
for (let i = 0; i < 25; i++) {
|
||||
store.insertCheckResult({
|
||||
durationMs: 100 + i,
|
||||
failure: null,
|
||||
matched: true,
|
||||
statusDetail: "200 OK",
|
||||
targetId: t1Id,
|
||||
targetId: "test-http",
|
||||
timestamp: `2025-01-01T01:${String(i).padStart(2, "0")}:00.000Z`,
|
||||
});
|
||||
}
|
||||
|
||||
const history = store.getHistory(t1Id, "2025-01-01T00:00:00.000Z", "2025-12-31T23:59:59.999Z");
|
||||
const history = store.getHistory("test-http", "2025-01-01T00:00:00.000Z", "2025-12-31T23:59:59.999Z");
|
||||
expect(history.items).toHaveLength(20);
|
||||
});
|
||||
|
||||
test("getTargetWindowStats 按时间窗口计算基础计数", () => {
|
||||
const t1Id = targetId(store, "test-http");
|
||||
|
||||
const stats = store.getTargetWindowStats(t1Id, "2025-01-01T00:00:00.000Z", "2025-01-01T00:01:00.000Z");
|
||||
const stats = store.getTargetWindowStats("test-http", "2025-01-01T00:00:00.000Z", "2025-01-01T00:01:00.000Z");
|
||||
expect(stats.totalChecks).toBeGreaterThan(0);
|
||||
expect(stats.upChecks + stats.downChecks).toBe(stats.totalChecks);
|
||||
expect(stats.availability).toBeGreaterThanOrEqual(0);
|
||||
@@ -242,9 +233,7 @@ describe("ProbeStore", () => {
|
||||
});
|
||||
|
||||
test("无记录目标的窗口 stats", () => {
|
||||
const t2Id = targetId(store, "test-cmd");
|
||||
|
||||
const stats = store.getTargetWindowStats(t2Id, "2025-01-01T00:00:00.000Z", "2025-01-01T00:01:00.000Z");
|
||||
const stats = store.getTargetWindowStats("test-cmd", "2025-01-01T00:00:00.000Z", "2025-01-01T00:01:00.000Z");
|
||||
expect(stats.totalChecks).toBe(0);
|
||||
expect(stats.upChecks).toBe(0);
|
||||
expect(stats.downChecks).toBe(0);
|
||||
@@ -253,16 +242,14 @@ describe("ProbeStore", () => {
|
||||
|
||||
test("getLatestChecksMap 支持 Dashboard 组装当前状态", () => {
|
||||
const latestChecksMap = store.getLatestChecksMap();
|
||||
const latest = latestChecksMap.get(targetId(store, "test-http"));
|
||||
const latest = latestChecksMap.get("test-http");
|
||||
|
||||
expect(latest).toBeDefined();
|
||||
expect(latest!.timestamp).toBe("2025-01-01T01:24:00.000Z");
|
||||
});
|
||||
|
||||
test("getTargetCheckpoints 返回窗口内升序检查点", () => {
|
||||
const t1Id = targetId(store, "test-http");
|
||||
|
||||
const checkpoints = store.getTargetCheckpoints(t1Id, "2025-01-01T00:00:00.000Z", "2025-01-01T00:01:00.000Z");
|
||||
const checkpoints = store.getTargetCheckpoints("test-http", "2025-01-01T00:00:00.000Z", "2025-01-01T00:01:00.000Z");
|
||||
expect(checkpoints).toEqual([
|
||||
{ duration_ms: 150.5, matched: 1, timestamp: "2025-01-01T00:00:00.000Z" },
|
||||
{ duration_ms: 300, matched: 1, timestamp: "2025-01-01T00:00:30.000Z" },
|
||||
@@ -271,16 +258,12 @@ describe("ProbeStore", () => {
|
||||
});
|
||||
|
||||
test("getTargetDurations 返回成功检查耗时升序数组", () => {
|
||||
const t1Id = targetId(store, "test-http");
|
||||
|
||||
const durations = store.getTargetDurations(t1Id, "2025-01-01T00:00:00.000Z", "2025-01-01T00:01:00.000Z");
|
||||
const durations = store.getTargetDurations("test-http", "2025-01-01T00:00:00.000Z", "2025-01-01T00:01:00.000Z");
|
||||
expect(durations).toEqual([150.5, 300]);
|
||||
});
|
||||
|
||||
test("getRecentSamples 返回最近采样数据", () => {
|
||||
const t1Id = targetId(store, "test-http");
|
||||
|
||||
const samples = store.getRecentSamples(t1Id, 10);
|
||||
const samples = store.getRecentSamples("test-http", 10);
|
||||
expect(Array.isArray(samples)).toBe(true);
|
||||
expect(samples.length).toBeGreaterThan(0);
|
||||
for (const sample of samples) {
|
||||
@@ -306,9 +289,9 @@ describe("ProbeStore", () => {
|
||||
};
|
||||
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;
|
||||
const targetAId = targets.find((t) => t.id === "sample-http-a")!.id;
|
||||
const targetBId = targets.find((t) => t.id === "sample-http-b")!.id;
|
||||
const emptyTargetId = targets.find((t) => t.id === "sample-http-empty")!.id;
|
||||
|
||||
for (const [index, timestamp] of [
|
||||
"2025-01-01T00:00:00.000Z",
|
||||
@@ -455,19 +438,16 @@ describe("ProbeStore", () => {
|
||||
});
|
||||
|
||||
test("getAllTargetWindowStats 返回所有 target 的窗口聚合统计", () => {
|
||||
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);
|
||||
|
||||
const stats1 = stats.get(t1Id);
|
||||
const stats1 = stats.get("test-http");
|
||||
expect(stats1).toBeDefined();
|
||||
expect(stats1!.totalChecks).toBeGreaterThan(0);
|
||||
expect(stats1!.upChecks + stats1!.downChecks).toBe(stats1!.totalChecks);
|
||||
expect(stats1!.availability).toBeGreaterThanOrEqual(0);
|
||||
|
||||
const stats2 = stats.get(t2Id);
|
||||
const stats2 = stats.get("test-cmd");
|
||||
if (stats2) {
|
||||
expect(stats2.totalChecks).toBe(0);
|
||||
expect(stats2.availability).toBe(0);
|
||||
@@ -548,8 +528,8 @@ describe("ProbeStore", () => {
|
||||
};
|
||||
incidentStore.syncTargets([httpA, httpB]);
|
||||
const targets = incidentStore.getTargets();
|
||||
const targetAId = targets.find((target) => target.name === "incident-http-a")!.id;
|
||||
const targetBId = targets.find((target) => target.name === "incident-http-b")!.id;
|
||||
const targetAId = targets.find((target) => target.id === "incident-http-a")!.id;
|
||||
const targetBId = targets.find((target) => target.id === "incident-http-b")!.id;
|
||||
|
||||
incidentStore.insertCheckResult({
|
||||
durationMs: 100,
|
||||
@@ -698,4 +678,12 @@ describe("ProbeStore", () => {
|
||||
expect(t.description).toBe("新描述");
|
||||
updateDescStore.close();
|
||||
});
|
||||
|
||||
test("name 为 null 时持久化为 null", () => {
|
||||
const nullNameStore = new ProbeStore(join(tempDir, "null-name.db"));
|
||||
nullNameStore.syncTargets([{ ...httpTarget, id: "null-name", name: null }]);
|
||||
const t = nullNameStore.getTargets()[0]!;
|
||||
expect(t.name).toBeNull();
|
||||
nullNameStore.close();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user