feat: 重构配置布局,server.listen/storage/logging + probes.execution 分组
- 新增 server.listen (host/port)、server.storage (dataDir/retention)、 server.logging 分组 - 新增 probes.execution (maxConcurrentChecks) 分组,替代顶层 runtime - 旧配置入口 (runtime/logging/server.host/server.port/server.dataDir) 启动期拒绝 - 更新 types.ts、builder.ts、config-loader.ts 适配新路径 - 更新 probe-config.schema.json、probes.example.yaml、README.md、 DEVELOPMENT.md - 补充 config-loader 和 variables 测试覆盖新路径和旧入口拒绝 - 同步 5 个 delta specs 到主规范 (probe-config, config-variables, data-retention, probe-engine, runtime-logging) - 归档 openspec change reorganize-config-layout
This commit is contained in:
@@ -182,11 +182,14 @@ describe("loadConfig", () => {
|
||||
await writeFile(
|
||||
configPath,
|
||||
`server:
|
||||
host: "0.0.0.0"
|
||||
port: 8080
|
||||
dataDir: "./my-data"
|
||||
runtime:
|
||||
maxConcurrentChecks: 5
|
||||
listen:
|
||||
host: "0.0.0.0"
|
||||
port: 8080
|
||||
storage:
|
||||
dataDir: "./my-data"
|
||||
probes:
|
||||
execution:
|
||||
maxConcurrentChecks: 5
|
||||
defaults:
|
||||
interval: "15s"
|
||||
timeout: "5s"
|
||||
@@ -508,7 +511,8 @@ targets:
|
||||
await writeFile(
|
||||
configPath,
|
||||
`server:
|
||||
dataDir: ${JSON.stringify(dataDir)}
|
||||
storage:
|
||||
dataDir: ${JSON.stringify(dataDir)}
|
||||
targets:
|
||||
- name: "test"
|
||||
id: "test"
|
||||
@@ -760,7 +764,8 @@ targets:
|
||||
await writeFile(
|
||||
configPath,
|
||||
`server:
|
||||
port: 99999
|
||||
listen:
|
||||
port: 99999
|
||||
targets:
|
||||
- name: "t"
|
||||
id: "t"
|
||||
@@ -769,15 +774,16 @@ targets:
|
||||
url: "http://a.com"
|
||||
`,
|
||||
);
|
||||
await expectConfigLoadError(configPath, "server.port 数值范围不合法");
|
||||
await expectConfigLoadError(configPath, "server.listen.port 数值范围不合法");
|
||||
});
|
||||
|
||||
test("非法 maxConcurrentChecks 抛出错误", async () => {
|
||||
const configPath = join(tempDir, "bad-concurrency.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`runtime:
|
||||
maxConcurrentChecks: -1
|
||||
`probes:
|
||||
execution:
|
||||
maxConcurrentChecks: -1
|
||||
targets:
|
||||
- name: "t"
|
||||
id: "t"
|
||||
@@ -786,7 +792,7 @@ targets:
|
||||
url: "http://a.com"
|
||||
`,
|
||||
);
|
||||
await expectConfigLoadError(configPath, "runtime.maxConcurrentChecks 数值范围不合法");
|
||||
await expectConfigLoadError(configPath, "probes.execution.maxConcurrentChecks 数值范围不合法");
|
||||
});
|
||||
|
||||
test("非法 size 格式抛出错误", async () => {
|
||||
@@ -1621,8 +1627,9 @@ targets:
|
||||
const configPath = join(tempDir, "retention-custom.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`runtime:
|
||||
retention: "24h"
|
||||
`server:
|
||||
storage:
|
||||
retention: "24h"
|
||||
targets:
|
||||
- name: "test"
|
||||
id: "test"
|
||||
@@ -1638,8 +1645,9 @@ targets:
|
||||
test("retention 非法格式抛出错误", async () => {
|
||||
await expectConfigError(
|
||||
"bad-retention.yaml",
|
||||
`runtime:
|
||||
retention: "7x"
|
||||
`server:
|
||||
storage:
|
||||
retention: "7x"
|
||||
targets:
|
||||
- name: "test"
|
||||
id: "test"
|
||||
@@ -2113,8 +2121,9 @@ targets:
|
||||
const configPath = join(tempDir, "logging-global-level.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`logging:
|
||||
level: "debug"
|
||||
`server:
|
||||
logging:
|
||||
level: "debug"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
@@ -2131,10 +2140,11 @@ targets:
|
||||
const configPath = join(tempDir, "logging-console-level.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`logging:
|
||||
level: "warn"
|
||||
console:
|
||||
level: "trace"
|
||||
`server:
|
||||
logging:
|
||||
level: "warn"
|
||||
console:
|
||||
level: "trace"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
@@ -2151,10 +2161,11 @@ targets:
|
||||
const configPath = join(tempDir, "logging-file-level.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`logging:
|
||||
level: "info"
|
||||
file:
|
||||
level: "error"
|
||||
`server:
|
||||
logging:
|
||||
level: "info"
|
||||
file:
|
||||
level: "error"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
@@ -2171,9 +2182,10 @@ targets:
|
||||
const configPath = join(tempDir, "logging-abs-path.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`logging:
|
||||
file:
|
||||
path: "/var/log/dial/app.log"
|
||||
`server:
|
||||
logging:
|
||||
file:
|
||||
path: "/var/log/dial/app.log"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
@@ -2189,9 +2201,10 @@ targets:
|
||||
const configPath = join(tempDir, "logging-rel-path.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`logging:
|
||||
file:
|
||||
path: "custom-logs/app.log"
|
||||
`server:
|
||||
logging:
|
||||
file:
|
||||
path: "custom-logs/app.log"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
@@ -2207,12 +2220,13 @@ targets:
|
||||
const configPath = join(tempDir, "logging-rotation.yaml");
|
||||
await writeFile(
|
||||
configPath,
|
||||
`logging:
|
||||
file:
|
||||
rotation:
|
||||
size: "100MB"
|
||||
frequency: "hourly"
|
||||
maxFiles: 30
|
||||
`server:
|
||||
logging:
|
||||
file:
|
||||
rotation:
|
||||
size: "100MB"
|
||||
frequency: "hourly"
|
||||
maxFiles: 30
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
@@ -2230,57 +2244,61 @@ targets:
|
||||
test("logging.level 非法等级抛出错误", async () => {
|
||||
await expectConfigError(
|
||||
"logging-bad-level.yaml",
|
||||
`logging:
|
||||
level: "verbose"
|
||||
`server:
|
||||
logging:
|
||||
level: "verbose"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"logging.level",
|
||||
"server.logging.level",
|
||||
);
|
||||
});
|
||||
|
||||
test("logging.console.level 非法等级抛出错误", async () => {
|
||||
await expectConfigError(
|
||||
"logging-bad-console-level.yaml",
|
||||
`logging:
|
||||
console:
|
||||
level: "nope"
|
||||
`server:
|
||||
logging:
|
||||
console:
|
||||
level: "nope"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"logging.console.level",
|
||||
"server.logging.console.level",
|
||||
);
|
||||
});
|
||||
|
||||
test("logging.file.level 非法等级抛出错误", async () => {
|
||||
await expectConfigError(
|
||||
"logging-bad-file-level.yaml",
|
||||
`logging:
|
||||
file:
|
||||
level: 123
|
||||
`server:
|
||||
logging:
|
||||
file:
|
||||
level: 123
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"logging.file.level",
|
||||
"server.logging.file.level",
|
||||
);
|
||||
});
|
||||
|
||||
test("logging.file.rotation.size 非法格式抛出错误", async () => {
|
||||
await expectConfigError(
|
||||
"logging-bad-rotation-size.yaml",
|
||||
`logging:
|
||||
file:
|
||||
rotation:
|
||||
size: "100TB"
|
||||
`server:
|
||||
logging:
|
||||
file:
|
||||
rotation:
|
||||
size: "100TB"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
@@ -2294,50 +2312,130 @@ targets:
|
||||
test("logging.file.rotation.frequency 非法值抛出错误", async () => {
|
||||
await expectConfigError(
|
||||
"logging-bad-frequency.yaml",
|
||||
`logging:
|
||||
file:
|
||||
rotation:
|
||||
frequency: "monthly"
|
||||
`server:
|
||||
logging:
|
||||
file:
|
||||
rotation:
|
||||
frequency: "monthly"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"logging.file.rotation.frequency",
|
||||
"server.logging.file.rotation.frequency",
|
||||
);
|
||||
});
|
||||
|
||||
test("logging.file.rotation.maxFiles 非整数抛出错误", async () => {
|
||||
await expectConfigError(
|
||||
"logging-bad-maxfiles.yaml",
|
||||
`logging:
|
||||
file:
|
||||
rotation:
|
||||
maxFiles: 3.5
|
||||
`server:
|
||||
logging:
|
||||
file:
|
||||
rotation:
|
||||
maxFiles: 3.5
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"logging.file.rotation.maxFiles",
|
||||
"server.logging.file.rotation.maxFiles",
|
||||
);
|
||||
});
|
||||
|
||||
test("logging.file.path 空字符串抛出错误", async () => {
|
||||
await expectConfigError(
|
||||
"logging-empty-path.yaml",
|
||||
`logging:
|
||||
file:
|
||||
path: ""
|
||||
`server:
|
||||
logging:
|
||||
file:
|
||||
path: ""
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"logging.file.path",
|
||||
"server.logging.file.path",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("旧路径拒绝", () => {
|
||||
test("顶层 runtime 段应被拒绝为未知字段", async () => {
|
||||
await expectConfigError(
|
||||
"legacy-runtime.yaml",
|
||||
`runtime:
|
||||
maxConcurrentChecks: 10
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"runtime 是未知字段",
|
||||
);
|
||||
});
|
||||
|
||||
test("顶层 logging 段应被拒绝为未知字段", async () => {
|
||||
await expectConfigError(
|
||||
"legacy-logging.yaml",
|
||||
`logging:
|
||||
level: "info"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"logging 是未知字段",
|
||||
);
|
||||
});
|
||||
|
||||
test("server.host 应被拒绝为未知字段", async () => {
|
||||
await expectConfigError(
|
||||
"legacy-server-host.yaml",
|
||||
`server:
|
||||
host: "0.0.0.0"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"host 是未知字段",
|
||||
);
|
||||
});
|
||||
|
||||
test("server.port 应被拒绝为未知字段", async () => {
|
||||
await expectConfigError(
|
||||
"legacy-server-port.yaml",
|
||||
`server:
|
||||
port: 8080
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"port 是未知字段",
|
||||
);
|
||||
});
|
||||
|
||||
test("server.dataDir 应被拒绝为未知字段", async () => {
|
||||
await expectConfigError(
|
||||
"legacy-server-datadir.yaml",
|
||||
`server:
|
||||
dataDir: "/tmp/data"
|
||||
targets:
|
||||
- id: "t"
|
||||
type: http
|
||||
http:
|
||||
url: "http://example.com"
|
||||
`,
|
||||
"dataDir 是未知字段",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -220,9 +220,9 @@ describe("config variables", () => {
|
||||
expect(typeof http2["ignoreSSL"]).toBe("boolean");
|
||||
});
|
||||
|
||||
test("runtime 段不替换", () => {
|
||||
test("server.storage 段不替换", () => {
|
||||
const result = resolveVariables({
|
||||
runtime: { maxConcurrentChecks: 10, retention: "${retention}" },
|
||||
server: { storage: { retention: "${retention}" } },
|
||||
targets: [
|
||||
{
|
||||
http: { url: "${host}" },
|
||||
@@ -234,8 +234,44 @@ describe("config variables", () => {
|
||||
});
|
||||
|
||||
expect(result.issues).toHaveLength(0);
|
||||
const config = result.config as { runtime: { retention: string } };
|
||||
expect(config.runtime.retention).toBe("${retention}");
|
||||
const config = result.config as { server: { storage: { retention: string } } };
|
||||
expect(config.server.storage.retention).toBe("${retention}");
|
||||
});
|
||||
|
||||
test("probes 段不替换", () => {
|
||||
const result = resolveVariables({
|
||||
probes: { execution: { maxConcurrentChecks: "${maxConcurrentChecks}" } },
|
||||
targets: [
|
||||
{
|
||||
http: { url: "${host}" },
|
||||
id: "probes-no-replace",
|
||||
type: "http",
|
||||
},
|
||||
],
|
||||
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}");
|
||||
});
|
||||
|
||||
test("server.logging 段不替换", () => {
|
||||
const result = resolveVariables({
|
||||
server: { logging: { level: "${logLevel}" } },
|
||||
targets: [
|
||||
{
|
||||
http: { url: "${host}" },
|
||||
id: "logging-no-replace",
|
||||
type: "http",
|
||||
},
|
||||
],
|
||||
variables: { host: "https://example.com", logLevel: "debug" },
|
||||
});
|
||||
|
||||
expect(result.issues).toHaveLength(0);
|
||||
const config = result.config as { server: { logging: { level: string } } };
|
||||
expect(config.server.logging.level).toBe("${logLevel}");
|
||||
});
|
||||
|
||||
test("variables 段为非对象时报错", () => {
|
||||
|
||||
Reference in New Issue
Block a user