- <config_root>/instance-settings.json is the final config layer: default > env-file(s) > process-env > cli > instance-settings - LoadForServe wires the CLI mutation hook BEFORE instance settings; serve/check/explain all use it; plain Load keeps legacy behavior - bootstrap denylist (env) fails closed (no self-locating file loop); SETTINGS_MODE=ignore skips the layer as the recovery path - reflection-based typed application: string/int/bool/duration(string or seconds)/string-array; unknown fields and type mismatches fail closed - group/world-writable settings file rejected; absent key = inherit - sources table gains the instance-settings layer; e2e smoke verified (override applies; ignore-mode reverts to default)
73 lines
2.1 KiB
Go
73 lines
2.1 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestFieldSourcesLayerAttribution(t *testing.T) {
|
|
dir := t.TempDir()
|
|
envf := filepath.Join(dir, ".env")
|
|
if err := os.WriteFile(envf, []byte("HTTP_HOST=0.0.0.0\nKB_DB_PASSWORD=from-file\nAGENT_BACKEND=stub\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("KB_DB_PASSWORD", "from-process") // 进程 env 覆盖 env-file
|
|
|
|
cfg, src, err := LoadWithSources(envf, nil)
|
|
if err != nil {
|
|
t.Fatalf("load: %v", err)
|
|
}
|
|
if cfg.HTTPHost != "0.0.0.0" || cfg.KbDbPassword != "from-process" || cfg.AgentBackend != "stub" {
|
|
t.Fatalf("values not applied: host=%s", cfg.HTTPHost)
|
|
}
|
|
if src["http_host"].Layer != "env-file:"+filepath.ToSlash(envf) {
|
|
t.Fatalf("http_host layer=%s", src["http_host"].Layer)
|
|
}
|
|
if src["kb_db_password"].Layer != "process-env" {
|
|
t.Fatalf("kb_db_password layer=%s", src["kb_db_password"].Layer)
|
|
}
|
|
if src["agent_backend"].Layer != "env-file:"+filepath.ToSlash(envf) {
|
|
t.Fatalf("agent_backend layer=%s", src["agent_backend"].Layer)
|
|
}
|
|
// 未设置字段仍是 default 层
|
|
if src["admin_username"].Layer != "default" || src["admin_username"].Set {
|
|
t.Fatalf("admin_username layer=%s set=%v", src["admin_username"].Layer, src["admin_username"].Set)
|
|
}
|
|
}
|
|
|
|
func TestApplyCLIOverridesAttribution(t *testing.T) {
|
|
cfg, src, err := LoadWithSources("", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
src2, err := ApplyCLIOverrides(cfg, src, func(c *Config) {
|
|
c.HTTPPort = 9999
|
|
c.AgentBackend = "stub"
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if src2["http_port"].Layer != "cli" || !src2["http_port"].Set {
|
|
t.Fatalf("http_port layer=%s set=%v", src2["http_port"].Layer, src2["http_port"].Set)
|
|
}
|
|
if src2["agent_backend"].Layer != "cli" {
|
|
t.Fatalf("agent_backend layer=%s", src2["agent_backend"].Layer)
|
|
}
|
|
// 未覆盖字段来源不变
|
|
if src2["http_host"].Layer != src["http_host"].Layer {
|
|
t.Fatalf("http_host layer changed unexpectedly")
|
|
}
|
|
}
|
|
|
|
// Load 行为回归:老入口签名与结果不变。
|
|
func TestLoadStillWorks(t *testing.T) {
|
|
cfg, err := Load("")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.HTTPPort == 0 || cfg.DataDir == "" {
|
|
t.Fatal("defaults lost")
|
|
}
|
|
}
|