Files
Agent-System-Backend/internal/config/config_test.go
T
weichen 8633711b1d
ci / vet-and-test (pull_request) Successful in 38s
Add the team PG tier, atomic migrations and the connection budget (U2)
Why: the approved collaboration plan U2 (KTD1/KTD8/KTD12) requires an explicit fail-closed team deployment tier, DDL-and-ledger-atomic migrations serialized across migrators, and a configurable PG connection budget with sanitized effective values.

Evidence: real PostgreSQL 18.6 + pgvector + Redis 8.10.2 local gate - full backend suite green twice with AGENT_PG_TEST_DSN/AGENT_REDIS_TEST_ADDR (isolated svc.sh instance collabtest:8897, redis 6399); SQLite suite green without env.

Impact: config tier+pool keys and validation; store PoolOptions + statement_timeout DSN param + single-transaction migration with PG advisory lock and SQLite _txlock=immediate + unknown-schema-version refusal; app tier guard fail-closed (team requires DSN+auth, no SQLite fallback); CLI flags and explain rows; tests for tier/pool parsing, atomicity fault-injection, racing migrators, old-schema upgrade; fixed pre-existing real-PG test defects (blind &search_path= appends in runmgr/pgbus helpers, pg_trgm per-schema install race, exhausted seq-conflict retries).

Refs: proposals/2026-10-11-collaboration-storage-and-distribution.md U2, KTD1/KTD8/KTD12; R2/R3/R7.
2026-10-11 03:10:55 +00:00

356 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func noEnv(string) (string, bool) { return "", false }
func fileWith(t *testing.T, content string) string {
t.Helper()
p := filepath.Join(t.TempDir(), "c.json")
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
return p
}
// 基线:无文件无环境无 CLI → 全默认。
func TestDefaults(t *testing.T) {
r, err := Load("", noEnv, Config{}, nil)
if err != nil {
t.Fatal(err)
}
if r.Addr != "127.0.0.1:8787" || r.DataDir != "./data" || r.AuthMode != "auto" || r.Mock {
t.Fatalf("defaults wrong: %+v", r.Config)
}
if r.SourceOf("addr") != SrcDefault {
t.Fatalf("addr source = %v", r.SourceOf("addr"))
}
}
// 四源优先级:文件 < 环境 < 显式 CLI。
func TestPrecedence(t *testing.T) {
f := fileWith(t, `{"addr":"127.0.0.1:5000","auth":"off"}`)
env := func(k string) (string, bool) {
if k == "AGENT_SYSTEM_ADDR" {
return "127.0.0.1:6000", true
}
return "", false
}
r, err := Load(f, env, Config{Addr: "127.0.0.1:7000", AuthMode: "on"}, map[string]bool{"addr": true, "auth": true})
if err != nil {
t.Fatal(err)
}
if r.Addr != "127.0.0.1:7000" || r.SourceOf("addr") != SrcCLI {
t.Fatalf("addr should be CLI: %q %v", r.Addr, r.SourceOf("addr"))
}
if r.AuthMode != "on" || r.SourceOf("auth") != SrcCLI {
t.Fatalf("auth should be CLI: %q %v", r.AuthMode, r.SourceOf("auth"))
}
}
// 未显式传入的 CLI 字段不参与覆盖(区分"未设置"与零值)。
func TestUnsetCLIDoesNotOverride(t *testing.T) {
env := func(k string) (string, bool) {
if k == "AGENT_SYSTEM_ADDR" {
return "127.0.0.1:6000", true
}
return "", false
}
// cli.Addr 是零值 "" 但未在 cliSet 中:不得覆盖环境值。
r, err := Load("", env, Config{}, nil)
if err != nil {
t.Fatal(err)
}
if r.Addr != "127.0.0.1:6000" || r.SourceOf("addr") != SrcEnv {
t.Fatalf("addr should stay env: %q %v", r.Addr, r.SourceOf("addr"))
}
}
// 文件层按键存在覆盖:mock:false 显式出现也生效。
func TestFilePresenceSemantics(t *testing.T) {
f := fileWith(t, `{"mock":true,"data":"./d2"}`)
r, err := Load(f, noEnv, Config{}, nil)
if err != nil {
t.Fatal(err)
}
if !r.Mock || r.DataDir != "./d2" || r.SourceOf("mock") != SrcFile {
t.Fatalf("file layer wrong: %+v mock_src=%v", r.Config, r.SourceOf("mock"))
}
}
// 未知键:错误须定位字段名。
func TestUnknownKey(t *testing.T) {
f := fileWith(t, `{"bogus":1}`)
_, err := Load(f, noEnv, Config{}, nil)
if err == nil || !bytes.Contains([]byte(err.Error()), []byte(`unknown key "bogus"`)) {
t.Fatalf("want unknown-key error, got %v", err)
}
}
// 显式传入而缺失:报错。
func TestMissingFile(t *testing.T) {
_, err := Load("/nonexistent/c.json", noEnv, Config{}, nil)
if err == nil {
t.Fatal("missing file must error")
}
}
// 布尔环境变量:非法值报错,true/false/1/0 合法。
func TestEnvBool(t *testing.T) {
env := func(k string) (string, bool) {
if k == "AGENT_SYSTEM_MOCK" {
return "true", true
}
return "", false
}
r, err := Load("", env, Config{}, nil)
if err != nil || !r.Mock {
t.Fatalf("env bool: %v %+v", err, r.Config)
}
bad := func(k string) (string, bool) {
if k == "AGENT_SYSTEM_MOCK" {
return "yes", true
}
return "", false
}
if _, err := Load("", bad, Config{}, nil); err == nil {
t.Fatal("invalid bool must error")
}
}
// disableExecTools:默认 false;文件、环境、CLI 三层均可打开,CLI 优先。
func TestDisableExecToolsLayers(t *testing.T) {
r, err := Load("", noEnv, Config{}, nil)
if err != nil || r.DisableExecTools {
t.Fatalf("default must be false: %v %+v", err, r.Config)
}
f := fileWith(t, `{"disableExecTools":true}`)
r, err = Load(f, noEnv, Config{}, nil)
if err != nil || !r.DisableExecTools || r.SourceOf("disableExecTools") != SrcFile {
t.Fatalf("file layer: %v %+v src=%v", err, r.Config, r.SourceOf("disableExecTools"))
}
env := func(k string) (string, bool) {
if k == "AGENT_SYSTEM_DISABLE_EXEC_TOOLS" {
return "1", true
}
return "", false
}
r, err = Load("", env, Config{}, nil)
if err != nil || !r.DisableExecTools || r.SourceOf("disableExecTools") != SrcEnv {
t.Fatalf("env layer: %v %+v", err, r.Config)
}
r, err = Load("", env, Config{DisableExecTools: false}, map[string]bool{"disableExecTools": true})
if err != nil || r.DisableExecTools || r.SourceOf("disableExecTools") != SrcCLI {
t.Fatalf("cli must win: %v %+v", err, r.Config)
}
var out bytes.Buffer
r.Explain(&out, "")
if !bytes.Contains(out.Bytes(), []byte("disableExecTools")) {
t.Fatalf("explain must list disableExecTools:\n%s", out.String())
}
}
// explain:来源标注 + DSN 脱敏;只读(不落任何盘外文件)。
func TestExplainRedactsDSN(t *testing.T) {
env := func(k string) (string, bool) {
if k == "AGENT_SYSTEM_DSN" {
return "postgres://u:secret@h:5432/db", true
}
return "", false
}
r, err := Load("", env, Config{}, nil)
if err != nil {
t.Fatal(err)
}
var b bytes.Buffer
r.Explain(&b, "")
out := b.String()
if bytes.Contains([]byte(out), []byte("secret")) {
t.Fatalf("dsn not redacted: %s", out)
}
if !bytes.Contains([]byte(out), []byte("u:***@h")) {
t.Fatalf("redact form wrong: %s", out)
}
if !bytes.Contains([]byte(out), []byte("env")) {
t.Fatalf("source column missing: %s", out)
}
}
func TestListenGuard(t *testing.T) {
cases := []struct {
addr string
ok bool
}{
{"127.0.0.1:8787", true},
{"localhost:8787", true},
{"[::1]:8787", true},
{":8787", false},
{"0.0.0.0:8787", false},
{"[::]:8787", false},
{"192.168.1.9:8787", false},
}
for _, tc := range cases {
if got := IsLoopbackListen(tc.addr); got != tc.ok {
t.Errorf("%s loopback=%v want %v", tc.addr, got, tc.ok)
}
}
refused, err := Load("", noEnv, Config{Addr: "0.0.0.0:8788"}, map[string]bool{"addr": true})
if err != nil {
t.Fatal(err)
}
if refused.ListenRefusal() == "" {
t.Fatal("non-loopback without auth must refuse")
}
var b bytes.Buffer
refused.Explain(&b, "")
if !bytes.Contains(b.Bytes(), []byte("拒绝启动")) {
t.Fatalf("explain missing refusal: %s", b.String())
}
allowed, err := Load("", noEnv, Config{Addr: "0.0.0.0:8788", InsecureListen: true}, map[string]bool{"addr": true, "insecureListen": true})
if err != nil || allowed.ListenRefusal() != "" {
t.Fatalf("explicit insecure listen should pass: %v %q", err, allowed.ListenRefusal())
}
authed, err := Load("", noEnv, Config{Addr: "0.0.0.0:8788", AuthMode: "on"}, map[string]bool{"addr": true, "auth": true})
if err != nil || !authed.AuthEnabled() || authed.ListenRefusal() != "" {
t.Fatalf("auth on should pass: %+v %q", authed.Config, authed.ListenRefusal())
}
}
// ---- 协作存储方案 U2:部署档(KTD1)与连接预算(KTD12)----
// team 档 fail-closed:缺 DSN、未开认证都拒绝;DSN+auth=on 放行。
func TestTierTeamFailClosed(t *testing.T) {
r, err := Load("", noEnv, Config{Tier: "team"}, map[string]bool{"tier": true})
if err != nil {
t.Fatal(err)
}
msgs := r.TierProblems()
if len(msgs) != 2 {
t.Fatalf("team without dsn/auth should refuse with 2 problems, got %v", msgs)
}
joined := msgs[0] + msgs[1]
if !strings.Contains(joined, "PostgreSQL DSN") || !strings.Contains(joined, "认证") {
t.Fatalf("refusal messages wrong: %v", msgs)
}
// 补上 DSN(auto 认证随 DSN 开启)→ 放行。
r, err = Load("", noEnv, Config{Tier: "team", StorageDSN: "postgres://u:p@h/db"}, map[string]bool{"tier": true, "dsn": true})
if err != nil {
t.Fatal(err)
}
if msgs = r.TierProblems(); len(msgs) != 0 {
t.Fatalf("team with dsn should pass, got %v", msgs)
}
// 显式关认证仍然拒绝。
r, err = Load("", noEnv, Config{Tier: "team", StorageDSN: "postgres://u:p@h/db", AuthMode: "off"}, map[string]bool{"tier": true, "dsn": true, "auth": true})
if err != nil {
t.Fatal(err)
}
if msgs = r.TierProblems(); len(msgs) != 1 || !strings.Contains(msgs[0], "认证") {
t.Fatalf("team with auth=off should refuse, got %v", msgs)
}
}
// local 档不允许 DSN;未知档名拒绝。
func TestTierLocalAndUnknown(t *testing.T) {
r, err := Load("", noEnv, Config{Tier: "local", StorageDSN: "postgres://u:p@h/db"}, map[string]bool{"tier": true, "dsn": true})
if err != nil {
t.Fatal(err)
}
if msgs := r.TierProblems(); len(msgs) != 1 || !strings.Contains(msgs[0], "local 档不使用") {
t.Fatalf("local with dsn should refuse, got %v", msgs)
}
r, err = Load("", noEnv, Config{Tier: "cluster"}, map[string]bool{"tier": true})
if err != nil {
t.Fatal(err)
}
if msgs := r.TierProblems(); len(msgs) != 1 || !strings.Contains(msgs[0], "未知部署档") {
t.Fatalf("unknown tier should refuse, got %v", msgs)
}
// auto(空)档维持现状语义:无 DSN 不拒绝。
r, err = Load("", noEnv, Config{}, nil)
if err != nil {
t.Fatal(err)
}
if msgs := r.TierProblems(); len(msgs) != 0 {
t.Fatalf("auto default should pass, got %v", msgs)
}
}
// 连接预算四源解析与环境变量整数校验;MaxIdle>MaxOpen 拒绝。
func TestPGPoolConfigLayers(t *testing.T) {
env := func(k string) (string, bool) {
switch k {
case "AGENT_SYSTEM_PG_MAX_OPEN":
return "7", true
case "AGENT_SYSTEM_PG_MAX_IDLE":
return "9", true
}
return "", false
}
f := fileWith(t, `{"pgMaxOpen":5}`)
r, err := Load(f, env, Config{PGMaxOpen: 3, PGMaxIdle: 2}, map[string]bool{"pgMaxIdle": true})
if err != nil {
t.Fatal(err)
}
// 文件 5 被 env 7 覆盖(CLI 值 3 未显式传入不参与);pgMaxIdle CLI 显式 2 胜。
if r.PGMaxOpen != 7 || r.SourceOf("pgMaxOpen") != SrcEnv {
t.Fatalf("pgMaxOpen = %d (%v), want env 7", r.PGMaxOpen, r.SourceOf("pgMaxOpen"))
}
if r.PGMaxIdle != 2 || r.SourceOf("pgMaxIdle") != SrcCLI {
t.Fatalf("pgMaxIdle = %d (%v), want CLI 2", r.PGMaxIdle, r.SourceOf("pgMaxIdle"))
}
if msgs := r.TierProblems(); len(msgs) != 0 {
t.Fatalf("2/2 pool valid, got %v", msgs)
}
// 有效值:零值落默认。
r, err = Load("", noEnv, Config{}, nil)
if err != nil {
t.Fatal(err)
}
if r.EffectivePGMaxOpen() != DefaultPGMaxOpen || r.EffectivePGMaxIdle() != DefaultPGMaxIdle || r.EffectivePGConnMaxIdleSec() != DefaultPGConnMaxIdleSec {
t.Fatalf("effective defaults wrong: %+v", r.Config)
}
// idle > open 拒绝。
r, err = Load("", noEnv, Config{PGMaxOpen: 2, PGMaxIdle: 3}, map[string]bool{"pgMaxOpen": true, "pgMaxIdle": true})
if err != nil {
t.Fatal(err)
}
if msgs := r.TierProblems(); len(msgs) != 1 || !strings.Contains(msgs[0], "pgMaxIdle(3) 大于 pgMaxOpen(2)") {
t.Fatalf("idle>open should refuse, got %v", msgs)
}
// 非法整数环境变量报错定位到变量。
bad := func(k string) (string, bool) {
if k == "AGENT_SYSTEM_PG_MAX_OPEN" {
return "many", true
}
return "", false
}
if _, err := Load("", bad, Config{}, nil); err == nil || !strings.Contains(err.Error(), "AGENT_SYSTEM_PG_MAX_OPEN") {
t.Fatalf("invalid int env should fail with var name, got %v", err)
}
}
// explain 输出包含部署档与预算行,且 tier guard 结论可见(脱敏诊断)。
func TestExplainTierAndPool(t *testing.T) {
r, err := Load("", noEnv, Config{Tier: "team"}, map[string]bool{"tier": true})
if err != nil {
t.Fatal(err)
}
var b bytes.Buffer
r.Explain(&b, "")
out := b.String()
for _, want := range []string{"tier", "pgMaxOpen", "pgMaxIdle", "pgConnMaxIdleSec", "pgConnMaxLifetimeSec", "pgQueryTimeoutSec", "tier guard: 拒绝启动"} {
if !strings.Contains(out, want) {
t.Fatalf("explain missing %q:\n%s", want, out)
}
}
}