package tools import ( "context" "os" "strings" "testing" "unicode/utf8" "golang.org/x/net/html" ) // braveFixture 按 Brave 真实版面精简:class 带 svelte 哈希、图标 无文本、 // 广告块 data-type 非 web、站内链接要过滤、同一结果重复出现。 var braveFixture = `
SocialSisterYi/bilibili-API-collect · GitHub
bilibili-API-collect 收集了各种 bilibili 的 API 与用法。
重复的同一条结果
应当被去重掉。
广告:别选这条
广告摘要
站内链接
不该出现在结果里
中文标题很长
` + strings.Repeat("很长的中文摘要内容", 40) + `
` func parseFixture(t *testing.T) []searchResult { t.Helper() root, err := html.Parse(strings.NewReader(braveFixture)) if err != nil { t.Fatal(err) } return dedupe(extractBrave(root)) } func TestExtractBraveResults(t *testing.T) { got := parseFixture(t) if len(got) != 2 { t.Fatalf("结果数 = %d, want 2(广告与站内链接必须被滤掉,重复要合并):\n%s", len(got), dumpResults(got)) } if got[0].URL != "https://github.com/SocialSisterYi/bilibili-API-collect" { t.Errorf("首条 URL = %q", got[0].URL) } if !strings.Contains(got[0].Title, "bilibili-API-collect") { t.Errorf("标题没取到:%q", got[0].Title) } if !strings.Contains(got[0].Snippet, "收集了各种 bilibili") { t.Errorf("摘要没取到:%q", got[0].Snippet) } if got[1].URL != "https://example.com/docs" { t.Errorf("第二条 URL = %q", got[1].URL) } } // TestRenderResultsCutsOnRuneBoundary:中文摘要截断不许切半个字。 func TestRenderResultsCutsOnRuneBoundary(t *testing.T) { got := parseFixture(t) out := renderResults("bilibili 投币 API", got, 8) if !strings.Contains(out, "1. ") || !strings.Contains(out, "web search:") { t.Errorf("排版缺编号或回显查询:\n%s", out) } // 每一行都必须合法 UTF-8(截断标记除外)。 for _, line := range strings.Split(out, "\n") { line = strings.TrimSuffix(line, "…") if !isUTF8Valid(line) { t.Errorf("切坏了 UTF-8:%q", line) } } // 只渲染前 limit 条。 if n := strings.Count(renderResults("q", got, 1), "\n "); n > 3 { t.Errorf("limit=1 却渲染了多条:%d", n) } } // TestExtractBraveEmptyOnUnknownMarkup:版面不认识时必须返回空(而不是崩或返回垃圾), // 由工具层给出"换 fetch"的可操作错误。 func TestExtractBraveEmptyOnUnknownMarkup(t *testing.T) { root, _ := html.Parse(strings.NewReader(`
hi
`)) if rs := extractBrave(root); len(rs) != 0 { t.Errorf("无关版面应解析出 0 条,got %d", len(rs)) } } // TestWebSearchToolShape:标注必须是网络类副作用工具——与 fetch 同档, // 一次批准按工具名缓存,不会每次轮询都打扰。 func TestWebSearchToolShape(t *testing.T) { tl := WebSearch() if tl.Name != "websearch" || !tl.SideEffect || tl.Kind != KindNetwork { t.Errorf("工具标注不对:%+v", tl) } if tl.ReadOnly || tl.ConcurrencySafe { t.Error("websearch 访问外部网络,不该标只读/可并发") } if _, err := tl.Run(context.Background(), `{"query":""}`); err == nil { t.Error("空 query 必须报错") } } // TestBraveSnippetNotFallbackToBlockText:认不出摘要元素时留空。 // 退回整块文本会把站点名/面包屑/URL 一起吐给模型(实时页面上真出现过)。 func TestBraveSnippetNotFallbackToBlockText(t *testing.T) { root, _ := html.Parse(strings.NewReader(`
标题
Example a.example
`)) rs := extractBrave(root) if len(rs) != 1 { t.Fatalf("应解析出 1 条,got %d", len(rs)) } if rs[0].Snippet != "" { t.Errorf("摘要应为空而不是整块噪声:%q", rs[0].Snippet) } if rs[0].Title != "标题" { t.Errorf("标题 = %q", rs[0].Title) } } // TestWebSearchLive 是真实网络冒烟:默认跳过,AUTOGO_LIVE_NET=1 时才跑。 // 单元夹具证明不了"后端今天还认我们"这件事,而这条工具整条链路就悬在这上面。 func TestWebSearchLive(t *testing.T) { if os.Getenv("AUTOGO_LIVE_NET") == "" { t.Skip("需要联网:AUTOGO_LIVE_NET=1") } res, err := WebSearch().Run(context.Background(), `{"query":"bilibili-API-collect 投币记录 API","count":5}`) if err != nil { t.Fatalf("实时搜索失败:%v", err) } t.Logf("结果:\n%s", res.Text) if !strings.Contains(res.Text, "bilibili-API-collect") { t.Errorf("这条查询应当命中权威文档仓库:\n%s", res.Text) } } func dumpResults(rs []searchResult) string { var b strings.Builder for _, r := range rs { b.WriteString(" " + r.URL + " | " + r.Title + " | " + r.Snippet + "\n") } return b.String() } func isUTF8Valid(s string) bool { return utf8.ValidString(s) }