- CheckResult: statusDetail -> observation (持久化) + detail (API 动态派生) - 存储: status_detail 列 -> observation TEXT (JSON) - CheckerDefinition: 新增 buildDetail(observation) 方法 - 各 checker 返回结构化 observation,API 层通过 registry 调用 buildDetail - HTTP: bodyPreview 在 status/header 失败时也提前采集 - UDP: observation 包含 durationMs,未响应归为 error failure - CMD: 超时/输出超限时保留已收集 observation - TCP: connectTimeMs 仅含连接建立耗时,不含 banner 等待 - 新增 buildDetail 单测和 mapCheckResult 覆盖测试 - 同步 openspec 主规范,归档 checker-observation 变更
186 lines
5.9 KiB
TypeScript
186 lines
5.9 KiB
TypeScript
import type { PrimaryTableCellParams, PrimaryTableCol } from "tdesign-react";
|
|
|
|
import { describe, expect, test } from "bun:test";
|
|
|
|
import type { TargetStatus } from "../../../src/shared/api";
|
|
|
|
import { createTargetTableColumns } from "../../../src/web/constants/target-table-columns";
|
|
|
|
interface TableFilter {
|
|
list?: Array<{ label: string; value: string }>;
|
|
type?: string;
|
|
}
|
|
|
|
function getColumn(columns: Array<PrimaryTableCol<TargetStatus>>, colKey: string): PrimaryTableCol<TargetStatus> {
|
|
const column = columns.find((item) => item.colKey === colKey);
|
|
expect(column).toBeDefined();
|
|
return column!;
|
|
}
|
|
|
|
function makeTarget(overrides: Partial<TargetStatus> = {}): TargetStatus {
|
|
return {
|
|
currentStreak: null,
|
|
description: null,
|
|
group: "default",
|
|
id: "1",
|
|
interval: "5s",
|
|
latestCheck: null,
|
|
name: "test",
|
|
recentSamples: [],
|
|
stats: { availability: 100, downChecks: 0, totalChecks: 0, upChecks: 0 },
|
|
target: "https://example.com",
|
|
type: "http",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("createTargetTableColumns", () => {
|
|
test("生成 7 个目标表格列", () => {
|
|
const columns = createTargetTableColumns(["http", "cmd"]);
|
|
|
|
expect(columns.map((column) => column.colKey)).toEqual([
|
|
"latestCheck.matched",
|
|
"name",
|
|
"type",
|
|
"stats.availability",
|
|
"recentSamples",
|
|
"currentStreak",
|
|
"latestCheck.durationMs",
|
|
]);
|
|
});
|
|
|
|
test("根据 checkerTypes 生成类型筛选器", () => {
|
|
const typeColumn = getColumn(createTargetTableColumns(["http", "cmd", "tcp"]), "type");
|
|
const filter = typeColumn.filter as TableFilter;
|
|
|
|
expect(filter.type).toBe("single");
|
|
expect(filter.list).toEqual([
|
|
{ label: "全部", value: "" },
|
|
{ label: "http", value: "http" },
|
|
{ label: "cmd", value: "cmd" },
|
|
{ label: "tcp", value: "tcp" },
|
|
]);
|
|
});
|
|
|
|
test("checkerTypes 为空时只保留全部选项", () => {
|
|
const typeColumn = getColumn(createTargetTableColumns([]), "type");
|
|
const filter = typeColumn.filter as TableFilter;
|
|
|
|
expect(filter.list).toEqual([{ label: "全部", value: "" }]);
|
|
});
|
|
|
|
test("类型列直接渲染原始 type 文本", () => {
|
|
const typeColumn = getColumn(createTargetTableColumns(["tcp"]), "type");
|
|
const renderCell = typeColumn.cell as (params: PrimaryTableCellParams<TargetStatus>) => {
|
|
props: { children: unknown };
|
|
};
|
|
const element = renderCell({
|
|
col: typeColumn,
|
|
colIndex: 2,
|
|
row: makeTarget({ type: "tcp" }),
|
|
rowIndex: 0,
|
|
});
|
|
|
|
expect(element.props.children).toBe("tcp");
|
|
});
|
|
|
|
test("连续状态列渲染 capped 标记", () => {
|
|
const streakColumn = getColumn(createTargetTableColumns(["http"]), "currentStreak");
|
|
const renderCell = streakColumn.cell as (params: PrimaryTableCellParams<TargetStatus>) => {
|
|
props: { children: unknown[] };
|
|
};
|
|
const element = renderCell({
|
|
col: streakColumn,
|
|
colIndex: 5,
|
|
row: makeTarget({ currentStreak: { capped: true, count: 30, up: false } }),
|
|
rowIndex: 0,
|
|
});
|
|
|
|
expect(streakColumn.title).toBe("连续(次)");
|
|
expect(element.props.children.join("")).toBe("▼ 30+");
|
|
});
|
|
|
|
test("延迟列超过 9999ms 时显示上限文案", () => {
|
|
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: {
|
|
detail: "200",
|
|
durationMs: 12000,
|
|
failure: null,
|
|
matched: true,
|
|
observation: null,
|
|
timestamp: "2026-01-01T00:00:00.000Z",
|
|
},
|
|
}),
|
|
rowIndex: 0,
|
|
});
|
|
|
|
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: {
|
|
detail: "200",
|
|
durationMs: 123,
|
|
failure: null,
|
|
matched: true,
|
|
observation: null,
|
|
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();
|
|
});
|
|
|
|
test("名称列 name 为 null 时显示 id", () => {
|
|
const nameColumn = getColumn(createTargetTableColumns(["http"]), "name");
|
|
const renderCell = nameColumn.cell as (params: PrimaryTableCellParams<TargetStatus>) => string;
|
|
const result = renderCell({
|
|
col: nameColumn,
|
|
colIndex: 1,
|
|
row: makeTarget({ id: "my-api", name: null }),
|
|
rowIndex: 0,
|
|
});
|
|
expect(result).toBe("my-api");
|
|
});
|
|
|
|
test("名称列 name 有值时显示 name", () => {
|
|
const nameColumn = getColumn(createTargetTableColumns(["http"]), "name");
|
|
const renderCell = nameColumn.cell as (params: PrimaryTableCellParams<TargetStatus>) => string;
|
|
const result = renderCell({
|
|
col: nameColumn,
|
|
colIndex: 1,
|
|
row: makeTarget({ id: "my-api", name: "我的 API" }),
|
|
rowIndex: 0,
|
|
});
|
|
expect(result).toBe("我的 API");
|
|
});
|
|
});
|