feat: 新增版本管理系统,重构 /health → /api/meta

This commit is contained in:
2026-05-24 23:32:19 +08:00
parent bc54f8352a
commit 7dc3a270ae
23 changed files with 450 additions and 111 deletions

View File

@@ -0,0 +1,48 @@
import { describe, expect, test } from "bun:test";
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();
});
test("生成的 server-entry 包含版本字面量", () => {
const version = "0.1.0";
const serverEntryTs = [
`import { bootstrap } from "../src/server/bootstrap";`,
`import { parseRuntimeArgs } from "../src/server/config";`,
`import { staticAssets } from "./static-assets";`,
"",
`const APP_VERSION = "${version}" as const;`,
"",
`async function main() {`,
` const { configPath } = parseRuntimeArgs();`,
` 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");
expect(serverEntryTs).toContain(`const APP_VERSION = "${version}"`);
expect(serverEntryTs).toContain("version: APP_VERSION");
});
test("版本字面量不依赖外部 package.json", () => {
const serverEntryTs = [`const APP_VERSION = "0.1.0" as const;`].join("\n");
expect(serverEntryTs).not.toContain("package.json");
expect(serverEntryTs).not.toContain("Bun.file");
expect(serverEntryTs).toContain('"0.1.0"');
});
});

View File

@@ -0,0 +1,73 @@
import { describe, expect, test } from "bun:test";
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();
});
test("bumpVersion set 缺少目标版本报错", () => {
expect(() => bumpVersion("1.2.3", "set")).toThrow("set command requires a target version");
});
});

View File

@@ -21,6 +21,7 @@ describe("bootstrap", () => {
signalRegistered = true;
};
const mockStartServer = (_options: StartServerOptions) => {
expect(_options.version).toBeUndefined();
started = true;
return {};
};
@@ -38,6 +39,24 @@ describe("bootstrap", () => {
expect(signalRegistered).toBe(true);
});
test("传递 version 给 startServer", async () => {
let receivedVersion: string | undefined;
const deps: BootstrapDependencies = {
loadConfig: async () => ({ host: "127.0.0.1", port: 0 }),
logError: () => {},
onSignal: () => {},
startServer: (options: StartServerOptions) => {
receivedVersion = options.version;
return {};
},
};
await bootstrap({ mode: "production", version: "1.2.3" }, deps);
expect(receivedVersion).toBe("1.2.3");
});
test("启动失败时调用 logError", async () => {
let errorLogged = false;

View File

@@ -10,10 +10,13 @@ import { renderWithProviders } from "./test-utils";
describe("App", () => {
test("渲染 Layout 骨架和品牌名", () => {
window.fetch = (async () => {
return new Response(JSON.stringify({ ok: true, service: "test-app", timestamp: new Date().toISOString() }), {
headers: { "Content-Type": "application/json" },
status: 200,
});
return new Response(
JSON.stringify({ ok: true, service: "test-app", timestamp: new Date().toISOString(), version: "0.1.0" }),
{
headers: { "Content-Type": "application/json" },
status: 200,
},
);
}) as unknown as typeof fetch;
renderWithProviders(createElement(App));
@@ -26,10 +29,13 @@ describe("App", () => {
test("渲染侧边栏菜单项", () => {
window.fetch = (async () => {
return new Response(JSON.stringify({ ok: true, service: "test-app", timestamp: new Date().toISOString() }), {
headers: { "Content-Type": "application/json" },
status: 200,
});
return new Response(
JSON.stringify({ ok: true, service: "test-app", timestamp: new Date().toISOString(), version: "0.1.0" }),
{
headers: { "Content-Type": "application/json" },
status: 200,
},
);
}) as unknown as typeof fetch;
renderWithProviders(createElement(App));

View File

@@ -9,10 +9,13 @@ import { renderWithProviders } from "../test-utils";
describe("DashboardPage", () => {
test("渲染欢迎信息", () => {
window.fetch = (async () => {
return new Response(JSON.stringify({ ok: true, service: "test-app", timestamp: new Date().toISOString() }), {
headers: { "Content-Type": "application/json" },
status: 200,
});
return new Response(
JSON.stringify({ ok: true, service: "test-app", timestamp: new Date().toISOString(), version: "0.1.0" }),
{
headers: { "Content-Type": "application/json" },
status: 200,
},
);
}) as unknown as typeof fetch;
renderWithProviders(createElement(DashboardPage));