Files
homeagent-sdk/example/deepsearch/searxng_test.go
JianFeeeee e50bffa34f fix(deepsearch): 只关自己拉起的 SearXNG;条数改由插件侧截断
两个都由「真调用/真测试」暴露,且都会让线上搜索表现为「后端不可用」。

一、归属:接管不等于拥有(例:E2E 测试把生产后端带走)
  旧实现只要探活成功就认领关闭责任 → 同机第二个实例(测试拉起的插件、另一个 daemon)
  退出时就 docker compose stop 掉**线上正在用的**后端。实测:跑一次
  `go test ./internal/plugins/ -run TestRealPlugin_DeepSearch`,teardown 即关停
  127.0.0.1:8888,用户看到的就是「搜索后端起不来」。
  修:只有真正执行过 `docker compose up -d` 的实例才算「我们起的」;探到已在运行只接管。

二、条数:SearXNG 不认 count/limit(count/max_results 形同虚设)
  实测 ?count=3、?limit=3、不带参数返回**完全相同的 35 条**,所以截断必须在插件里做。
  旧实现把 count 当 limit 参数发给 SearXNG 就以为生效了 → 模型每次吞 35~58 条带摘要结果,
  还会把「命中 N 条」当成「拿到了 N 条」报给用户(实测发生过)。
  修:新增 limitResults(默认取 max_results,上限 20);输出改成
  「命中 N 条,返回前 M 条」;不再发无意义的 limit 参数。

验证:22 项单测全过、-race 干净、vet/gofmt 干净;两条归属测试做过扰动(把旧语义放回
去后必红,并如实打出它执行的 `docker compose stop -t 2`);内核 E2E 三条通过且
**跑完 healthz 仍 200、容器未重启**;线上 1.1.2 实测 count=3 → 「命中 40 条,返回前 3 条」。

版本 1.1.0 → 1.1.2。
2026-09-13 09:55:27 +08:00

224 lines
6.9 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 main
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
)
type fakeCall struct {
dir string
name string
args []string
}
func (c fakeCall) String() string { return c.name + " " + strings.Join(c.args, " ") }
// newFakeRunner 记录调用并返回预设结果
func newFakeRunner(calls *[]fakeCall, out string, err error) cmdRunner {
var mu sync.Mutex
return func(ctx context.Context, dir, name string, args ...string) (string, error) {
mu.Lock()
*calls = append(*calls, fakeCall{dir: dir, name: name, args: args})
mu.Unlock()
return out, err
}
}
// fastBudget 把就绪窗口压到毫秒级,避免单测真等
func fastBudget() searxBudget {
return searxBudget{
probe: 50 * time.Millisecond,
up: time.Second,
ready: 200 * time.Millisecond,
shutdown: time.Second,
}
}
// 1) 后端没跑 → 应执行 docker compose up -d并认领关闭责任
func TestEnsureSearxngStartsWhenUnreachable(t *testing.T) {
var calls []fakeCall
p := &Plugin{
name: "deepsearch", searxURL: "http://127.0.0.1:1", searxDir: "/tmp/fake-searx",
manageSearx: true, stopOnExit: true, userAgent: "test",
bud: fastBudget(), run: newFakeRunner(&calls, "Container searxng-agent Started", nil),
}
p.ensureSearxng()
if len(calls) != 1 {
t.Fatalf("应恰好拉起一次,实际 %d 次:%v", len(calls), calls)
}
got := calls[0]
if got.name != "docker" || strings.Join(got.args, " ") != "compose up -d" {
t.Errorf("命令不对:%s", got)
}
if got.dir != "/tmp/fake-searx" {
t.Errorf("工作目录应为配置的 compose 目录,实际 %q", got.dir)
}
if !p.searxOwned {
t.Error("既然是我们拉起的,就应认领关闭责任")
}
}
// 2) 后端已在跑 → 不重启,**且不认领关闭责任**
//
// 这条是关键同一台机器上会有第二个实例E2E 测试拉起的插件、另一个 daemon
// 如果「接管」也算「我拥有」,任一实例退出就会把生产后端关掉 —— 线上实测就是
// 测试实例在 teardown 时 `docker compose stop`,把搜索服务反复关停。
func TestEnsureSearxngAdoptsRunningBackendWithoutOwning(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/healthz" {
_, _ = w.Write([]byte("OK"))
return
}
http.NotFound(w, r)
}))
defer srv.Close()
var calls []fakeCall
p := &Plugin{
name: "deepsearch", searxURL: srv.URL, searxDir: "/tmp/fake-searx",
manageSearx: true, stopOnExit: true, userAgent: "test",
bud: fastBudget(), run: newFakeRunner(&calls, "", nil),
}
p.ensureSearxng()
if len(calls) != 0 {
t.Errorf("已在跑就不该重启它,实际执行了:%v", calls)
}
if p.searxOwned {
t.Error("不是我们拉起的,就不能认领关闭责任(否则退出时会带走别人的后端)")
}
}
// 2b) 接管的实例退出时,一个 docker 命令都不能发
func TestAdoptedBackendSurvivesShutdown(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("OK"))
}))
defer srv.Close()
var calls []fakeCall
p := &Plugin{
name: "deepsearch", searxURL: srv.URL, searxDir: "/tmp/fake-searx",
manageSearx: true, stopOnExit: true, userAgent: "test",
bud: fastBudget(), run: newFakeRunner(&calls, "", nil),
}
p.ensureSearxng()
if err := p.Stop(); err != nil {
t.Fatalf("Stop: %v", err)
}
if len(calls) != 0 {
t.Errorf("接管来的后端在退出时必须留着,实际执行了:%v", calls)
}
}
// 3) 关掉托管 → 完全不碰 docker
func TestEnsureSearxngDisabled(t *testing.T) {
var calls []fakeCall
p := &Plugin{
name: "deepsearch", searxURL: "http://127.0.0.1:1", searxDir: "/tmp/fake-searx",
manageSearx: false, stopOnExit: true, userAgent: "test",
bud: fastBudget(), run: newFakeRunner(&calls, "", nil),
}
p.ensureSearxng()
if len(calls) != 0 || p.searxOwned {
t.Errorf("manage_searxng=false 时不该有任何动作calls=%v owned=%v", calls, p.searxOwned)
}
}
// 4) 拉起失败不能让插件起不来(记日志即可)
func TestEnsureSearxngFailureNonFatal(t *testing.T) {
var calls []fakeCall
p := &Plugin{
name: "deepsearch", searxURL: "http://127.0.0.1:1", searxDir: "/tmp/fake-searx",
manageSearx: true, stopOnExit: true, userAgent: "test",
bud: fastBudget(), run: newFakeRunner(&calls, "Cannot connect to the Docker daemon", errors.New("exit status 1")),
}
p.ensureSearxng() // 不应 panic
if p.searxOwned {
t.Error("没拉起来就不该认领关闭责任(否则停止时会去关一个不是我们起的服务)")
}
}
// 5) 停止:关掉我们拉起的后端,且幂等
func TestShutdownStopsOwnedBackend(t *testing.T) {
var calls []fakeCall
runner := newFakeRunner(&calls, "ok", nil)
p := &Plugin{
name: "deepsearch", searxURL: "http://127.0.0.1:1", searxDir: "/tmp/fake-searx",
manageSearx: true, stopOnExit: true, userAgent: "test",
bud: fastBudget(), run: runner,
}
p.ensureSearxng()
calls = nil
p.shutdownSearxng()
if len(calls) != 1 {
t.Fatalf("应执行一次 compose stop实际 %v", calls)
}
if got := strings.Join(calls[0].args, " "); !strings.HasPrefix(got, "compose stop") {
t.Errorf("停止命令不对:%s", got)
}
if p.searxOwned {
t.Error("停止后应清掉认领标记")
}
p.shutdownSearxng() // 幂等:不应再调一次
if len(calls) != 1 {
t.Errorf("重复停止应无副作用,实际 %v", calls)
}
}
// 6) 不是我们拉起的 → 停止时不许动它
func TestShutdownSkippedWhenNotOwned(t *testing.T) {
var calls []fakeCall
p := &Plugin{
name: "deepsearch", searxDir: "/tmp/fake-searx", manageSearx: true, stopOnExit: true,
bud: fastBudget(), run: newFakeRunner(&calls, "", nil),
}
p.shutdownSearxng()
if len(calls) != 0 {
t.Errorf("不该去停一个我们没起的服务:%v", calls)
}
}
// 7) 配了「停止时保留」→ 认领过也不关
func TestShutdownKeepsBackendWhenConfigured(t *testing.T) {
var calls []fakeCall
p := &Plugin{
name: "deepsearch", searxURL: "http://127.0.0.1:1", searxDir: "/tmp/fake-searx",
manageSearx: true, stopOnExit: false, userAgent: "test",
bud: fastBudget(), run: newFakeRunner(&calls, "", nil),
}
p.ensureSearxng()
calls = nil
p.shutdownSearxng()
if len(calls) != 0 {
t.Errorf("stop_searxng_on_exit=false 时不应关闭:%v", calls)
}
}
// 8) Stop() 自身也要收尾(内核 stdin 关闭路径不会走 stop handler 的注册顺序之外)
func TestStopTriggersShutdown(t *testing.T) {
var calls []fakeCall
p := &Plugin{
name: "deepsearch", searxURL: "http://127.0.0.1:1", searxDir: "/tmp/fake-searx",
manageSearx: true, stopOnExit: true, userAgent: "test",
bud: fastBudget(), run: newFakeRunner(&calls, "", nil),
}
p.ensureSearxng()
calls = nil
if err := p.Stop(); err != nil {
t.Fatalf("Stop 返回错误: %v", err)
}
if len(calls) != 1 {
t.Errorf("Stop 应触发一次关闭,实际 %v", calls)
}
}