package main import ( "context" "encoding/json" "io" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/capability/registry" "git.yeij.top/AskaEth/Cyrene_v2/internal/config" "git.yeij.top/AskaEth/Cyrene_v2/internal/config/runtimeconfig" ) // pluginFakeMCP is a minimal Streamable HTTP MCP server for the composition-root // test: it answers initialize and tools/list over POST. type pluginFakeMCP struct{ tools []string } func (f pluginFakeMCP) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { w.Header().Set("Content-Type", "text/event-stream") w.WriteHeader(http.StatusOK) if fl, ok := w.(http.Flusher); ok { fl.Flush() } <-r.Context().Done() return } body, _ := io.ReadAll(r.Body) var req struct { ID json.RawMessage `json:"id"` Method string `json:"method"` } _ = json.Unmarshal(body, &req) if len(req.ID) == 0 || string(req.ID) == "null" { w.WriteHeader(http.StatusAccepted) return } var result any switch req.Method { case "initialize": result = map[string]any{"protocolVersion": "2025-11-25", "capabilities": map[string]any{}, "serverInfo": map[string]any{"name": "fake", "version": "1.0"}} case "tools/list": list := make([]map[string]any, 0, len(f.tools)) for _, name := range f.tools { list = append(list, map[string]any{"name": name, "description": name, "inputSchema": map[string]any{"type": "object"}}) } result = map[string]any{"tools": list} default: result = map[string]any{} } resp, _ := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": json.RawMessage(req.ID), "result": result}) w.Header().Set("Content-Type", "application/json") _, _ = w.Write(resp) } // TestWirePlugins_RestoresPersistedServer proves the composition root builds the // plugin manager over the resolved data dir (plugins.json) and reinstalls a // persisted MCP server at boot, so a panel install survives a restart. func TestWirePlugins_RestoresPersistedServer(t *testing.T) { dir := t.TempDir() mcpSrv := httptest.NewServer(pluginFakeMCP{tools: []string{"add"}}) defer mcpSrv.Close() plugins := []runtimeconfig.PluginConfig{{ ID: "p1", Name: "calc", Kind: runtimeconfig.PluginKindMCP, Transport: runtimeconfig.TransportHTTP, Endpoint: mcpSrv.URL, CreatedAt: "2026-09-08T00:00:00Z", }} raw, err := json.Marshal(plugins) if err != nil { t.Fatalf("marshal: %v", err) } if err := os.WriteFile(filepath.Join(dir, "plugins.json"), raw, 0o600); err != nil { t.Fatalf("write plugins.json: %v", err) } caps := registry.New() a := &app{caps: caps, cfg: &config.Config{DataDir: dir}, log: testLog()} a.wirePlugins(context.Background()) if a.plugins == nil { t.Fatal("plugins manager not wired") } defer a.plugins.Close(context.Background()) if _, err := caps.Get("add"); err != nil { t.Fatalf("restored tool add not registered: %v", err) } if _, err := caps.Get("mcp:calc"); err != nil { t.Fatalf("mcp:calc capability not registered: %v", err) } }