fix(websearch): 面板「填 key + 同一次选它当主 provider」不再被自己的校验回滚
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

真机报障(2026-10-07 09:50):面板上给 zhipu 填了 API Key,保存却一直回
400 primary_not_configured;盘上配置文件 mtime 一动不动 —— 连刚填的 key
一起被回滚,界面表现为「怎么填都存不进去」。

根因(顺序错误):saveProvider 先 applySelectionPatch(内含「主 provider 必须
已配齐」的校验),后 UpsertProviderEntry。校验看的是**盘上的旧文件**,而这次
提交里的 key 还没落盘 ⇒ 校验判它没配齐,整笔拒绝。

修法:把校验从 applySelectionPatch 里摘出来,改名 mergeSelectionPatch(只合并
文件级字段 + 回报有没有给 primary),由 saveProvider 在 UpsertProviderEntry
**之后**对最终文件校验,再写盘。判据本身一字未改 —— 「真的没配齐的主家照样拒」。

验证:cmd/cyrene 整包测试绿;新增回归 TestWebSearchSaveProviderAndPrimaryInOneSubmit
(同一次提交填 key + 选主家 ⇒ 200 且盘上 key 与 primary 都在;反向:真没配齐仍 400
且文件字节不变);用 -overlay 把校验改回「拿旧文件校验」⇒ 该用例变红且报错文案
与主人看到的逐字相同。
This commit is contained in:
AskaEth committed 2026-10-07 09:56:37 +08:00
1 parent ef8fc66e54
commit aa0bf8b5a0
3 files changed
+73 -17

No files matched your search

+46
View File
@@ -280,3 +280,49 @@ func TestWebSearchProbeNotConfiguredIs400(t *testing.T) {
t.Fatalf("err = %#v, want 400 %s", err, search.ProbeNotConfigured)
}
}
// TestWebSearchSaveProviderAndPrimaryInOneSubmit 是 2026-10-07 真机报障的回归:
// 面板上第一次给某家填 key、又在**同一次提交**里把它选为主 provider,必须**一次成功**。
//
// 缺陷原形:主 provider 的可用性校验拿的是**盘上的旧文件**(那条 provider 还没 key)
// ⇒ 整笔(**连同刚填的 key**)被 400 primary_not_configured 回滚,界面上表现为
// 「怎么填都存不进去」,而盘上文件 mtime 一动不动。修法:校验移到「本次条目已并进
// 文件之后」(websearch_action_data.go 的 saveProvider 先 UpsertProviderEntry 再 check)。
func TestWebSearchSaveProviderAndPrimaryInOneSubmit(t *testing.T) {
env := newReloadTestEnv(t, "WEB_SEARCH_PROVIDER="+search.NameSearXNG)
// 前置:盘上那条 zhipu 没有 key —— 面板第一次配置它时正是这个状态。
if e, ok := findProviderEntry(env.loadFile(t), search.NameZhipu); !ok || strings.TrimSpace(e.APIKey) != "" {
t.Fatalf("前置不成立:盘上 zhipu 应存在且尚无 key(found=%v, key=%q)", ok, e.APIKey)
}
if _, err := env.save(map[string]any{
"provider": search.NameZhipu, "apiKey": "test-key-001", "enabled": true,
"primary": search.NameZhipu,
}); err != nil {
t.Fatalf("同一次提交里填 key + 选它当主家被拒(键与选择必须能一起落盘): %v", err)
}
after := env.loadFile(t)
if got := strings.ToLower(strings.TrimSpace(after.Provider)); got != search.NameZhipu {
t.Fatalf("盘上 primary = %q, want %q", got, search.NameZhipu)
}
entry, ok := findProviderEntry(after, search.NameZhipu)
if !ok || entry.APIKey != "test-key-001" {
t.Fatalf("盘上 zhipu 条目 = %+v, want 带本次填的 key", entry)
}
// 反向护栏:**真的没配齐**的主家仍必须被拒,且盘上一个字节都不动。
// (本次修复只改「校验看哪一份文件」,不改判据。)
beforeReject := env.rawFile(t)
_, err := env.save(map[string]any{
"provider": search.NameBocha, "enabled": true, "primary": search.NameBocha,
})
ae, ok := pluginmgr.AsActionError(err)
if !ok || ae.Code != actionCodePrimaryNotConfigured {
t.Fatalf("err = %#v, want 400 %s(bocha 这次没填 key)", err, actionCodePrimaryNotConfigured)
}
if got := env.rawFile(t); !bytes.Equal(got, beforeReject) {
t.Fatalf("被拒的保存动了盘上的文件:\nbefore=%s\nafter =%s", beforeReject, got)
}
}