接入位表六家扩到八家,端点形状与错误码一字未改(契约 v0.87,仅两处名单)。
- tencent_wsa:POST {base}/SearchPro + Authorization Bearer,零签名。
两条官方口径决定实现:HTTP 状态码恒 200(判失败只能读 Response.Error.Code);
Pages 是「JSON 字符串数组」需二次解析。错误码按官方表分类
(UnauthorizedOperation/AuthFailure.* = 凭据、ResourceUnavailable = 欠费、
RequestLimitExceeded* = 限流),文案只带 code 不带上游 Message。
- metaso:POST {base}/api/v1/search,响应 schema 仅第三方来源且互相打架,
实现走宽松解析(数组候选与字段名回退),逐处标「未实测」。
- 零新 env 键:两家只走 $DATA_DIR/config/plugins/web-search.json,
.env / .env.example 未动;组合根 websearch_providers.go 零功能改动。
- 契约 v0.87:values.provider 合法闭集与 plugin.providers 候选各加两条,
计数不变(14 + 25 + 40 = 79)。
92 lines
3.8 KiB
Go
92 lines
3.8 KiB
Go
package main
|
||
|
||
// 本文件钉住本片新增的两家(`tencent_wsa` / `metaso`)在**组合根里真的接通了**:
|
||
// 它们**没有 `.env` 字段**(`.env` 已冻结),因此凭据与地址只能来自面板文件
|
||
// `$DATA_DIR/config/plugins/web-search.json`,而组合根**零改动**地按接入位表把它们投影出来
|
||
// (`resolveSearchProviders` 逐家走 `search.Descriptors()`)。
|
||
//
|
||
// 为什么值得端到端一条:`cmd/cyrene/websearch_providers.go` 里**一行都没改**——这正是
|
||
// 「按接入位表投影」的设计意图;没有这条用例,「零改动」就只是一句没有证据的话。
|
||
//
|
||
// 夹具(假上游、`wsSurfaceApp`、`wsExecuteSearch`)在 websearch_testenv_test.go /
|
||
// websearch_projection_test.go;`.env` 字段表的完整性由 wire_web_search_*_test.go 钉住。
|
||
|
||
import (
|
||
"net/http"
|
||
"strings"
|
||
"testing"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config/runtimeconfig"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/plugins/tools/io/search"
|
||
)
|
||
|
||
// TestWebSearchPanelOnlyProvidersWiredEndToEnd 是判据:面板文件里配好一家**没有 env 字段**
|
||
// 的家 ⇒ 工具进注册面 ⇒ 真的把请求打到面板里那个地址、带上面板里那把 key,且结果里如实
|
||
// 标注 provider。
|
||
func TestWebSearchPanelOnlyProvidersWiredEndToEnd(t *testing.T) {
|
||
t.Parallel()
|
||
cases := []struct {
|
||
name string
|
||
path string
|
||
fixture string
|
||
}{
|
||
{
|
||
name: search.NameTencentWSA,
|
||
path: "/SearchPro",
|
||
// Pages 的元素是 JSON **字符串**(官方 Array of String 形状)。
|
||
fixture: `{"Response":{"Pages":["{\"title\":\"元宝结果\",\"url\":\"https://t.example/1\",` +
|
||
`\"passage\":\"元宝摘要\"}"],"Query":"昔涟","RequestId":"r"}}`,
|
||
},
|
||
{
|
||
name: search.NameMetaso,
|
||
path: "/api/v1/search",
|
||
fixture: `{"webpages":[{"title":"秘塔结果","link":"https://m.example/1","snippet":"秘塔摘要"}]}`,
|
||
},
|
||
}
|
||
for _, tc := range cases {
|
||
tc := tc
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
t.Parallel()
|
||
const key = "test-panel-only-key"
|
||
up := newWSFakeUpstream(t, http.StatusOK, tc.fixture)
|
||
// `.env` 只给「谁当主家」:这两家在 `.env` 里没有字段,凭据与地址只能来自面板文件。
|
||
cfg := wsLoadConfig(t, "WEB_SEARCH_PROVIDER="+tc.name)
|
||
// 前提:它确实不在 `.env` 字段表里(否则这条用例会退化成既有 6 家的回归)。
|
||
for _, p := range searchProviderVector(cfg.WebSearch) {
|
||
if strings.EqualFold(p.Name, tc.name) {
|
||
t.Fatalf("前提被破坏:%s 不该出现在 .env 的字段表里", tc.name)
|
||
}
|
||
}
|
||
// 前提:接入位表里确有它(否则组合根根本投影不出来)。
|
||
if !search.Known(tc.name) {
|
||
t.Fatalf("前提被破坏:%s 应已在接入位表里登记", tc.name)
|
||
}
|
||
wsSavePanel(t, cfg, runtimeconfig.WebSearchFile{
|
||
Provider: tc.name,
|
||
Providers: []runtimeconfig.ProviderEntry{
|
||
{Provider: tc.name, APIKey: key, BaseURL: up.url, Enabled: runtimeconfig.EnableFlag(true)},
|
||
},
|
||
})
|
||
|
||
a := wsSurfaceApp(t, cfg, nil)
|
||
if !wsHasTool(a, webSearchToolName) {
|
||
t.Fatalf("面板文件里配好的 %s 应让工具进注册面,实际工具面 = %v", tc.name, toolNames(a))
|
||
}
|
||
if got := wsExecuteSearch(t, a, "昔涟"); got["provider"] != tc.name {
|
||
t.Fatalf("provider = %v, want %q(如实标注实际应答的家)", got["provider"], tc.name)
|
||
}
|
||
select {
|
||
case r := <-up.hits:
|
||
if r.URL.Path != tc.path {
|
||
t.Errorf("请求路径 = %q, want %q(各家固定路径)", r.URL.Path, tc.path)
|
||
}
|
||
if got := r.Header.Get("Authorization"); got != "Bearer "+key {
|
||
t.Errorf("Authorization = %q,want 面板文件里那把 key(凭据必须以文件为准)", got)
|
||
}
|
||
default:
|
||
t.Fatalf("%s 一个请求都没收到 —— 面板里的配置没接到 provider 上", tc.name)
|
||
}
|
||
})
|
||
}
|
||
}
|