From 18589b270ecb3254312a95e6cc2124e7eeed81fa Mon Sep 17 00:00:00 2001 From: luckone <1139448596@qq.com> Date: Wed, 30 Sep 2026 15:57:29 +0800 Subject: [PATCH] =?UTF-8?q?fix(db):=20=E4=BF=AE=E6=AD=A3=E5=85=AC=E5=8F=B8?= =?UTF-8?q?=20ICP=20=E5=BD=92=E5=B1=9E=E7=94=A8=E4=BE=8B=E8=BF=9E=E6=8E=A5?= =?UTF-8?q?=E5=85=B3=E9=97=AD=E9=A1=BA=E5=BA=8F=E5=AF=BC=E8=87=B4=E7=9A=84?= =?UTF-8?q?=E8=B5=84=E4=BA=A7=E6=AE=8B=E7=95=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 清理注册在 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 --- db/company_scope_test.go | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/db/company_scope_test.go b/db/company_scope_test.go index 713938e..31fcbc3 100644 --- a/db/company_scope_test.go +++ b/db/company_scope_test.go @@ -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.