- GET /api/v1/admin/panels:按调用者角色过滤后的接入点快照(PLG-1 的 catalog 挂进插件管理器)
- POST /api/v1/admin/plugins/{id}/actions/{action}:**只接受该插件声明过的动作名**
(未声明 ⇒ 404);写动作 writeAuth + 落审计;插件只给动作名不给 URL ⇒ 不给插件开任意请求面
- 生命周期:install/uninstall/enable/disable 变化后**重算面板集合**并经进程内 bus 推 panels_changed
(前端可热更新;不新开端口)
契约 v0.78 登记由主会话随后统一落(本片报告给了可直接粘贴的契约段)。
142 lines
5.1 KiB
Go
142 lines
5.1 KiB
Go
package plugin
|
||
|
||
import (
|
||
"context"
|
||
"path/filepath"
|
||
"testing"
|
||
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/internal/config/runtimeconfig"
|
||
"git.yeij.top/AskaEth/Cyrene_v2/pkg/plugins/sdk"
|
||
)
|
||
|
||
// 本文件锁定 PLG-2 的**接线**:面板目录跟着插件管理器的安装/卸载/启停一起重算,
|
||
// 并且每次重算推一条带原因的变更通知(§5.2.1 生命周期一栏)。
|
||
|
||
// newPanelManager 构造一个已接线面板中枢的管理器(plugins.json 落在临时目录)。
|
||
func newPanelManager(t *testing.T) (*Manager, *Panels, *changeRecorder) {
|
||
t.Helper()
|
||
mgr := NewManager(registry.New(), filepath.Join(t.TempDir(), "plugins.json"), nil)
|
||
hub, rec := newTestHub(t)
|
||
mgr.AttachPanels(hub)
|
||
if mgr.Panels() != hub {
|
||
t.Fatalf("Panels() 未回接线的中枢")
|
||
}
|
||
return mgr, hub, rec
|
||
}
|
||
|
||
// installFake 安装一个指向假 MCP server 的插件。
|
||
func installFake(t *testing.T, mgr *Manager, name, endpoint string) {
|
||
t.Helper()
|
||
_, err := mgr.Install(context.Background(), runtimeconfig.PluginConfig{
|
||
Name: name, Kind: runtimeconfig.PluginKindMCP,
|
||
Transport: runtimeconfig.TransportHTTP, Endpoint: endpoint,
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("Install(%s): %v", name, err)
|
||
}
|
||
}
|
||
|
||
// TestManagerPanelLifecycleInstallUninstall 锁定:安装 ⇒ 声明可见 + 推 install;
|
||
// 卸载 ⇒ 声明与动作一起消失 + 推 uninstall。
|
||
func TestManagerPanelLifecycleInstallUninstall(t *testing.T) {
|
||
t.Parallel()
|
||
mcp := newFakeMCP(t, "", "add")
|
||
mgr, hub, rec := newPanelManager(t)
|
||
defer mgr.Close(context.Background())
|
||
|
||
// 声明在安装之前就登记(模拟未来的 manifest 读取:先有声明,装成之后生效)。
|
||
if _, err := hub.Declare("calc", []sdk.Panel{samplePanel()}); err != nil {
|
||
t.Fatalf("Declare: %v", err)
|
||
}
|
||
installFake(t, mgr, "calc", mcp.URL)
|
||
|
||
if got := mustIDs(t, hub); len(got) != 1 {
|
||
t.Fatalf("安装后应当看到面板,实际 %v", got)
|
||
}
|
||
if _, ok := hub.Action("calc", "plugin.saveProvider"); !ok {
|
||
t.Fatalf("安装后动作应当可分发")
|
||
}
|
||
|
||
if removed, err := mgr.Uninstall(context.Background(), "calc"); err != nil || !removed {
|
||
t.Fatalf("Uninstall = (%v,%v),期望 (true,nil)", removed, err)
|
||
}
|
||
if got := mustIDs(t, hub); len(got) != 0 {
|
||
t.Fatalf("卸载后不该再有面板,实际 %v", got)
|
||
}
|
||
if _, ok := hub.Action("calc", "plugin.saveProvider"); ok {
|
||
t.Fatalf("卸载后动作不得仍被声明")
|
||
}
|
||
reasons := changeReasons(rec.all())
|
||
if len(reasons) != 3 || reasons[0] != PanelChangeDeclared ||
|
||
reasons[1] != PanelChangeInstalled || reasons[2] != PanelChangeUninstalled {
|
||
t.Fatalf("变更原因 = %v,期望 [declare install uninstall]", reasons)
|
||
}
|
||
}
|
||
|
||
// TestManagerPanelLifecycleDisableEnable 锁定 §5.2.1 的启停一栏:停用摘掉入口
|
||
// (且动作不再分发),启用让它回来 —— 两条路各推一条变更。
|
||
func TestManagerPanelLifecycleDisableEnable(t *testing.T) {
|
||
t.Parallel()
|
||
mcp := newFakeMCP(t, "", "add")
|
||
mgr, hub, rec := newPanelManager(t)
|
||
caps := mgr.reg
|
||
defer mgr.Close(context.Background())
|
||
|
||
if _, err := hub.Declare("calc", []sdk.Panel{samplePanel()}); err != nil {
|
||
t.Fatalf("Declare: %v", err)
|
||
}
|
||
installFake(t, mgr, "calc", mcp.URL)
|
||
before := len(rec.all())
|
||
|
||
if err := mgr.SetEnabled(context.Background(), "calc", false); err != nil {
|
||
t.Fatalf("SetEnabled(false): %v", err)
|
||
}
|
||
if got := mustIDs(t, hub); len(got) != 0 {
|
||
t.Fatalf("停用后入口应当不出现,实际 %v", got)
|
||
}
|
||
if _, err := caps.Get("mcp:calc"); err == nil {
|
||
t.Fatalf("停用后该插件的 MCP 能力应当已下线")
|
||
}
|
||
if err := mgr.SetEnabled(context.Background(), "calc", true); err != nil {
|
||
t.Fatalf("SetEnabled(true): %v", err)
|
||
}
|
||
if got := mustIDs(t, hub); len(got) != 1 {
|
||
t.Fatalf("重新启用后入口应当回来,实际 %v", got)
|
||
}
|
||
reasons := changeReasons(rec.all())[before:]
|
||
if len(reasons) != 2 || reasons[0] != PanelChangeDisabled || reasons[1] != PanelChangeEnabled {
|
||
t.Fatalf("启停变更原因 = %v,期望 [disable enable]", reasons)
|
||
}
|
||
|
||
// 未安装的名字必须显式报「找不到」,绝不静默成功。
|
||
if err := mgr.SetEnabled(context.Background(), "ghost", false); err == nil {
|
||
t.Fatalf("停用一个不存在的插件应当报错")
|
||
}
|
||
}
|
||
|
||
// TestManagerPanelsUnwiredIsNil 锁定未接线时的诚实语义:Panels() 回 nil,
|
||
// 管理器对面板的一切调用都是安全空操作(既有行为逐字不变)。
|
||
func TestManagerPanelsUnwiredIsNil(t *testing.T) {
|
||
t.Parallel()
|
||
mcp := newFakeMCP(t, "", "add")
|
||
mgr := NewManager(registry.New(), filepath.Join(t.TempDir(), "plugins.json"), nil)
|
||
defer mgr.Close(context.Background())
|
||
if mgr.Panels() != nil {
|
||
t.Fatalf("未接线时 Panels() 应当回 nil")
|
||
}
|
||
installFake(t, mgr, "calc", mcp.URL)
|
||
if _, err := mgr.Uninstall(context.Background(), "calc"); err != nil {
|
||
t.Fatalf("未接线时卸载不应受影响: %v", err)
|
||
}
|
||
}
|
||
|
||
// changeReasons 抽取变更原因序列(顺序敏感)。
|
||
func changeReasons(changes []PanelChange) []string {
|
||
out := make([]string, 0, len(changes))
|
||
for _, c := range changes {
|
||
out = append(out, c.Reason)
|
||
}
|
||
return out
|
||
}
|