feat: 新增 target description 字段,收紧 id/name 长度,调整延迟列和名称列
This commit is contained in:
@@ -41,6 +41,7 @@ describe("API 路由", () => {
|
||||
store = new ProbeStore(join(tempDir, "test.db"));
|
||||
store.syncTargets([
|
||||
{
|
||||
description: null,
|
||||
group: "default",
|
||||
http: {
|
||||
headers: {},
|
||||
@@ -64,6 +65,7 @@ describe("API 路由", () => {
|
||||
exec: "echo",
|
||||
maxOutputBytes: 104857600,
|
||||
},
|
||||
description: null,
|
||||
group: "default",
|
||||
id: "test-b",
|
||||
intervalMs: 60000,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { bootstrap, type BootstrapDependencies } from "../../src/server/bootstra
|
||||
type ShutdownSignal = "SIGINT" | "SIGTERM";
|
||||
|
||||
const target: ResolvedTargetBase = {
|
||||
description: null,
|
||||
group: "default",
|
||||
id: "test",
|
||||
intervalMs: 30000,
|
||||
|
||||
@@ -1534,4 +1534,150 @@ targets:
|
||||
"无效的时长格式",
|
||||
);
|
||||
});
|
||||
|
||||
test("解析 description 字段", async () => {
|
||||
const configPath = join(tempDir, "description.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`targets:
|
||||
- id: "api-health"
|
||||
description: "检查生产 API 健康状态"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
);
|
||||
const config = await loadConfig(configPath);
|
||||
expect(config.targets[0]!.description).toBe("检查生产 API 健康状态");
|
||||
});
|
||||
|
||||
test("description 使用变量替换", async () => {
|
||||
const configPath = join(tempDir, "description-var.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`variables:
|
||||
env: "生产"
|
||||
targets:
|
||||
- id: "api-health"
|
||||
description: "\${env} 环境健康检查"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
);
|
||||
const config = await loadConfig(configPath);
|
||||
expect(config.targets[0]!.description).toBe("生产 环境健康检查");
|
||||
});
|
||||
|
||||
test("description 缺省为 null", async () => {
|
||||
const configPath = join(tempDir, "no-description.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`targets:
|
||||
- id: "api-health"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
);
|
||||
const config = await loadConfig(configPath);
|
||||
expect(config.targets[0]!.description).toBeNull();
|
||||
});
|
||||
|
||||
test("description 为空字符串通过", async () => {
|
||||
const configPath = join(tempDir, "empty-description.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`targets:
|
||||
- id: "api-health"
|
||||
description: ""
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
);
|
||||
const config = await loadConfig(configPath);
|
||||
expect(config.targets[0]!.description).toBe("");
|
||||
});
|
||||
|
||||
test("description 非字符串抛出错误", async () => {
|
||||
const configPath = join(tempDir, "bad-description-type.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`targets:
|
||||
- id: "api-health"
|
||||
description: 123
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("description");
|
||||
});
|
||||
|
||||
test("description 超过 500 字符抛出错误", async () => {
|
||||
const configPath = join(tempDir, "long-description.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`targets:
|
||||
- id: "api-health"
|
||||
description: "${"a".repeat(501)}"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("description");
|
||||
});
|
||||
|
||||
test("变量替换后 description 超长抛出错误", async () => {
|
||||
const configPath = join(tempDir, "var-long-description.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`variables:
|
||||
prefix: "${"x".repeat(490)}"
|
||||
targets:
|
||||
- id: "api-health"
|
||||
description: "\${prefix}${"a".repeat(15)}"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("description");
|
||||
});
|
||||
|
||||
test("id 超过 30 字符抛出错误", async () => {
|
||||
const configPath = join(tempDir, "long-id.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`targets:
|
||||
- id: "${"a".repeat(31)}"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("id");
|
||||
});
|
||||
|
||||
test("name 超过 30 字符抛出错误", async () => {
|
||||
const configPath = join(tempDir, "long-name.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`targets:
|
||||
- id: "test"
|
||||
name: "${"a".repeat(31)}"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/await-thenable
|
||||
await expect(loadConfig(configPath)).rejects.toThrow("name");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -55,6 +55,7 @@ function makeCommandTarget(name: string, overrides?: Partial<ResolvedCommandTarg
|
||||
exec: "bun",
|
||||
maxOutputBytes: 1024 * 1024,
|
||||
},
|
||||
description: null,
|
||||
group: "default",
|
||||
id: name,
|
||||
intervalMs: 60000,
|
||||
@@ -259,6 +260,7 @@ describe("ProbeEngine", () => {
|
||||
|
||||
try {
|
||||
const httpTarget: ResolvedHttpTarget = {
|
||||
description: null,
|
||||
group: "default",
|
||||
http: {
|
||||
headers: {},
|
||||
|
||||
@@ -30,6 +30,7 @@ function makeTarget(
|
||||
maxOutputBytes: 1024 * 1024,
|
||||
...cmd,
|
||||
},
|
||||
description: null,
|
||||
group: "default",
|
||||
id: "test-cmd",
|
||||
intervalMs: 60000,
|
||||
|
||||
@@ -19,6 +19,7 @@ function makeTarget(db: Partial<ResolvedDbTarget["db"]>, overrides?: Partial<Res
|
||||
url: "sqlite://:memory:",
|
||||
...db,
|
||||
},
|
||||
description: null,
|
||||
group: "default",
|
||||
id: "test-db",
|
||||
intervalMs: 60000,
|
||||
|
||||
@@ -155,6 +155,7 @@ describe("HttpChecker", () => {
|
||||
url?: string;
|
||||
}): ResolvedHttpTarget {
|
||||
return {
|
||||
description: null,
|
||||
expect: overrides.expect,
|
||||
group: "default",
|
||||
http: {
|
||||
|
||||
@@ -29,6 +29,7 @@ function targetId(store: ProbeStore, name: string): string {
|
||||
}
|
||||
|
||||
const httpTarget: ResolvedHttpTarget = {
|
||||
description: null,
|
||||
expect: { maxDurationMs: 3000, status: [200] },
|
||||
group: "default",
|
||||
http: {
|
||||
@@ -54,6 +55,7 @@ const commandTarget: ResolvedCommandTarget = {
|
||||
exec: "ping",
|
||||
maxOutputBytes: 104857600,
|
||||
},
|
||||
description: null,
|
||||
group: "default",
|
||||
id: "test-cmd",
|
||||
intervalMs: 60000,
|
||||
@@ -364,6 +366,7 @@ describe("ProbeStore", () => {
|
||||
test("删除 target 级联删除 check_results", () => {
|
||||
const cascadeStore = new ProbeStore(join(tempDir, "cascade.db"));
|
||||
const cascadeTarget: ResolvedHttpTarget = {
|
||||
description: null,
|
||||
group: "default",
|
||||
http: {
|
||||
headers: {},
|
||||
@@ -427,6 +430,7 @@ describe("ProbeStore", () => {
|
||||
const freshStore = new ProbeStore(join(tempDir, "fresh-map.db"));
|
||||
freshStore.syncTargets([
|
||||
{
|
||||
description: null,
|
||||
group: "default",
|
||||
http: {
|
||||
headers: {},
|
||||
@@ -474,6 +478,7 @@ describe("ProbeStore", () => {
|
||||
const freshStore = new ProbeStore(join(tempDir, "fresh-stats.db"));
|
||||
freshStore.syncTargets([
|
||||
{
|
||||
description: null,
|
||||
group: "default",
|
||||
http: {
|
||||
headers: {},
|
||||
@@ -662,4 +667,35 @@ describe("ProbeStore", () => {
|
||||
|
||||
pruneStore.close();
|
||||
});
|
||||
|
||||
test("syncTargets 持久化 description", () => {
|
||||
const descStore = new ProbeStore(join(tempDir, "desc.db"));
|
||||
const targetWithDesc: ResolvedHttpTarget = {
|
||||
...httpTarget,
|
||||
description: "检查 API 健康状态",
|
||||
id: "desc-test",
|
||||
name: "desc-test",
|
||||
};
|
||||
descStore.syncTargets([targetWithDesc]);
|
||||
const t = descStore.getTargets()[0]!;
|
||||
expect(t.description).toBe("检查 API 健康状态");
|
||||
descStore.close();
|
||||
});
|
||||
|
||||
test("未配置 description 时持久化为 null", () => {
|
||||
const noDescStore = new ProbeStore(join(tempDir, "no-desc.db"));
|
||||
noDescStore.syncTargets([{ ...httpTarget, description: null, id: "no-desc", name: "no-desc" }]);
|
||||
const t = noDescStore.getTargets()[0]!;
|
||||
expect(t.description).toBeNull();
|
||||
noDescStore.close();
|
||||
});
|
||||
|
||||
test("同步更新 description", () => {
|
||||
const updateDescStore = new ProbeStore(join(tempDir, "update-desc.db"));
|
||||
updateDescStore.syncTargets([{ ...httpTarget, description: "旧描述", id: "update-desc", name: "update-desc" }]);
|
||||
updateDescStore.syncTargets([{ ...httpTarget, description: "新描述", id: "update-desc", name: "update-desc" }]);
|
||||
const t = updateDescStore.getTargets()[0]!;
|
||||
expect(t.description).toBe("新描述");
|
||||
updateDescStore.close();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { OverviewTab } from "../../../src/web/components/OverviewTab";
|
||||
describe("OverviewTab", () => {
|
||||
const target: TargetStatus = {
|
||||
currentStreak: null,
|
||||
description: null,
|
||||
group: "default",
|
||||
id: "1",
|
||||
interval: "30s",
|
||||
|
||||
@@ -19,6 +19,7 @@ describe("TargetBoard", () => {
|
||||
const targets: TargetStatus[] = [
|
||||
{
|
||||
currentStreak: null,
|
||||
description: null,
|
||||
group: "default",
|
||||
id: "1",
|
||||
interval: "30s",
|
||||
@@ -31,6 +32,7 @@ describe("TargetBoard", () => {
|
||||
},
|
||||
{
|
||||
currentStreak: null,
|
||||
description: null,
|
||||
group: "production",
|
||||
id: "2",
|
||||
interval: "30s",
|
||||
|
||||
@@ -9,6 +9,7 @@ import { TargetDetailDrawer } from "../../../src/web/components/TargetDetailDraw
|
||||
describe("TargetDetailDrawer", () => {
|
||||
const target: TargetStatus = {
|
||||
currentStreak: null,
|
||||
description: null,
|
||||
group: "default",
|
||||
id: "1",
|
||||
interval: "30s",
|
||||
|
||||
@@ -15,6 +15,7 @@ describe("TargetGroup", () => {
|
||||
const targets: TargetStatus[] = [
|
||||
{
|
||||
currentStreak: null,
|
||||
description: null,
|
||||
group: "default",
|
||||
id: "1",
|
||||
interval: "30s",
|
||||
@@ -33,6 +34,7 @@ describe("TargetGroup", () => {
|
||||
},
|
||||
{
|
||||
currentStreak: null,
|
||||
description: null,
|
||||
group: "default",
|
||||
id: "2",
|
||||
interval: "30s",
|
||||
|
||||
@@ -20,6 +20,7 @@ function getColumn(columns: Array<PrimaryTableCol<TargetStatus>>, colKey: string
|
||||
function makeTarget(overrides: Partial<TargetStatus> = {}): TargetStatus {
|
||||
return {
|
||||
currentStreak: null,
|
||||
description: null,
|
||||
group: "default",
|
||||
id: "1",
|
||||
interval: "5s",
|
||||
@@ -119,7 +120,40 @@ describe("createTargetTableColumns", () => {
|
||||
rowIndex: 0,
|
||||
});
|
||||
|
||||
expect(element.props.children).toBe("9999+ms");
|
||||
expect(element.props.children).toBe("9999+");
|
||||
expect(element.props.className).toContain("latency-value");
|
||||
});
|
||||
|
||||
test("延迟列标题为 延迟(ms)", () => {
|
||||
const latencyColumn = getColumn(createTargetTableColumns(["http"]), "latestCheck.durationMs");
|
||||
expect(latencyColumn.title).toBe("延迟(ms)");
|
||||
});
|
||||
|
||||
test("延迟列正常值不包含 ms 单位", () => {
|
||||
const latencyColumn = getColumn(createTargetTableColumns(["http"]), "latestCheck.durationMs");
|
||||
const renderCell = latencyColumn.cell as (params: PrimaryTableCellParams<TargetStatus>) => {
|
||||
props: { children: string; className: string };
|
||||
};
|
||||
const element = renderCell({
|
||||
col: latencyColumn,
|
||||
colIndex: 6,
|
||||
row: makeTarget({
|
||||
latestCheck: {
|
||||
durationMs: 123,
|
||||
failure: null,
|
||||
matched: true,
|
||||
statusDetail: "200",
|
||||
timestamp: "2026-01-01T00:00:00.000Z",
|
||||
},
|
||||
}),
|
||||
rowIndex: 0,
|
||||
});
|
||||
expect(element.props.children).toBe("123");
|
||||
});
|
||||
|
||||
test("名称列无排序配置", () => {
|
||||
const nameColumn = getColumn(createTargetTableColumns(["http"]), "name");
|
||||
expect(nameColumn.sorter).toBeUndefined();
|
||||
expect(nameColumn.sortType).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,16 +2,12 @@ import { describe, expect, test } from "bun:test";
|
||||
|
||||
import type { TargetStatus } from "../../../src/shared/api";
|
||||
|
||||
import {
|
||||
availabilitySorter,
|
||||
latencySorter,
|
||||
nameSorter,
|
||||
statusSorter,
|
||||
} from "../../../src/web/constants/target-table-sorters";
|
||||
import { availabilitySorter, latencySorter, statusSorter } from "../../../src/web/constants/target-table-sorters";
|
||||
|
||||
function makeTarget(overrides: Partial<TargetStatus> = {}): TargetStatus {
|
||||
return {
|
||||
currentStreak: null,
|
||||
description: null,
|
||||
group: "default",
|
||||
id: "1",
|
||||
interval: "5s",
|
||||
@@ -97,18 +93,3 @@ describe("latencySorter", () => {
|
||||
expect(latencySorter(noLatency, hasLatency)).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("nameSorter", () => {
|
||||
test("按名称字母排序", () => {
|
||||
const a = makeTarget({ name: "Alpha" });
|
||||
const b = makeTarget({ name: "Beta" });
|
||||
expect(nameSorter(a, b)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
test("中文名称排序", () => {
|
||||
const a = makeTarget({ name: "百度" });
|
||||
const b = makeTarget({ name: "谷歌" });
|
||||
const result = nameSorter(a, b);
|
||||
expect(typeof result).toBe("number");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user