From eeba1e49f83ac52e5b29791be50bbe1a74187cd9 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Fri, 16 Oct 2026 07:51:21 +0800 Subject: [PATCH] =?UTF-8?q?fix(channels):=20account=5Fclear=5Fcredentialle?= =?UTF-8?q?ss=20=E5=B0=8A=E9=87=8D=20dry=5Frun=EF=BC=88args=3Dreq=20?= =?UTF-8?q?=E5=9B=9E=E9=80=80=E5=90=83=E6=8E=89=20body=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 真实缺陷(我自己在上一个提交里引入,部署后实测踩到) handler 开头照其它 18 处写了 `if args.platform == nil then args = req end`。 但本函数**不传 platform 是合法用法**(清全部平台)⇒ args.platform 自然为 nil ⇒ args 被换成 HandlerRequest ⇒ 刚解出来的 dry_run 就丢了 ⇒ **传 dry_run=false 却被当成未传,静默只预演**。 破坏性操作里这是最糟的形态:操作者以为执行了、实际没执行(反过来同样危险)。 实测:不带 platform 时 deleted 恒为 nil,池子一个没动。 ## 修法 兜底只认「req 本身就是参数表」这种形态(`req.body == nil and req.query == nil`), 不再用「某字段为 nil」当判据 —— 那个字段为 nil 可能只是本次调用**合法地没用它**。 ## ★ 判据为什么第一版没咬住 我原来的夹具用 **map body** 调 Call,而缺陷只在**字符串 body**(HTTP 真实路径) 上暴露 —— `bodyOf` 对 table 直接返回,根本走不到回退那行。 补 `TestAccountClearCredentiallessRespectsDryRunViaStringBody`(用 `InvokeHandler` + 字符串 body + 不传 platform)后变异才咬住。 **教训:夹具的调用形态必须与生产一致 —— 用 map 便捷调用会绕过 body 解析路径。** 判据 4 条(含本条)+ 变异 3/3。 --- .../channels_clear_credentialless_test.go | 40 +++++++++++++++++++ internal/lua/plugins/channels.lua | 10 ++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/internal/core/channels_clear_credentialless_test.go b/internal/core/channels_clear_credentialless_test.go index e85043f..72d24c9 100644 --- a/internal/core/channels_clear_credentialless_test.go +++ b/internal/core/channels_clear_credentialless_test.go @@ -193,3 +193,43 @@ func accountIDs(t *testing.T, h *accountCleanupHarness, platform string) []strin } return out } + +// ★ 字符串 body(真实 HTTP 路径)+ 不传 platform 时,dry_run 必须被尊重。 +// +// 反例(实测踩到,我自己写的):照其它 handler 写 +// +// `if args.platform == nil then args = req end` +// +// —— 而"不传 platform"在本函数里是**合法用法**(清全部平台),于是 args 被换成 +// HandlerRequest,body 里解出来的 dry_run 就丢了 ⇒ 传 dry_run=false 却被当成 +// 未传,静默只预演。**破坏性操作里最糟的形态**:操作者以为执行了、实际没执行。 +// +// 夹具必须用**字符串 body**(我第一版传 map,走不到这条路径,所以没咬住)。 +func TestAccountClearCredentiallessRespectsDryRunViaStringBody(t *testing.T) { + h := newAccountCleanupHarness(t) + + // 不带 platform + dry_run=false,且 body 是**字符串**(HTTP 真实形态) + res, err := h.ps.InvokeHandler("channels", "account_clear_credentialless", + &lua.HandlerRequest{Method: "POST", Body: `{"dry_run":false}`}) + if err != nil { + t.Fatal(err) + } + if res.Status != 200 { + t.Fatalf("status=%d body=%v", res.Status, res.Body) + } + j, _ := res.JSON.(map[string]interface{}) + if j["dry_run"] == true { + t.Fatalf("字符串 body 里传了 dry_run=false 却被当成未传(回退吃掉 body): %v", j) + } + if j["deleted"] == nil { + t.Fatalf("dry_run=false 时必须真的执行(应有 deleted 字段): %v", j) + } + // 全平台的 5 个空壳都被删(codearts 3 + trae 2) + if got := len(accountIDs(t, h, "codearts")); got != 1 { + t.Fatalf("codearts 应剩 1(有凭据那个),实得 %d", got) + } + if got := len(accountIDs(t, h, "trae")); got != 0 { + t.Fatalf("trae 的 2 个空壳应被删,实得 %d", got) + } + t.Logf("✓ 字符串 body 且无 platform 时 dry_run=false 生效: deleted=%v", j["deleted"]) +} diff --git a/internal/lua/plugins/channels.lua b/internal/lua/plugins/channels.lua index 6a538de..629011d 100644 --- a/internal/lua/plugins/channels.lua +++ b/internal/lua/plugins/channels.lua @@ -6440,7 +6440,15 @@ end function plugin.account_clear_credentialless(req) local args = bodyOf(req) - if args.platform == nil then args = req end + -- ⚠️ 不要照其它 handler 写 `if args.platform == nil then args = req end`: + -- 那个回退是为「直接把参数当 req 传」的旧调用准备的,但它会**丢掉 body** + -- —— 本函数不传 platform 是**合法用法**(清全部平台),此时 args.platform + -- 自然为 nil,回退会把 args 换成 HandlerRequest ⇒ 刚解出来的 dry_run 就没了, + -- 于是「传 dry_run=false 却被当成未传」而静默只预演(破坏性操作里最糟的形态: + -- 操作者以为执行了、实际没执行,或者反过来)。 + -- 实测踩到:不带 platform 时 deleted 恒为 nil。 + -- 兜底只认「req 本身就是参数表」这种形态。 + if type(req) == "table" and req.body == nil and req.query == nil then args = req end local dry = args.dry_run if dry == nil then dry = true end