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 }