mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-09-20 08:58:03 +00:00
两个都由「真调用/真测试」暴露,且都会让线上搜索表现为「后端不可用」。 一、归属:接管不等于拥有(例: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。
224 lines
6.9 KiB
Go
224 lines
6.9 KiB
Go
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)
|
||
}
|
||
}
|