chore: 强化代码质量与风格检查体系
ESLint 升级到 recommended-type-checked + stylistic-type-checked, 引入 perfectionist 导入排序和 import 插件导入验证。 Prettier 显式声明全部格式化参数,消除跨环境差异。 TypeScript 启用 noUnusedLocals 和 noPropertyAccessFromIndexSignature。 完善 ignore 列表,排除 .agents/、bun.lock、data/ 等。 引入 husky + lint-staged(pre-commit)+ commitlint(commit-msg)。 更新 DEVELOPMENT.md 代码质量章节。 修复所有新增规则检测到的类型和风格违规。
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
||||
import { createFetchHandler, type StaticAssets } from "../../src/server/app";
|
||||
import { ProbeStore } from "../../src/server/checker/store";
|
||||
import type { HistoryResponse, SummaryResponse, TargetStatus, HealthResponse } from "../../src/shared/api";
|
||||
import { mkdir, rm } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import type { HealthResponse, HistoryResponse, SummaryResponse, TargetStatus } from "../../src/shared/api";
|
||||
|
||||
import { createFetchHandler, type StaticAssets } from "../../src/server/app";
|
||||
import { checkerRegistry } from "../../src/server/checker/runner";
|
||||
import { HttpChecker } from "../../src/server/checker/runner/http/runner";
|
||||
import { CommandChecker } from "../../src/server/checker/runner/command/runner";
|
||||
import { HttpChecker } from "../../src/server/checker/runner/http/runner";
|
||||
import { ProbeStore } from "../../src/server/checker/store";
|
||||
|
||||
function ensureRegistered() {
|
||||
if (!checkerRegistry.supportedTypes.includes("http")) {
|
||||
@@ -21,12 +23,12 @@ beforeAll(() => {
|
||||
});
|
||||
|
||||
const staticAssets: StaticAssets = {
|
||||
indexHtml: new Blob(['<!doctype html><title>DiAL</title><div id="root"></div>'], {
|
||||
type: "text/html",
|
||||
}),
|
||||
files: {
|
||||
"/assets/app.js": new Blob(["console.log('app');"], { type: "text/javascript" }),
|
||||
},
|
||||
indexHtml: new Blob(['<!doctype html><title>DiAL</title><div id="root"></div>'], {
|
||||
type: "text/html",
|
||||
}),
|
||||
};
|
||||
|
||||
describe("API 路由", () => {
|
||||
@@ -40,57 +42,57 @@ describe("API 路由", () => {
|
||||
store = new ProbeStore(join(tempDir, "test.db"));
|
||||
store.syncTargets([
|
||||
{
|
||||
type: "http",
|
||||
name: "test-a",
|
||||
group: "default",
|
||||
http: {
|
||||
url: "http://a.com",
|
||||
method: "GET",
|
||||
headers: {},
|
||||
maxBodyBytes: 104857600,
|
||||
method: "GET",
|
||||
url: "http://a.com",
|
||||
},
|
||||
intervalMs: 30000,
|
||||
name: "test-a",
|
||||
timeoutMs: 10000,
|
||||
type: "http",
|
||||
},
|
||||
{
|
||||
type: "command",
|
||||
name: "test-b",
|
||||
group: "default",
|
||||
command: {
|
||||
exec: "echo",
|
||||
args: ["hello"],
|
||||
cwd: "/tmp",
|
||||
env: {},
|
||||
exec: "echo",
|
||||
maxOutputBytes: 104857600,
|
||||
},
|
||||
group: "default",
|
||||
intervalMs: 60000,
|
||||
name: "test-b",
|
||||
timeoutMs: 5000,
|
||||
type: "command",
|
||||
},
|
||||
]);
|
||||
|
||||
const targets = store.getTargets();
|
||||
store.insertCheckResult({
|
||||
durationMs: 150,
|
||||
failure: null,
|
||||
matched: true,
|
||||
statusDetail: "200 OK",
|
||||
targetId: targets[0]!.id,
|
||||
timestamp: "2025-01-01T00:00:00.000Z",
|
||||
matched: true,
|
||||
durationMs: 150,
|
||||
statusDetail: "200 OK",
|
||||
failure: null,
|
||||
});
|
||||
store.insertCheckResult({
|
||||
durationMs: null,
|
||||
failure: {
|
||||
actual: 500,
|
||||
expected: 200,
|
||||
kind: "error",
|
||||
message: "状态码不匹配",
|
||||
path: "$.status",
|
||||
phase: "status",
|
||||
},
|
||||
matched: false,
|
||||
statusDetail: null,
|
||||
targetId: targets[0]!.id,
|
||||
timestamp: "2025-01-01T00:00:30.000Z",
|
||||
matched: false,
|
||||
durationMs: null,
|
||||
statusDetail: null,
|
||||
failure: {
|
||||
kind: "error",
|
||||
phase: "status",
|
||||
path: "$.status",
|
||||
expected: 200,
|
||||
actual: 500,
|
||||
message: "状态码不匹配",
|
||||
},
|
||||
});
|
||||
|
||||
fetchHandler = createFetchHandler({ mode: "test", staticAssets, store });
|
||||
@@ -98,11 +100,11 @@ describe("API 路由", () => {
|
||||
|
||||
afterAll(async () => {
|
||||
store.close();
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
await rm(tempDir, { force: true, recursive: true });
|
||||
});
|
||||
|
||||
test("/health 返回健康检查", async () => {
|
||||
const response = await fetchHandler(new Request("http://localhost/health"));
|
||||
const response = fetchHandler(new Request("http://localhost/health"));
|
||||
const body = (await response.json()) as HealthResponse;
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
@@ -111,7 +113,7 @@ describe("API 路由", () => {
|
||||
});
|
||||
|
||||
test("/api/summary 返回总览统计", async () => {
|
||||
const response = await fetchHandler(new Request("http://localhost/api/summary"));
|
||||
const response = fetchHandler(new Request("http://localhost/api/summary"));
|
||||
const body = (await response.json()) as SummaryResponse;
|
||||
expect(response.status).toBe(200);
|
||||
expect(body.total).toBe(2);
|
||||
@@ -122,7 +124,7 @@ describe("API 路由", () => {
|
||||
});
|
||||
|
||||
test("/api/targets 返回目标列表", async () => {
|
||||
const response = await fetchHandler(new Request("http://localhost/api/targets"));
|
||||
const response = fetchHandler(new Request("http://localhost/api/targets"));
|
||||
const body = (await response.json()) as TargetStatus[];
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
@@ -150,7 +152,7 @@ describe("API 路由", () => {
|
||||
const targets = store.getTargets();
|
||||
const from = "2024-01-01T00:00:00.000Z";
|
||||
const to = "2026-12-31T23:59:59.999Z";
|
||||
const response = await fetchHandler(
|
||||
const response = fetchHandler(
|
||||
new Request(`http://localhost/api/targets/${targets[0]!.id}/history?from=${from}&to=${to}`),
|
||||
);
|
||||
const body = (await response.json()) as HistoryResponse;
|
||||
@@ -168,7 +170,7 @@ describe("API 路由", () => {
|
||||
const targets = store.getTargets();
|
||||
const from = "2024-01-01T00:00:00.000Z";
|
||||
const to = "2026-12-31T23:59:59.999Z";
|
||||
const response = await fetchHandler(
|
||||
const response = fetchHandler(
|
||||
new Request(`http://localhost/api/targets/${targets[0]!.id}/history?from=${from}&to=${to}&pageSize=1`),
|
||||
);
|
||||
const body = (await response.json()) as HistoryResponse;
|
||||
@@ -182,90 +184,92 @@ describe("API 路由", () => {
|
||||
const targets = store.getTargets();
|
||||
const from = "2024-01-01T00:00:00.000Z";
|
||||
const to = "2026-12-31T23:59:59.999Z";
|
||||
const response = await fetchHandler(
|
||||
const response = fetchHandler(
|
||||
new Request(`http://localhost/api/targets/${targets[0]!.id}/trend?from=${from}&to=${to}`),
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as unknown[];
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(Array.isArray(body)).toBe(true);
|
||||
});
|
||||
|
||||
test("查询不存在的目标返回 404", async () => {
|
||||
const response = await fetchHandler(
|
||||
const response = fetchHandler(
|
||||
new Request(
|
||||
"http://localhost/api/targets/99999/history?from=2024-01-01T00:00:00.000Z&to=2026-12-31T23:59:59.999Z",
|
||||
),
|
||||
);
|
||||
const body = await response.json();
|
||||
const body = (await response.json()) as Record<string, unknown>;
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(body.error).toBe("Target not found");
|
||||
expect(body["error"]).toBe("Target not found");
|
||||
});
|
||||
|
||||
test("history 缺少 from/to 参数返回 400", async () => {
|
||||
const targets = store.getTargets();
|
||||
const response = await fetchHandler(new Request(`http://localhost/api/targets/${targets[0]!.id}/history`));
|
||||
const body = await response.json();
|
||||
const response = fetchHandler(new Request(`http://localhost/api/targets/${targets[0]!.id}/history`));
|
||||
const body = (await response.json()) as Record<string, unknown>;
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(body.error).toContain("from and to");
|
||||
expect(body["error"]).toContain("from and to");
|
||||
});
|
||||
|
||||
test("trend 缺少 from/to 参数返回 400", async () => {
|
||||
const targets = store.getTargets();
|
||||
const response = await fetchHandler(new Request(`http://localhost/api/targets/${targets[0]!.id}/trend`));
|
||||
const body = await response.json();
|
||||
const response = fetchHandler(new Request(`http://localhost/api/targets/${targets[0]!.id}/trend`));
|
||||
const body = (await response.json()) as Record<string, unknown>;
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(body.error).toContain("from and to");
|
||||
expect(body["error"]).toContain("from and to");
|
||||
});
|
||||
|
||||
test("无效目标 ID 返回 400", async () => {
|
||||
const response = await fetchHandler(new Request("http://localhost/api/targets/abc/history"));
|
||||
const body = await response.json();
|
||||
test("trend 无效 targetId 返回 400", async () => {
|
||||
const response = fetchHandler(
|
||||
new Request("http://localhost/api/targets/invalid/trend?from=2024-01-01T00:00:00Z&to=2024-01-02T00:00:00Z"),
|
||||
);
|
||||
const body = (await response.json()) as Record<string, unknown>;
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(body.error).toBe("Invalid target ID");
|
||||
expect(body["error"]).toBe("Invalid target ID");
|
||||
});
|
||||
|
||||
test("未知 /api/* 返回 404", async () => {
|
||||
const response = await fetchHandler(new Request("http://localhost/api/missing"));
|
||||
test("未知 /api/* 返回 404", () => {
|
||||
const response = fetchHandler(new Request("http://localhost/api/missing"));
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
|
||||
test("HEAD 请求返回 headers 无 body", async () => {
|
||||
const response = await fetchHandler(new Request("http://localhost/api/summary", { method: "HEAD" }));
|
||||
const response = fetchHandler(new Request("http://localhost/api/summary", { method: "HEAD" }));
|
||||
const body = await response.text();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(body).toBe("");
|
||||
});
|
||||
|
||||
test("不支持的 method 返回 405", async () => {
|
||||
const response = await fetchHandler(new Request("http://localhost/api/summary", { method: "POST" }));
|
||||
test("不支持的 method 返回 405", () => {
|
||||
const response = fetchHandler(new Request("http://localhost/api/summary", { method: "POST" }));
|
||||
|
||||
expect(response.status).toBe(405);
|
||||
expect(response.headers.get("allow")).toBe("GET, HEAD");
|
||||
});
|
||||
|
||||
test("生产响应包含安全 headers", async () => {
|
||||
test("生产响应包含安全 headers", () => {
|
||||
const prodHandler = createFetchHandler({ mode: "production", staticAssets, store });
|
||||
const response = await prodHandler(new Request("http://localhost/api/summary"));
|
||||
const response = prodHandler(new Request("http://localhost/api/summary"));
|
||||
|
||||
expect(response.headers.get("x-content-type-options")).toBe("nosniff");
|
||||
expect(response.headers.get("referrer-policy")).toBe("strict-origin-when-cross-origin");
|
||||
});
|
||||
|
||||
test("静态资源和 SPA fallback 正常工作", async () => {
|
||||
const root = await fetchHandler(new Request("http://localhost/"));
|
||||
test("静态资源和 SPA fallback 正常工作", () => {
|
||||
const root = fetchHandler(new Request("http://localhost/"));
|
||||
expect(root.status).toBe(200);
|
||||
|
||||
const fallback = await fetchHandler(new Request("http://localhost/dashboard"));
|
||||
const fallback = fetchHandler(new Request("http://localhost/dashboard"));
|
||||
expect(fallback.status).toBe(200);
|
||||
|
||||
const asset = await fetchHandler(new Request("http://localhost/assets/app.js"));
|
||||
const asset = fetchHandler(new Request("http://localhost/assets/app.js"));
|
||||
expect(asset.status).toBe(200);
|
||||
});
|
||||
|
||||
@@ -274,12 +278,12 @@ describe("API 路由", () => {
|
||||
const t1Id = targets[0]!.id;
|
||||
|
||||
store.insertCheckResult({
|
||||
durationMs: 100,
|
||||
failure: { kind: "error", message: "test", path: "$", phase: "body" },
|
||||
matched: false,
|
||||
statusDetail: "200 OK",
|
||||
targetId: t1Id,
|
||||
timestamp: "2025-06-01T00:00:00.000Z",
|
||||
matched: false,
|
||||
durationMs: 100,
|
||||
statusDetail: "200 OK",
|
||||
failure: { kind: "error", phase: "body", path: "$", message: "test" },
|
||||
});
|
||||
|
||||
(store as unknown as { db: { prepare: (sql: string) => { run: (...args: unknown[]) => void } } }).db
|
||||
@@ -288,9 +292,7 @@ describe("API 路由", () => {
|
||||
|
||||
const from = "2025-06-01T00:00:00.000Z";
|
||||
const to = "2025-06-01T23:59:59.999Z";
|
||||
const response = await fetchHandler(
|
||||
new Request(`http://localhost/api/targets/${t1Id}/history?from=${from}&to=${to}`),
|
||||
);
|
||||
const response = fetchHandler(new Request(`http://localhost/api/targets/${t1Id}/history?from=${from}&to=${to}`));
|
||||
const body = (await response.json()) as HistoryResponse;
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
Reference in New Issue
Block a user