Files
AutoZero/tools/websearch_test.go
T
Sakurasan d5dac5a6fc feat: AutoZero 通用 Agent 框架(从 AutoGo 五阶段拆分完成)
llm(三协议 Provider)/ session(JSONL 持久化)/ tools(Registry、围栏、
builtin、computer)/ cdp(browser)/ skills / mcp / agent(循环、两级压缩、
编排、审批门控)+ 底座 jsonl/textx/execx/diag/scratch/spill/imagecache。

产品装配代码(config 桥接、schedule 工具、memory/plan/retro/prompt)
留在 AutoGo,宿主接入方式见 MIGRATION.md。
2026-10-26 00:54:46 +08:00

171 lines
6.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package tools
import (
"context"
"os"
"strings"
"testing"
"unicode/utf8"
"golang.org/x/net/html"
)
// braveFixture 按 Brave 真实版面精简:class 带 svelte 哈希、图标 <a> 无文本、
// 广告块 data-type 非 web、站内链接要过滤、同一结果重复出现。
var braveFixture = `<html><body><main class="main-column">
<div class="snippet svelte-jmfu5f" data-pos="0" data-type="web" data-keynav="true">
<div class="result-body"><div class="result-content">
<a href="https://github.com/SocialSisterYi/bilibili-API-collect" target="_self" class="svelte-14r20fy">
<div class="site-name-wrapper"><img class="favicon" src="https://imgs.search.brave.com/xx"></div>
<div class="title">SocialSisterYi/bilibili-API-collect · GitHub</div>
</a>
<div class="snippet-description">bilibili-API-collect 收集了各种 bilibili 的 API 与用法。</div>
</div></div>
</div>
<div class="snippet svelte-jmfu5f" data-pos="1" data-type="web">
<div class="result-content">
<a href="https://github.com/SocialSisterYi/bilibili-API-collect" class="svelte-14r20fy">
<div class="title">重复的同一条结果</div>
</a>
<div class="snippet-description">应当被去重掉。</div>
</div>
</div>
<div class="snippet svelte-jmfu5f" data-pos="2" data-type="ad">
<div class="result-content">
<a href="https://ads.example.com/bili"><div class="title">广告:别选这条</div></a>
<div class="snippet-description">广告摘要</div>
</div>
</div>
<div class="snippet svelte-jmfu5f" data-pos="3" data-type="web">
<div class="result-content">
<a href="/internal/only"><div class="title">站内链接</div></a>
<div class="snippet-description">不该出现在结果里</div>
</div>
</div>
<div class="snippet svelte-jmfu5f" data-pos="4" data-type="web">
<div class="result-content">
<a href="https://example.com/docs"><div class="title search-snippet-title line-clamp-1">中文标题很长</div></a>
<div class="generic-snippet svelte-1cwdgg3"><div class="content line-clamp-dynamic">` + strings.Repeat("很长的中文摘要内容", 40) + `</div></div>
</div>
</div>
</main></body></html>`
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(`<html><body><div class="whatever"><a href="https://x.com">hi</a></div></body></html>`))
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(`<html><body>
<div class="snippet" data-type="web"><a href="https://a.example/x"><div class="title">标题</div></a>
<div class="site-name-wrapper">Example a.example</div></div></body></html>`))
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)
}