- 插件管理器加生产动作处理器登记面(RegisterActions);未登记处理器/未声明动作仍 404 - web-search 四个动作真读写真源 + 真发 HTTP 探针:plugin.listProviders/saveProvider/ testProvider/reauthProvider(配置落 $DATA_DIR/config/plugins/web-search.json,0600) - 失败回结构化错误码(不含上游原文)+ 审计带 code 字段;501 文案改为「未登记处理器」 - 拆出 panelactions.go(动作索引推导/OptionsActionName/三闸门分发),panels.go 519→347 行 缺口(如实):面板保存后运行中的工具**不热重载**(下一片做);声明面缺 primary/fallback (按 DECISIONS-2026-10-06 #1/#2 补)。
89 lines
3.7 KiB
Go
89 lines
3.7 KiB
Go
package main
|
|
|
|
// 本文件钉住搜索提供商动作面的**危险/边界结局**(切片 L1/PLG-2):损坏配置不许被覆盖、
|
|
// 装配期要如实点名「声明了却没实现」的动作。它们与主用例集分开成文件的理由只有一条:
|
|
// `wire_plugin_actions_test.go` 已接近「文件 ≤500 行」的上限(AGENTS §10)。
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/bus"
|
|
pluginmgr "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/plugin"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
|
|
"git.yeij.top/AskaEth/Cyrene_v2/plugins/tools/io/search"
|
|
)
|
|
|
|
// TestWebSearchActionConfigUnreadableIsReported 锁定「文件存在但不合法」的口径:
|
|
// 结构化失败(500 config_unreadable),**不覆盖**原文件 —— 损坏的配置可以人工救回。
|
|
func TestWebSearchActionConfigUnreadableIsReported(t *testing.T) {
|
|
env := newActionTestEnv(t, "WEB_SEARCH_PROVIDER="+search.NameSearXNG)
|
|
const broken = `{"version":1,"providers":[{"provider":"bocha"},{"provider":"BOCHA"}]}`
|
|
wsWriteRawPanel(t, env.path, broken)
|
|
|
|
_, err := env.dispatch(t, search.ActionListProviders, map[string]any{})
|
|
ae, ok := pluginmgr.AsActionError(err)
|
|
if !ok || ae.Code != actionCodeConfigUnreadable || ae.Status != http.StatusInternalServerError {
|
|
t.Fatalf("err = %#v, want 500 config_unreadable", err)
|
|
}
|
|
if _, err := env.dispatch(t, search.ActionSaveProvider, map[string]any{
|
|
"values": map[string]any{"provider": search.NameBocha, "apiKey": "sk-x"},
|
|
}); err == nil {
|
|
t.Fatal("配置不合法时必须拒绝保存(否则会静默盖掉可救回的原文件)")
|
|
}
|
|
raw, rerr := os.ReadFile(env.path)
|
|
if rerr != nil {
|
|
t.Fatalf("read: %v", rerr)
|
|
}
|
|
if string(raw) != broken {
|
|
t.Fatalf("原文件被改动了:%q", raw)
|
|
}
|
|
}
|
|
|
|
// TestWarnImpliedButMissingNamesGaps 锁定装配期的**核对**:声明里有、登记里没有的动作
|
|
// 必须在日志里点名(否则用户只会看到一个点了回 501 的按钮)。这里直接覆盖核对函数本身。
|
|
func TestWarnImpliedButMissingNamesGaps(t *testing.T) {
|
|
cfg := wsLoadConfig(t, "WEB_SEARCH_PROVIDER="+search.NameSearXNG)
|
|
b := bus.New()
|
|
defer b.Close()
|
|
mgr := pluginmgr.NewManager(registry.New(), filepath.Join(cfg.DataDir, "plugins.json"), testLog())
|
|
defer mgr.Close(context.Background())
|
|
|
|
var buf bytes.Buffer
|
|
log := logger.New(logger.Options{Out: &buf, Level: "debug"})
|
|
wirePanelHub(&app{cfg: cfg, log: log, bus: b, plugins: mgr})
|
|
hub := mgr.Panels()
|
|
|
|
// 模拟「插件声明新增了一个动作,但宿主忘了给它接线」:补一条声明(Declare 是整体
|
|
// 替换语义 ⇒ 原有四个动作 + 候选来源仍在),只多一个没人实现的名字。
|
|
extra := sdk.Panel{
|
|
ID: "extra", Title: "额外页",
|
|
Sections: []sdk.Section{{
|
|
Type: sdk.SectionTable, ID: "t", Title: "额外表",
|
|
Source: &sdk.Source{Action: "plugin.notWired"},
|
|
Columns: []sdk.Column{{Key: "k", Label: "K"}},
|
|
}},
|
|
}
|
|
if _, err := hub.Declare(search.PanelPluginID, append(search.AdminPanels(), extra)); err != nil {
|
|
t.Fatalf("Declare: %v", err)
|
|
}
|
|
warnImpliedButMissing(hub, log)
|
|
|
|
out := buf.String()
|
|
if !strings.Contains(out, "plugin.notWired") || !strings.Contains(out, "no handler") {
|
|
t.Fatalf("声明了却没实现的动作必须被点名(否则只剩一个回 501 的按钮): %q", out)
|
|
}
|
|
// 已登记的动作不得被误报。
|
|
if idx := strings.Index(out, "action=plugin.listProviders"); idx >= 0 &&
|
|
strings.Contains(out[idx:], "no handler") {
|
|
t.Fatalf("已登记的动作被误报成缺处理器: %q", out)
|
|
}
|
|
}
|