ci / vet-and-test (pull_request) Successful in 38s
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.
356 lines
11 KiB
Go
356 lines
11 KiB
Go
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)
|
||
}
|
||
}
|
||
}
|