mirror of
https://gitcode.com/JianFeeeee/homeagent-sdk.git
synced 2026-11-06 17:27:24 +00:00
feat(deepsearch): 联网检索插件 + SearXNG 生命周期托管
把原 websearch 示例改名为 deepsearch(目录/go.mod/plg.json/工具前缀/README 全量对齐, 工具名 websearch_* → deepsearch_*)。 新增 searxng.go:插件自己托管搜索后端 - 启动探 healthz:已在跑则直接接管(不重启),没跑就 docker compose up -d 并等就绪 - 关闭动作注册为 stop handler(幂等、限时 4s < 内核 5s 宽限期) - 配置 manage_searxng / searxng_dir / stop_searxng_on_exit - 崩溃/被 kill 时不关后端(下次启动接管):安全失败方向 - 可注入 cmdRunner + 时间预算,8 项单测离线覆盖接管/拉起/失败/幂等/保留 契约依据(internal/plugin/proc):停止插件 = plugin.stop → RunStopHandlers(LIFO、 幂等)→ Stop() → exit(0),宽限期 5s;stdin 关闭同路径。 验证:19 项单测(18 通过 + 1 联调跳过)、-race 干净、vet/gofmt 干净;内核 E2E 真调用 返回 58 条/37 条、58/58 带摘要;线上 daemon 实测 stop handler 与冷启动(约 3s)。
This commit is contained in:
197
example/deepsearch/searxng_test.go
Normal file
197
example/deepsearch/searxng_test.go
Normal file
@ -0,0 +1,197 @@
|
||||
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) 后端已在跑 → 不重启,直接接管
|
||||
func TestEnsureSearxngAdoptsRunningBackend(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("接管后也应负责停止(与「不重启」不冲突)")
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user