feat: 引入分层配置生命周期,支持变量引用和 JSON Schema 校验
- 新增 src/server/config/ 模块(types、issues、variables、normalizer、schema)
- 配置布局从 server.host/server.port 切换为 server.listen.host/server.listen.port
- 移除 HOST/PORT 隐式环境变量覆盖,改为 YAML 显式 ${KEY} 变量引用
- 支持 ${KEY}、${KEY|default}、${KEY|}、$${KEY} 变量语法
- 使用 @sinclair/typebox + ajv 实现运行时严格契约校验和 JSON Schema 导出
- 新增 scripts/generate-config-schema.ts 和 config.schema.json
- 新增 bun run schema / schema:check 命令,check 先执行 schema:check
- 更新 README.md 和 DEVELOPMENT.md 匹配新配置体系
- 新增变量解析、schema 校验和 schema 同步测试
This commit is contained in:
115
tests/server/config/schema.test.ts
Normal file
115
tests/server/config/schema.test.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
import { createAuthoringConfigSchema, createNormalizedConfigSchema } from "../../../src/server/config/schema/builder";
|
||||
import { createConfigJsonSchema } from "../../../src/server/config/schema/export";
|
||||
import {
|
||||
createConfigAjv,
|
||||
issuesFromAjvErrors,
|
||||
validateConfigContract,
|
||||
} from "../../../src/server/config/schema/validate";
|
||||
|
||||
describe("导出 schema 生成", () => {
|
||||
test("createConfigJsonSchema 返回有效 JSON Schema", () => {
|
||||
const schema = createConfigJsonSchema();
|
||||
expect(schema["$schema"]).toBe("http://json-schema.org/draft-07/schema#");
|
||||
expect(schema["$id"]).toBe("https://app.local/config.schema.json");
|
||||
expect(schema["type"]).toBe("object");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Authoring schema 校验", () => {
|
||||
const ajv = createConfigAjv();
|
||||
const validate = ajv.compile(createAuthoringConfigSchema());
|
||||
|
||||
test("接受空对象", () => {
|
||||
expect(validate({})).toBe(true);
|
||||
});
|
||||
|
||||
test("接受新布局 server.listen", () => {
|
||||
expect(validate({ server: { listen: { host: "127.0.0.1", port: 3000 } } })).toBe(true);
|
||||
});
|
||||
|
||||
test("接受变量引用语法", () => {
|
||||
expect(validate({ server: { listen: { port: "${PORT|3000}" } } })).toBe(true);
|
||||
});
|
||||
|
||||
test("接受 variables 字段", () => {
|
||||
expect(validate({ variables: { HOST: "127.0.0.1" } })).toBe(true);
|
||||
});
|
||||
|
||||
test("拒绝未知字段 server.host", () => {
|
||||
expect(validate({ server: { host: "127.0.0.1" } })).toBe(false);
|
||||
const issues = issuesFromAjvErrors(validate.errors ?? [], {});
|
||||
expect(issues.some((i) => i.code === "unknown-field")).toBe(true);
|
||||
});
|
||||
|
||||
test("拒绝未知字段 server.port", () => {
|
||||
expect(validate({ server: { port: 3000 } })).toBe(false);
|
||||
const issues = issuesFromAjvErrors(validate.errors ?? [], {});
|
||||
expect(issues.some((i) => i.code === "unknown-field")).toBe(true);
|
||||
});
|
||||
|
||||
test("拒绝非法类型 port", () => {
|
||||
expect(validate({ server: { listen: { port: "not-a-number" } } })).toBe(false);
|
||||
});
|
||||
|
||||
test("拒绝超出范围的 port", () => {
|
||||
expect(validate({ server: { listen: { port: 70000 } } })).toBe(false);
|
||||
});
|
||||
|
||||
test("拒绝负数 port", () => {
|
||||
expect(validate({ server: { listen: { port: -1 } } })).toBe(false);
|
||||
});
|
||||
|
||||
test("拒绝顶层未知字段", () => {
|
||||
expect(validate({ unknown: true })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Normalized schema 校验", () => {
|
||||
const ajv = createConfigAjv();
|
||||
const validate = ajv.compile(createNormalizedConfigSchema());
|
||||
|
||||
test("接受新布局 server.listen", () => {
|
||||
expect(validate({ server: { listen: { host: "127.0.0.1", port: 3000 } } })).toBe(true);
|
||||
});
|
||||
|
||||
test("Normalized 不接受 variables 字段", () => {
|
||||
expect(validate({ variables: { HOST: "127.0.0.1" } })).toBe(false);
|
||||
});
|
||||
|
||||
test("Normalized 不接受变量引用语法", () => {
|
||||
expect(validate({ server: { listen: { port: "${PORT|3000}" } } })).toBe(false);
|
||||
});
|
||||
|
||||
test("接受空对象", () => {
|
||||
expect(validate({})).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateConfigContract", () => {
|
||||
test("有效配置通过校验", () => {
|
||||
const result = validateConfigContract({ server: { listen: { host: "0.0.0.0", port: 8080 } } });
|
||||
expect(result.config).not.toBeNull();
|
||||
});
|
||||
|
||||
test("空配置通过校验", () => {
|
||||
const result = validateConfigContract({});
|
||||
expect(result.config).not.toBeNull();
|
||||
});
|
||||
|
||||
test("包含未知字段的配置被拒绝", () => {
|
||||
const result = validateConfigContract({ server: { host: "bad" } });
|
||||
expect(result.config).toBeNull();
|
||||
expect(result.issues.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("schema 同步测试", () => {
|
||||
test("config.schema.json 与 createConfigJsonSchema() 输出一致", async () => {
|
||||
const file = Bun.file("config.schema.json");
|
||||
const existing = await file.text();
|
||||
const generated = `${JSON.stringify(createConfigJsonSchema(), null, 2)}\n`;
|
||||
expect(existing).toBe(generated);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user