feat: 扩展配置变量替换范围至 server/probes/targets,支持空默认值语法
This commit is contained in:
@@ -436,6 +436,49 @@ targets:
|
||||
expect(target.http.maxRedirects).toBe(5);
|
||||
});
|
||||
|
||||
test("server 和 probes 在 schema 校验前完成变量替换", async () => {
|
||||
const configPath = join(tempDir, "variables-server-probes.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`variables:
|
||||
server_host: "0.0.0.0"
|
||||
server_port: 3100
|
||||
retention: "24h"
|
||||
log_level: "debug"
|
||||
rotation_max_files: 30
|
||||
max_checks: 5
|
||||
server:
|
||||
listen:
|
||||
host: "\${server_host}"
|
||||
port: "\${server_port}"
|
||||
storage:
|
||||
retention: "\${retention}"
|
||||
logging:
|
||||
level: "\${log_level}"
|
||||
file:
|
||||
rotation:
|
||||
maxFiles: "\${rotation_max_files}"
|
||||
probes:
|
||||
execution:
|
||||
maxConcurrentChecks: "\${max_checks}"
|
||||
targets:
|
||||
- id: "server-vars"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
);
|
||||
|
||||
const config = await loadConfig(configPath);
|
||||
expect(config.host).toBe("0.0.0.0");
|
||||
expect(config.port).toBe(3100);
|
||||
expect(config.retentionMs).toBe(86400000);
|
||||
expect(config.logging.consoleLevel).toBe("debug");
|
||||
expect(config.logging.fileLevel).toBe("debug");
|
||||
expect(config.logging.rotationMaxFiles).toBe(30);
|
||||
expect(config.maxConcurrentChecks).toBe(5);
|
||||
});
|
||||
|
||||
test("变量替换后类型不匹配导致 schema 校验失败", async () => {
|
||||
const configPath = join(tempDir, "bad-var-type.yaml");
|
||||
await writeFile(
|
||||
@@ -494,6 +537,38 @@ targets:
|
||||
await expectConfigLoadError(configPath, "未定义的变量");
|
||||
});
|
||||
|
||||
test("server 和 probes 中未定义变量阻止启动并输出字段路径", async () => {
|
||||
await expectConfigError(
|
||||
"unresolved-server-probes.yaml",
|
||||
`server:
|
||||
listen:
|
||||
host: "\${MISSING_SERVER_HOST}"
|
||||
probes:
|
||||
execution:
|
||||
maxConcurrentChecks: "\${MISSING_MAX_CHECKS}"
|
||||
targets:
|
||||
- id: "unresolved-root"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"server.listen.host",
|
||||
);
|
||||
await expectConfigError(
|
||||
"unresolved-probes.yaml",
|
||||
`probes:
|
||||
execution:
|
||||
maxConcurrentChecks: "\${MISSING_MAX_CHECKS}"
|
||||
targets:
|
||||
- id: "unresolved-probes"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"probes.execution.maxConcurrentChecks",
|
||||
);
|
||||
});
|
||||
|
||||
test("绝对 dataDir 保持不变", async () => {
|
||||
const dataDir = join(tempDir, "absolute-data");
|
||||
const configPath = join(tempDir, "absolute-data-dir.yaml");
|
||||
|
||||
@@ -150,36 +150,63 @@ describe("config variables", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("替换范围仅 targets,且跳过 id 和 type 字段", () => {
|
||||
test("替换范围覆盖 server、probes、targets,并跳过 variables 和 target 顶层 id/type", () => {
|
||||
const result = resolveVariables({
|
||||
defaults: { interval: "${interval}" },
|
||||
server: { host: "${host}" },
|
||||
probes: { execution: { maxConcurrentChecks: "${maxConcurrentChecks}" } },
|
||||
server: { host: "${host}", logging: { level: "${logLevel}" }, storage: { retention: "${retention}" } },
|
||||
targets: [
|
||||
{
|
||||
cmd: {
|
||||
args: ["--host", "${host}"],
|
||||
env: { TOKEN: "${token}" },
|
||||
env: { "${TOKEN_KEY}": "${token}" },
|
||||
exec: "echo",
|
||||
},
|
||||
expect: { rows: [{ id: { equals: "${expectedId}" }, type: { equals: "${expectedType}" } }] },
|
||||
id: "${id}",
|
||||
type: "${type}",
|
||||
},
|
||||
],
|
||||
variables: { host: "localhost", id: "resolved", interval: "30s", token: "abc", type: "cmd" },
|
||||
variables: {
|
||||
expectedId: 1,
|
||||
expectedType: "service",
|
||||
host: "localhost",
|
||||
id: "resolved",
|
||||
interval: "30s",
|
||||
logLevel: "debug",
|
||||
maxConcurrentChecks: 20,
|
||||
retention: "24h",
|
||||
token: "abc",
|
||||
TOKEN_KEY: "TOKEN",
|
||||
type: "cmd",
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.issues).toHaveLength(0);
|
||||
const config = result.config as {
|
||||
defaults: { interval: string };
|
||||
server: { host: string };
|
||||
targets: Array<{ cmd: { args: string[]; env: Record<string, string> }; id: string; type: string }>;
|
||||
probes: { execution: { maxConcurrentChecks: number } };
|
||||
server: { host: string; logging: { level: string }; storage: { retention: string } };
|
||||
targets: Array<{
|
||||
cmd: { args: string[]; env: Record<string, string> };
|
||||
expect: { rows: Array<{ id: { equals: number }; type: { equals: string } }> };
|
||||
id: string;
|
||||
type: string;
|
||||
}>;
|
||||
variables: { host: string };
|
||||
};
|
||||
expect(config.server.host).toBe("${host}");
|
||||
expect(config.server.host).toBe("localhost");
|
||||
expect(config.server.logging.level).toBe("debug");
|
||||
expect(config.server.storage.retention).toBe("24h");
|
||||
expect(config.probes.execution.maxConcurrentChecks).toBe(20);
|
||||
expect(config.variables.host).toBe("localhost");
|
||||
expect(config.defaults.interval).toBe("${interval}");
|
||||
expect(config.targets[0]!.id).toBe("${id}");
|
||||
expect(config.targets[0]!.type).toBe("${type}");
|
||||
expect(config.targets[0]!.cmd.args[1]).toBe("localhost");
|
||||
expect(config.targets[0]!.cmd.env["TOKEN"]).toBe("abc");
|
||||
expect(config.targets[0]!.cmd.env["${TOKEN_KEY}"]).toBe("abc");
|
||||
expect(config.targets[0]!.expect.rows[0]!.id.equals).toBe(1);
|
||||
expect(config.targets[0]!.expect.rows[0]!.type.equals).toBe("service");
|
||||
});
|
||||
|
||||
test("默认值推断为 boolean(true/false)", () => {
|
||||
@@ -220,7 +247,39 @@ describe("config variables", () => {
|
||||
expect(typeof http2["ignoreSSL"]).toBe("boolean");
|
||||
});
|
||||
|
||||
test("server.storage 段不替换", () => {
|
||||
test("空默认值和空环境变量保持字符串空值", () => {
|
||||
const original = process.env["EMPTY_ENV_VALUE"];
|
||||
process.env["EMPTY_ENV_VALUE"] = "";
|
||||
|
||||
try {
|
||||
const result = resolveVariables({
|
||||
server: { listen: { host: "${EMPTY_ENV_VALUE}" }, logging: { file: { path: "${OPTIONAL_LOG_PATH|}" } } },
|
||||
targets: [
|
||||
{
|
||||
http: { body: "${OPTIONAL_BODY|}", url: "${HOST|localhost}" },
|
||||
id: "empty-values",
|
||||
type: "http",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.issues).toHaveLength(0);
|
||||
const config = result.config as {
|
||||
server: { listen: { host: unknown }; logging: { file: { path: unknown } } };
|
||||
targets: Array<{ http: { body: unknown } }>;
|
||||
};
|
||||
expect(config.server.listen.host).toBe("");
|
||||
expect(typeof config.server.listen.host).toBe("string");
|
||||
expect(config.server.logging.file.path).toBe("");
|
||||
expect(typeof config.server.logging.file.path).toBe("string");
|
||||
expect(config.targets[0]!.http.body).toBe("");
|
||||
expect(typeof config.targets[0]!.http.body).toBe("string");
|
||||
} finally {
|
||||
restoreEnv("EMPTY_ENV_VALUE", original);
|
||||
}
|
||||
});
|
||||
|
||||
test("server.storage 段支持替换", () => {
|
||||
const result = resolveVariables({
|
||||
server: { storage: { retention: "${retention}" } },
|
||||
targets: [
|
||||
@@ -235,10 +294,10 @@ describe("config variables", () => {
|
||||
|
||||
expect(result.issues).toHaveLength(0);
|
||||
const config = result.config as { server: { storage: { retention: string } } };
|
||||
expect(config.server.storage.retention).toBe("${retention}");
|
||||
expect(config.server.storage.retention).toBe("24h");
|
||||
});
|
||||
|
||||
test("probes 段不替换", () => {
|
||||
test("probes 段支持替换", () => {
|
||||
const result = resolveVariables({
|
||||
probes: { execution: { maxConcurrentChecks: "${maxConcurrentChecks}" } },
|
||||
targets: [
|
||||
@@ -248,15 +307,15 @@ describe("config variables", () => {
|
||||
type: "http",
|
||||
},
|
||||
],
|
||||
variables: { host: "https://example.com", maxConcurrentChecks: "20" },
|
||||
variables: { host: "https://example.com", maxConcurrentChecks: 20 },
|
||||
});
|
||||
|
||||
expect(result.issues).toHaveLength(0);
|
||||
const config = result.config as { probes: { execution: { maxConcurrentChecks: string } } };
|
||||
expect(config.probes.execution.maxConcurrentChecks).toBe("${maxConcurrentChecks}");
|
||||
const config = result.config as { probes: { execution: { maxConcurrentChecks: number } } };
|
||||
expect(config.probes.execution.maxConcurrentChecks).toBe(20);
|
||||
});
|
||||
|
||||
test("server.logging 段不替换", () => {
|
||||
test("server.logging 段支持替换", () => {
|
||||
const result = resolveVariables({
|
||||
server: { logging: { level: "${logLevel}" } },
|
||||
targets: [
|
||||
@@ -271,7 +330,7 @@ describe("config variables", () => {
|
||||
|
||||
expect(result.issues).toHaveLength(0);
|
||||
const config = result.config as { server: { logging: { level: string } } };
|
||||
expect(config.server.logging.level).toBe("${logLevel}");
|
||||
expect(config.server.logging.level).toBe("debug");
|
||||
});
|
||||
|
||||
test("variables 段为非对象时报错", () => {
|
||||
|
||||
Reference in New Issue
Block a user