fix(db): 修正公司 ICP 归属用例连接关闭顺序导致的资产残留

清理注册在 t.Cleanup 里,而连接关闭用的是 defer d.Close()。defer 在函数
返回时先执行、t.Cleanup 在其后,于是两条 DELETE 全部落在**已关闭的连接**
上,错误又被 `_, _ =` 丢弃,测试资产与公司因此永久残留在库里。

该用例用 MAX(companies.id)+1 当假 TaskID 给资产打标。残留本身不会立刻报错,
但一旦这个数字与其他用例的任务 id 相撞,那个按「恰好 N 个资产」断言的用例
就会失败,而现象(某个无关测试的资产计数不对)与原因(另一个测试的连接
关闭顺序)相距极远,排查成本很高。

改为关闭连接也走 t.Cleanup 并注册在清理之前(t.Cleanup 后进先出,保证清理
先跑),同时让清理失败显式报错而不是被吞掉。

注:同一模式(defer d.Close() + 在 t.Cleanup 里写库)在 db 包另有十余处,
本次只修被实证触发的那一处。

Co-Authored-By: Claude Code <[email protected]>
This commit is contained in:
luckoneandClaude Code committed 2026-09-30 16:24:47 +08:00
1 parent 50b7b2a05b
commit 18589b270e
1 file changed
+16 -3
+16 -3
View File
@@ -233,7 +233,15 @@ func TestCompanyICPAttribution(t *testing.T) {
if err != nil {
t.Skipf("postgres unavailable (%v) — skipping", err)
}
defer d.Close()
// 关连接必须走 t.Cleanup 且**注册在清理之前**:t.Cleanup 是后进先出,
// 先注册关闭 → 关闭最后执行,下面的数据清理才连得上库。
// 原先这里是 `defer d.Close()`:defer 在函数返回时先跑,t.Cleanup 在那之后
// 才执行,于是清理语句全落在**已关闭的连接**上、错误又被 `_, _ =` 丢弃,
// 资产与公司就永久残留在库里。残留本身不会立刻报错,但本用例用
// `MAX(companies.id)+1` 当假 TaskID 给资产打标(见下方 suffix),
// 一旦这个数字与别的用例的任务 id 撞上,那个用例按「恰好 N 个资产」的断言
// 就会莫名失败——排查成本极高。
t.Cleanup(func() { d.Close() })
var suffix int64
if err := d.QueryRow(`SELECT COALESCE(MAX(id),0)+1 FROM companies`).Scan(&suffix); err != nil {
@@ -246,8 +254,13 @@ func TestCompanyICPAttribution(t *testing.T) {
t.Fatal(err)
}
t.Cleanup(func() {
_, _ = d.Exec(`DELETE FROM assets WHERE task_ids @> ARRAY[$1]::bigint[]`, suffix)
_, _ = d.Exec(`DELETE FROM companies WHERE id=$1`, companyID)
// 不吞错误:清理失败会污染后续用例,必须让它在本次运行里显形。
if _, err := d.Exec(`DELETE FROM assets WHERE task_ids @> ARRAY[$1]::bigint[]`, suffix); err != nil {
t.Errorf("清理测试资产失败: %v", err)
}
if _, err := d.Exec(`DELETE FROM companies WHERE id=$1`, companyID); err != nil {
t.Errorf("清理测试公司失败: %v", err)
}
})
// A keyword can guide an Agent, but must never claim an asset by its name.