
Beads 测试基础设施修复实录环境变量污染导致 Dolt 测试套件连错服务器的问题根因与修复【免费下载链接】beadsBeads - A memory upgrade for your coding agent项目地址: https://gitcode.com/GitHub_Trending/beads1/beads导读本文以 Beads 仓库中发布门禁Release Gate文档 release-gates/be-79jh-dolt-testcontainer-ambient-port-gate.md 为主体完整还原一次针对 Dolt 测试基础设施的真实缺陷修复测试套件在启动自己的 testcontainer 后本应通过端口环境变量连回该容器却因BEADS_DOLT_SERVER_PORT这一“新端口变量”的优先级高于旧变量BEADS_DOLT_PORT在环境被污染ambient 环境变量已存在时静默连向了外部共享 Dolt 服务器导致测试数据库被写到错误的服务端。通过阅读本文你将理解该缺陷的根因链条、三项配套修复手段双站点同步设置端口、防火墙拒绝端口不一致、TestMain 启动时大声失败的源码级实现以及门禁评估中如何用 decoy-server 复现实验验证“不产生任何数据库”并掌握在 CI/测试框架中防止同类环境变量污染的方法论。一、缺陷背景两个端口环境变量的一次优先级反转1.1 问题现象门禁文档开门见山地给出了缺陷定义见 release-gates/be-79jh-dolt-testcontainer-ambient-port-gate.md 标题与 Criterion 7dolt test suites open stores on the ambient shared Dolt server, not their testcontainer即Dolt 测试套件把存储store打开在了 ambient环境中预先存在的共享 Dolt 服务器上而不是自己启动的 testcontainer 上。1.2 根因applyConfigDefaults的端口解析优先级端口解析的最终实现位于 internal/storage/dolt/store.go 的applyConfigDefaults// Port resolution: caller-preset explicit ServerPort BEADS_DOLT_SERVER_PORT // env (or legacy BEADS_DOLT_PORT) BEADS_TEST_MODE guard metadata config default. envPort : os.Getenv(BEADS_DOLT_SERVER_PORT) if envPort { envPort os.Getenv(BEADS_DOLT_PORT) // legacy fallback } if envPort ! (cfg.ServerPort 0 || !cfg.ServerPortSource.IsAuthoritative()) { if p, err : strconv.Atoi(envPort); err nil p 0 { cfg.ServerPort p cfg.ServerPortSource doltserver.PortSourceEnv } }关键行为BEADS_DOLT_SERVER_PORT是最高优先级的环境变量只有当它为空时才回退到旧变量BEADS_DOLT_PORT测试基础设施的容器启动代码在修复前只设置了BEADS_DOLT_PORT因此当开发机或 CI 机器上已经存在一个 ambient 的BEADS_DOLT_SERVER_PORT例如 agent 工具链的共享开发服务器、或历史遗留的 shell 导出时测试进程解析端口时会越过自己容器的端口直接采用 ambient 值——测试数据库被静默创建在错误的服务器上。1.3 为什么这是一个“静默”的严重缺陷Dolt 的 SQL 服务协议与 MySQL 兼容测试代码通过 DSN 连接127.0.0.1:port即可工作不会因端口变化而报错数据库名若恰好存在甚至能“正常”跑完测试。这导致测试数据写入非预期服务端污染共享服务器测试之间通过共享服务器产生跨测试耦合出现难以定位的随机失败生产端口 3307 若被命中还可能引发数据泄漏这是另一道防火墙的存在原因见下文。二、修复方案一两个容器启动站点同步设置BEADS_DOLT_SERVER_PORT2.1 涉及的源码修复集中在 internal/testutil/testdoltserver.go该文件是 Beads 测试工具包internal/testutil中 Dolt 测试容器管理的核心它使用 testcontainers-go 的 Dolt 模块启动容器。文件中存在两个容器启动站点StartIsolatedDoltContainer—— 为单个测试启动一个独立容器测试结束自动销毁ensureSharedContainer由EnsureDoltContainerForTestMain调用—— 为整个测试套件启动一个进程级单例容器。修复后两个站点都同时设置两个变量// StartIsolatedDoltContainer starts a per-test Dolt container and returns the // mapped host port, additionally pointing BEADS_DOLT_PORT and // BEADS_DOLT_SERVER_PORT at it for the duration of the test. func StartIsolatedDoltContainer(t *testing.T) string { t.Helper() c : StartIsolatedDoltContainerHandle(t) t.Setenv(BEADS_DOLT_PORT, c.Port) t.Setenv(BEADS_DOLT_SERVER_PORT, c.Port) return c.Port }func ensureSharedContainer() { doltServerOnce.Do(func() { doltServerErr startDoltContainer() if doltServerErr nil doltTestPort ! { if err : os.Setenv(BEADS_DOLT_PORT, doltTestPort); err ! nil { doltServerErr fmt.Errorf(set BEADS_DOLT_PORT: %w, err) } else if err : os.Setenv(BEADS_DOLT_SERVER_PORT, doltTestPort); err ! nil { doltServerErr fmt.Errorf(set BEADS_DOLT_SERVER_PORT: %w, err) } } }) }两个站点分别采用t.Setenv与os.Setenv是有讲究的t.Setenv是 testing 包提供的测试级环境变量设置测试结束自动还原不会污染进程内其他测试os.Setenv用于EnsureDoltContainerForTestMain这类TestMain 级设置需要在整个测试进程生命周期内生效并在 TestMain 退出时清理。2.2 为何保留一个“不设置环境变量”的入口值得注意的是StartIsolatedDoltContainerHandle被刻意设计为不触碰任何环境变量// Unlike StartIsolatedDoltContainer this does NOT touch BEADS_DOLT_PORT or // BEADS_DOLT_SERVER_PORT: those are process-wide, so a test that only wants // its own server should not perturb sibling tests sharing the process. func StartIsolatedDoltContainerHandle(t *testing.T) *IsolatedDoltContainer {这是对“测试隔离”原则的细致权衡进程级环境变量是共享状态仅需自己服务器的测试不应打扰同进程的兄弟测试因此提供“裸句柄”与“带环境变量”两个入口由调用方按需选择。2.3 新测试守卫TestDoltContainerStartSites_SetBeadsServerPortEnv新增测试 internal/testutil/testdoltserver_env_test.go 直接断言两个站点都必须设置BEADS_DOLT_SERVER_PORTfunc TestDoltContainerStartSites_SetBeadsServerPortEnv(t *testing.T) { if state : checkDolt(); state ! doltReady { t.Skipf(skipping test: %s, state) } t.Run(StartIsolatedDoltContainer, func(t *testing.T) { portStr : StartIsolatedDoltContainer(t) got : os.Getenv(BEADS_DOLT_SERVER_PORT) if got ! portStr { t.Errorf(BEADS_DOLT_SERVER_PORT %q, want %q ..., got, portStr) } }) t.Run(EnsureDoltContainerForTestMain, func(t *testing.T) { if err : EnsureDoltContainerForTestMain(); err ! nil { t.Fatalf(EnsureDoltContainerForTestMain: %v, err) } t.Cleanup(TerminateDoltContainer) want : strconv.Itoa(DoltContainerPortInt()) got : os.Getenv(BEADS_DOLT_SERVER_PORT) if got ! want { t.Errorf(BEADS_DOLT_SERVER_PORT %q, want %q ..., got, want) } }) }该测试的测试注释直白地说明了防线逻辑applyConfigDefaults会先读BEADS_DOLT_SERVER_PORT只有当其未设置时才回退到旧变量BEADS_DOLT_PORT因此两个启动站点必须同时设置新变量否则 ambient 值会静默胜出。门禁记录中该测试运行 PASS44.76s两个子测试均通过。三、修复方案二SetupSharedTestDB的 ambient 端口防火墙3.1 防火墙的实现仅靠“启动站点设置变量”仍不够——若解析链上游例如 TestMain 显式传入端口传入的端口与 ambient 环境变量不一致测试仍可能连向错误服务器。因此在 internal/testutil/testdoltbranch.go 的SetupSharedTestDB中增加了一道在任何连接建立之前触发的防火墙// FIREWALL: refuse when the ambient BEADS_DOLT_SERVER_PORT disagrees // with the port we were asked to use — proceeding would silently // create the shared test DB against whichever server // BEADS_DOLT_SERVER_PORT points at instead of our own testcontainer // (be-33se). if ambient : os.Getenv(BEADS_DOLT_SERVER_PORT); ambient ! ambient ! strconv.Itoa(port) { _ db.Close() return nil, fmt.Errorf(SetupSharedTestDB: REFUSED — port %d disagrees with ambient BEADS_DOLT_SERVER_PORT%s, port, ambient) }要点防火墙位于打开连接、执行任何查询之前与既有的生产端口 3307 防火墙if port 3307 { ... }并列错误信息明确点名BEADS_DOLT_SERVER_PORT便于定位对“ambient 为空”或“ambient 与传入端口一致”两种情况放行不产生误伤。3.2 新测试守卫TestSetupSharedTestDB_RefusesAmbientPortMismatchinternal/testutil/testdoltbranch_test.go 中的新测试模拟了“decoy ambient 端口”场景并断言拒绝行为func TestSetupSharedTestDB_RefusesAmbientPortMismatch(t *testing.T) { t.Setenv(BEADS_DOLT_SERVER_PORT, 19191) // decoy ambient shared server db, err : SetupSharedTestDB(29292, irrelevant_db) // disagrees with ambient port above if db ! nil { _ db.Close() } if err nil { t.Fatal(SetupSharedTestDB: expected refusal when port disagrees with ambient BEADS_DOLT_SERVER_PORT, got nil error) } if !strings.Contains(err.Error(), BEADS_DOLT_SERVER_PORT) { t.Errorf(SetupSharedTestDB error %q, want it to name the ambient BEADS_DOLT_SERVER_PORT disagreement, err.Error()) } }注意测试设计中的两个精妙之处测试注释中明确说明不依赖真实 Dolt 服务器——防火墙必须在连接尝试之前触发这与既有生产端口防火墙的要求一致使测试可以快速、稳定地运行门禁记录中该测试 0.00s 通过错误信息断言——不仅要求返回错误还要求错误文本包含BEADS_DOLT_SERVER_PORT防止未来实现退化成笼统报错而失去可诊断性。四、修复方案三TestMain 启动时对端口解析结果“大声失败”4.1 实现位置第三道防线位于 internal/storage/dolt/testmain_test.go 的testMainInner。在套件真正跑测试之前它构造一个Config{ServerPort: 0}、调用applyConfigDefaults得到解析后的端口并与自己容器的端口比对不一致则立即 FATAL 退出// Guard against the ambient-Dolt-port bug (be-33se): confirm // applyConfigDefaults actually resolves to this suites own // container rather than a stray ambient BEADS_DOLT_SERVER_PORT. resolveCfg : Config{ServerPort: 0} applyConfigDefaults(resolveCfg) if resolveCfg.ServerPort ! testServerPort { fmt.Fprintf(os.Stderr, FATAL: applyConfigDefaults resolved port %d, want this suites container port %d (ambient BEADS_DOLT_SERVER_PORT redirect?)\n, resolveCfg.ServerPort, testServerPort) return 1 }4.2 设计意图失败要“响”若解析出的端口不是容器端口说明环境被污染此时静默继续会让大量测试在错误服务器上“假绿”因此选择在 TestMain 直接返回非零退出码让 CI 立即显红与解析链对齐Config{ServerPort: 0}刻意模拟“未显式指定端口”的默认路径走与真实测试完全相同的一条applyConfigDefaults分支从而对“ambient 重定向”这一特定 bug 有最强的检测灵敏度。4.3 TestMain 的整体形态testMainInner展示了 Beads 中 Dolt 测试套件的标准装配流程三处修复在此汇合if err : testutil.EnsureDoltContainerForTestMain(); err ! nil { fmt.Fprintf(os.Stderr, WARN: %v, skipping Dolt tests\n, err) } else { defer testutil.TerminateDoltContainer() testServerPort testutil.DoltContainerPortInt() // —— 防线三端口解析一致性校验如上—— // Set up shared database for branch-per-test isolation testSharedDB dolt_pkg_shared db, err : testutil.SetupSharedTestDB(testServerPort, testSharedDB) // —— 防线二在此生效SetupSharedTestDB 内部防火墙—— ... // Commit schema to main so branches get a clean snapshot if err : initSharedSchema(testServerPort); err ! nil { ... } }TestMain还顺带设置了BEADS_TEST_MODE1、BEADS_TEST_PDEATHSIG1、BEADS_TEST_SERVER1等测试模式标记后者的注释追溯到 be-c5p 的数据库名防火墙设计并在退出前调用doltserver.SweepOrphanedTestServers(suiteTempRoot)清理孤儿服务器。五、验证策略decoy-server 复现实验证明“零数据库产生”门禁评估中最具说服力的证据是独立复现实验Criterion 2 第 5 项且执行了两次用一个真实、独立的本地dolt sql-server扮演 ambient 共享服务器decoy导出BEADS_DOLT_SERVER_PORTdecoy port绕过scripts/test.sh的 hermetic 包装直接运行go test ./internal/storage/dolt/ -run TestDoltNew_SmartRemoteMigrateGate_RealDolt——因为scripts/test.sh会剥离这个 bug 所涉及的精确环境变量直接运行才能让 ambient 污染场景真实发生两次运行测试均 PASS10.10s、27.97s对 decoy 数据目录做ls前后对比逐字节一致未出现任何新数据库目录。TestDoltNew_SmartRemoteMigrateGate_RealDolt本身位于 internal/storage/dolt/smart_remote_migrate_gate_test.go它是对“smart remote migrate gate”#4516的端到端验证通过真实 Dolt 服务器 真实缓存的远程跟踪 ref走完auto-migrate → adopt → fork-skew三种状态。门禁用它作为“本修复端到端锻炼”的目标测试是因为它真实地完成了New(ctx, Config{...})打开存储的全过程——正是会受端口解析影响的关键路径。这个实验证明了修复的净效果即使 ambient 变量存在测试打开存储的端口解析也被正确导向了自己的 testcontainer或在该测试场景下正确地操作目标服务器decoy 上不产生任何数据。六、门禁评估七项判据与结论该修复通过发布门禁 release-gates/be-79jh-dolt-testcontainer-ambient-port-gate.md最终裁定PASS进入隔离部署分支与 PR。门禁文档展示了一套完整的评估方法论值得在类似修复中复用判据结论评估要点Criterion 6先评估PASSgit merge-base origin/main deploySHA等于 origin/main 当前 tip无分叉风险预检gh api .../pulls返回[]尚无 PRCriterion 1PASSreview bead be-ytec 以close_reason: pass关闭notes 含verdict: pass×2 与 deploy bead/commit 元数据Criterion 2PASSbe-33se 的 Done-when 清单 5 项每项都对照实际 diff 或独立重跑验证非仅采信 reviewer 结论Criterion 3PASSgo build ./...、go vet全绿internal/testutil全包清扫 0 FAIL 0 意外 SKIP目标测试在 decoy 协议下 ×2 PASSCriterion 4PASSgofmt/go vet 干净golangci-lint 双 lane 0 issues无 HIGH 级发现Criterion 5PASSdeploy SHA 处git status --short无已跟踪文件修改仅 4 个与本次无关的先前 untracked 文件Criterion 7PASS全部 5 个变更文件只服务一个主题ambient 端口重定向修复TDD 红→绿提交对1d2bda15c→8a3039af1评估中体现的方法论亮点不轻信 reviewer 结论每个验收项都通过直接读 diff 或独立重跑交叉确认失败归因3ainternal/storage/dolt全量回归中出现的失败被归类为两类与本次 diff 无关的原因——评估者 shell 环境缺口ambientBEADS_DOLT_SERVER_PORT/缺DOCKER_HOST以及共享单服务器在高并发打开下的容量耗尽 flake单独立案 be-1o1c不阻塞本次评审区分环境污染与代码缺陷3bmake ci-pr-policy首次失败源于 gc worktree 上的 rig shim 文件.githooks/commit-msg非 git 跟踪路径在第二个干净 worktree 中重跑即 100% 通过最终归因于 worktree 局部环境污染而非仓库缺陷CI lane 匹配3bmake ci-pr-lint使用与真实 PR CI 一致的BD_LINT_NEW_FROM_MERGE_BASEorigin/main范围双 lane 0 issuesci-pr-core 的取舍说明repo 级-race -short未重跑是有意为之——scripts/ci/lib/test-env.sh的 hermetic 环境默认将dolt加入跳过列表除非BEADS_TEST_ENV_RUN_DOLT1对本 diff 的 dolt 代码无实际覆盖原始go test才是忠实检查。此外文档记录了一处命名不一致be-79jh 描述文本要求分支命名为deploy/be-ytec-gate以 review bead 命名与常规约定以 deploy bead 命名如deploy/be-pp7e-gate冲突最终以deploy/be-79jh-gate为准并显式记录在案。七、给测试基础设施的工程启示环境变量优先级是隐式的全局契约当新增一个更高优先级的环境变量如BEADS_DOLT_SERVER_PORT时所有设置旧变量的站点都必须同步更新——本 bug 正是“新变量优先、旧站点只写旧变量”的错位。修复后新增的守卫测试把这一契约显式化。防火墙应建在“任何副作用之前”SetupSharedTestDB的拒绝逻辑在打开连接前触发同时保留既有的生产端口防火墙形成“生产端口 → ambient 端口不一致”两层防御。TestMain 应做“启动自检”用一个Config{ServerPort: 0}走默认解析路径并校验结果能在套件大量测试开始前以最小代价暴露环境问题。守卫测试要“非平凡”且可诊断RefusesAmbientPortMismatch不依赖真实服务器、毫秒级完成SetBeadsServerPortEnv直接断言修复行为错误信息点名关键环境变量保证未来回归时一眼可定位。验证要逼近真实环境decoy-server 实验绕过 hermetic 包装、用真实dolt sql-server扮演污染源、以“前后ls逐字节一致”作为客观证据比单纯“测试通过了”更有说服力。相关文件索引门禁文档release-gates/be-79jh-dolt-testcontainer-ambient-port-gate.md容器启动与端口设置internal/testutil/testdoltserver.goambient 端口防火墙internal/testutil/testdoltbranch.go守卫测试启动站点internal/testutil/testdoltserver_env_test.go守卫测试端口拒绝internal/testutil/testdoltbranch_test.go端口解析实现internal/storage/dolt/store.goapplyConfigDefaultsTestMain 自检internal/storage/dolt/testmain_test.go端到端验证目标测试internal/storage/dolt/smart_remote_migrate_gate_test.go【免费下载链接】beadsBeads - A memory upgrade for your coding agent项目地址: https://gitcode.com/GitHub_Trending/beads1/beads创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考