feat: 版本管理,package.json 唯一版本源、/api/meta 返回版本、Dashboard Header 展示版本号
This commit is contained in:
70
tests/scripts/build.test.ts
Normal file
70
tests/scripts/build.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
||||
import { mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { validateVersion } from "../../scripts/bump-version-logic";
|
||||
|
||||
describe("build 版本注入", () => {
|
||||
test("validateVersion 接受有效版本", () => {
|
||||
expect(() => validateVersion("0.1.0")).not.toThrow();
|
||||
expect(() => validateVersion("1.2.3")).not.toThrow();
|
||||
});
|
||||
|
||||
test("validateVersion 拒绝无效版本", () => {
|
||||
expect(() => validateVersion("invalid")).toThrow();
|
||||
expect(() => validateVersion("1.0.0-beta.1")).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("server-entry 版本字面量", () => {
|
||||
let tempDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = join(tmpdir(), `build-version-test-${Date.now()}`);
|
||||
await mkdir(tempDir, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await rm(tempDir, { force: true, recursive: true });
|
||||
});
|
||||
|
||||
test("生成的 server-entry 包含版本字面量", async () => {
|
||||
const version = "0.1.0";
|
||||
const serverEntryTs = [
|
||||
`import { bootstrap } from "../src/server/bootstrap";`,
|
||||
`import { readRuntimeConfig } from "../src/server/config";`,
|
||||
`import { staticAssets } from "./static-assets";`,
|
||||
"",
|
||||
`const APP_VERSION = "${version}" as const;`,
|
||||
"",
|
||||
`async function main() {`,
|
||||
` const { configPath } = readRuntimeConfig();`,
|
||||
` await bootstrap({ configPath, mode: "production", staticAssets, version: APP_VERSION });`,
|
||||
`}`,
|
||||
"",
|
||||
`void main().catch((error) => {`,
|
||||
` console.error("启动失败:", error instanceof Error ? error.message : error);`,
|
||||
` process.exit(1);`,
|
||||
`});`,
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
await writeFile(join(tempDir, "server-entry.ts"), serverEntryTs);
|
||||
|
||||
const content = await Bun.file(join(tempDir, "server-entry.ts")).text();
|
||||
expect(content).toContain(`const APP_VERSION = "${version}"`);
|
||||
expect(content).toContain("version: APP_VERSION");
|
||||
});
|
||||
|
||||
test("版本字面量不依赖外部 package.json", async () => {
|
||||
const serverEntryTs = [`const APP_VERSION = "0.1.0" as const;`].join("\n");
|
||||
|
||||
await writeFile(join(tempDir, "server-entry.ts"), serverEntryTs);
|
||||
|
||||
const content = await Bun.file(join(tempDir, "server-entry.ts")).text();
|
||||
expect(content).not.toContain("package.json");
|
||||
expect(content).not.toContain("Bun.file");
|
||||
expect(content).toContain('"0.1.0"');
|
||||
});
|
||||
});
|
||||
94
tests/scripts/bump-version.test.ts
Normal file
94
tests/scripts/bump-version.test.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
||||
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { bumpVersion, formatVersion, parseVersion, validateVersion } from "../../scripts/bump-version-logic";
|
||||
|
||||
describe("版本解析与校验", () => {
|
||||
test("parseVersion 解析有效版本", () => {
|
||||
expect(parseVersion("0.1.0")).toEqual([0, 1, 0]);
|
||||
expect(parseVersion("1.2.3")).toEqual([1, 2, 3]);
|
||||
expect(parseVersion("10.20.30")).toEqual([10, 20, 30]);
|
||||
});
|
||||
|
||||
test("parseVersion 拒绝无效版本", () => {
|
||||
expect(() => parseVersion("invalid")).toThrow();
|
||||
expect(() => parseVersion("1.2")).toThrow();
|
||||
expect(() => parseVersion("1.2.3.4")).toThrow();
|
||||
expect(() => parseVersion("1.2.a")).toThrow();
|
||||
});
|
||||
|
||||
test("validateVersion 接受有效版本", () => {
|
||||
expect(() => validateVersion("0.1.0")).not.toThrow();
|
||||
expect(() => validateVersion("1.2.3")).not.toThrow();
|
||||
expect(() => validateVersion("10.20.30")).not.toThrow();
|
||||
});
|
||||
|
||||
test("validateVersion 拒绝无效版本", () => {
|
||||
expect(() => validateVersion("")).toThrow();
|
||||
expect(() => validateVersion("invalid")).toThrow();
|
||||
expect(() => validateVersion("1.2")).toThrow();
|
||||
expect(() => validateVersion("1.2.3.4")).toThrow();
|
||||
expect(() => validateVersion("1.0.0-beta.1")).toThrow();
|
||||
expect(() => validateVersion("v1.0.0")).toThrow();
|
||||
});
|
||||
|
||||
test("formatVersion 格式化版本", () => {
|
||||
expect(formatVersion(0, 1, 0)).toBe("0.1.0");
|
||||
expect(formatVersion(1, 2, 3)).toBe("1.2.3");
|
||||
expect(formatVersion(10, 20, 30)).toBe("10.20.30");
|
||||
});
|
||||
});
|
||||
|
||||
describe("版本升迁逻辑", () => {
|
||||
test("bumpVersion patch 升迁", () => {
|
||||
expect(bumpVersion("1.2.3", "patch")).toBe("1.2.4");
|
||||
expect(bumpVersion("0.1.0", "patch")).toBe("0.1.1");
|
||||
expect(bumpVersion("0.0.1", "patch")).toBe("0.0.2");
|
||||
});
|
||||
|
||||
test("bumpVersion minor 危迁", () => {
|
||||
expect(bumpVersion("1.2.3", "minor")).toBe("1.3.0");
|
||||
expect(bumpVersion("0.1.0", "minor")).toBe("0.2.0");
|
||||
expect(bumpVersion("0.0.1", "minor")).toBe("0.1.0");
|
||||
});
|
||||
|
||||
test("bumpVersion major 危迁", () => {
|
||||
expect(bumpVersion("1.2.3", "major")).toBe("2.0.0");
|
||||
expect(bumpVersion("0.1.0", "major")).toBe("1.0.0");
|
||||
expect(bumpVersion("0.0.1", "major")).toBe("1.0.0");
|
||||
});
|
||||
|
||||
test("bumpVersion set 设置版本", () => {
|
||||
expect(bumpVersion("1.2.3", "set", "2.0.0")).toBe("2.0.0");
|
||||
expect(bumpVersion("0.1.0", "set", "0.2.0")).toBe("0.2.0");
|
||||
});
|
||||
|
||||
test("bumpVersion set 拒绝无效版本", () => {
|
||||
expect(() => bumpVersion("1.2.3", "set", "invalid")).toThrow();
|
||||
expect(() => bumpVersion("1.2.3", "set", "1.0.0-beta.1")).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("写入前保持原版本", () => {
|
||||
let tempFile: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tempFile = join(tmpdir(), `bump-version-test-${Date.now()}.json`);
|
||||
writeFileSync(tempFile, JSON.stringify({ name: "test", version: "1.2.3" }, null, 2) + "\n");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (existsSync(tempFile)) {
|
||||
unlinkSync(tempFile);
|
||||
}
|
||||
});
|
||||
|
||||
test("set 无效版本不修改文件", () => {
|
||||
const original = readFileSync(tempFile, "utf-8");
|
||||
expect(() => bumpVersion("1.2.3", "set", "invalid")).toThrow();
|
||||
const after = readFileSync(tempFile, "utf-8");
|
||||
expect(after).toBe(original);
|
||||
});
|
||||
});
|
||||
@@ -175,6 +175,7 @@ describe("API 路由", () => {
|
||||
config: { host: "127.0.0.1", port: 0 },
|
||||
mode: "test",
|
||||
store,
|
||||
version: "0.1.0",
|
||||
});
|
||||
baseUrl = `http://127.0.0.1:${server.port}`;
|
||||
});
|
||||
@@ -235,7 +236,7 @@ describe("API 路由", () => {
|
||||
expect(invalidLimit.status).toBe(400);
|
||||
});
|
||||
|
||||
test("/api/meta 返回 checker 类型列表", async () => {
|
||||
test("/api/meta 返回 checker 类型列表和版本号", async () => {
|
||||
const response = await fetch(`${baseUrl}/api/meta`);
|
||||
const body = (await response.json()) as MetaResponse;
|
||||
|
||||
@@ -243,6 +244,7 @@ describe("API 路由", () => {
|
||||
expect(body.checkerTypes).toEqual(checkerRegistry.supportedTypes);
|
||||
expect(body.checkerTypes).toContain("http");
|
||||
expect(body.checkerTypes).toContain("cmd");
|
||||
expect(body.version).toBe("0.1.0");
|
||||
});
|
||||
|
||||
test("不支持的 method 在有 API 通配符时返回 404", async () => {
|
||||
@@ -410,6 +412,7 @@ describe("API 路由", () => {
|
||||
config: { host: "127.0.0.1", port: 0 },
|
||||
mode: "production",
|
||||
store,
|
||||
version: "0.1.0",
|
||||
});
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${prodServer.port}/api/dashboard`);
|
||||
|
||||
@@ -77,6 +77,7 @@ function createHarness(overrides: BootstrapDependencies = {}) {
|
||||
startServer(options) {
|
||||
expect(options.config).toEqual({ host: config.host, port: config.port });
|
||||
expect(options.store).toBe(store);
|
||||
expect(options.version).toBe("0.1.0");
|
||||
calls.push(`startServer:${options.mode}`);
|
||||
},
|
||||
...overrides,
|
||||
@@ -89,7 +90,7 @@ describe("bootstrap", () => {
|
||||
test("开发模式执行完整启动序列", async () => {
|
||||
const { calls, dependencies } = createHarness();
|
||||
|
||||
await bootstrap({ configPath: "/tmp/probes.yaml", mode: "development" }, dependencies);
|
||||
await bootstrap({ configPath: "/tmp/probes.yaml", mode: "development", version: "0.1.0" }, dependencies);
|
||||
|
||||
expect(calls).toEqual([
|
||||
"loadConfig:/tmp/probes.yaml",
|
||||
@@ -106,7 +107,7 @@ describe("bootstrap", () => {
|
||||
test("收到退出信号时停止 engine 并关闭 store", async () => {
|
||||
const { calls, dependencies, shutdownHandlers } = createHarness();
|
||||
|
||||
await bootstrap({ configPath: "/tmp/probes.yaml", mode: "development" }, dependencies);
|
||||
await bootstrap({ configPath: "/tmp/probes.yaml", mode: "development", version: "0.1.0" }, dependencies);
|
||||
|
||||
expect(() => shutdownHandlers.get("SIGINT")!()).toThrow("exit:0");
|
||||
|
||||
@@ -122,7 +123,7 @@ describe("bootstrap", () => {
|
||||
|
||||
let error: unknown;
|
||||
try {
|
||||
await bootstrap({ configPath: "/tmp/probes.yaml", mode: "development" }, dependencies);
|
||||
await bootstrap({ configPath: "/tmp/probes.yaml", mode: "development", version: "0.1.0" }, dependencies);
|
||||
} catch (caught) {
|
||||
error = caught;
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ function installMatchMedia(initialMatches: boolean) {
|
||||
void vi.mock("../../../src/web/hooks/use-queries", () => ({
|
||||
useDashboard: vi.fn(() => createDashboardResult()),
|
||||
useMeta: vi.fn(() => ({
|
||||
data: { checkerTypes: ["http", "cmd"] },
|
||||
data: { checkerTypes: ["http", "cmd"], version: "0.1.0" },
|
||||
})),
|
||||
}));
|
||||
|
||||
@@ -208,4 +208,25 @@ describe("App", () => {
|
||||
act(() => matchMediaController.setMatches(true));
|
||||
await waitFor(() => expect(document.documentElement.getAttribute("theme-mode")).toBe("dark"));
|
||||
});
|
||||
|
||||
test("Header 展示版本号", () => {
|
||||
render(<App />);
|
||||
expect(screen.getByText("v0.1.0")).not.toBeNull();
|
||||
});
|
||||
|
||||
test("缺失版本时不展示版本占位", () => {
|
||||
const { useMeta } = require("../../../src/web/hooks/use-queries");
|
||||
useMeta.mockReturnValue({
|
||||
data: { checkerTypes: ["http", "cmd"] },
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
expect(screen.queryByText(/v\d+\.\d+\.\d+/)).toBeNull();
|
||||
});
|
||||
|
||||
test("复用 useMeta 查询结果", () => {
|
||||
const { useMeta } = require("../../../src/web/hooks/use-queries");
|
||||
render(<App />);
|
||||
expect(useMeta).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user