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。
171 lines
6.3 KiB
Go
171 lines
6.3 KiB
Go
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)
|
||
}
|