Files
WeKnora/internal/handler/plugin_test.go
T
lyingbug ed63e35cc7 feat(plugin): install declarative plugins — MCP servers, skills and model vendors (#3716)
* feat(plugin): install declarative plugin packages across nodes
* feat(plugin): serve installed plugins' MCP servers in each workspace
* feat(plugin): let installed plugins contribute skills and model vendors
* feat(plugin): register plugin skills into the workspace skill catalog
* feat(frontend): manage installed plugins and their configuration
* test(weaviate): read the class state before the probe barrier
* fix(plugin): keep MCP tool directories reachable behind the plugin wrapper
* test(plugin): wrap a long line
2026-09-26 14:44:34 +08:00

178 lines
6.4 KiB
Go

package handler
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/Tencent/WeKnora/internal/middleware"
"github.com/Tencent/WeKnora/internal/plugin/driver"
"github.com/Tencent/WeKnora/internal/plugin/manifest"
"github.com/Tencent/WeKnora/internal/plugin/plugintest"
"github.com/Tencent/WeKnora/internal/plugin/registry"
"github.com/Tencent/WeKnora/internal/plugin/tenancy"
"github.com/Tencent/WeKnora/internal/types"
)
func newPluginHandlerTestRouter(t *testing.T) *gin.Engine {
t.Helper()
reg := registry.New()
err := reg.Register(&manifest.Manifest{
SchemaVersion: manifest.SchemaVersion,
ID: "weknora.feishu",
Version: "1.0.0",
Name: manifest.Text("Feishu", map[string]string{"zh-CN": "飞书"}),
Publisher: manifest.Publisher{ID: manifest.BuiltinPublisher},
Builtin: true,
Runtime: manifest.Runtime{Type: manifest.RuntimeBuiltin},
Contributes: manifest.Contributions{
manifest.PointConnectors: {{ID: "feishu", Name: manifest.Text("Feishu", nil), Aliases: []string{"feishu"}}},
manifest.PointIMChannels: {{ID: "feishu", Name: manifest.Text("Feishu", nil), Aliases: []string{"feishu"}}},
},
})
if err != nil {
t.Fatal(err)
}
gin.SetMode(gin.TestMode)
r := gin.New()
r.Use(middleware.ErrorHandler())
h := NewPluginHandler(reg, nil, driver.NewSet(driver.NewBuiltin(reg.Plugin)))
r.GET("/plugins", h.ListPlugins)
r.GET("/plugins/contributions", h.ListContributions)
r.GET("/plugins/:id", h.GetPlugin)
return r
}
func getJSON(t *testing.T, r *gin.Engine, path string, out any) int {
t.Helper()
w := httptest.NewRecorder()
r.ServeHTTP(w, httptest.NewRequest(http.MethodGet, path, nil))
if out != nil && w.Code == http.StatusOK {
if err := json.Unmarshal(w.Body.Bytes(), out); err != nil {
t.Fatalf("decode %s: %v (%s)", path, err, w.Body.String())
}
}
return w.Code
}
func TestPluginHandlerListsPluginsAndContributions(t *testing.T) {
r := newPluginHandlerTestRouter(t)
var list struct {
Data []struct {
Manifest manifest.Manifest `json:"manifest"`
Enabled bool `json:"enabled"`
} `json:"data"`
}
if code := getJSON(t, r, "/plugins", &list); code != http.StatusOK {
t.Fatalf("list status = %d", code)
}
if len(list.Data) != 1 || list.Data[0].Manifest.Name.Resolve("zh-CN") != "飞书" || !list.Data[0].Enabled {
t.Fatalf("unexpected plugins: %+v", list.Data)
}
var contribs struct {
Data struct {
Points []manifest.PointInfo `json:"points"`
Contributions map[string][]map[string]any `json:"contributions"`
} `json:"data"`
}
if code := getJSON(t, r, "/plugins/contributions?point=connectors", &contribs); code != http.StatusOK {
t.Fatalf("contributions status = %d", code)
}
if len(contribs.Data.Points) != 1 || len(contribs.Data.Contributions) != 1 {
t.Fatalf("point filter not applied: %+v", contribs.Data)
}
got := contribs.Data.Contributions["connectors"]
if len(got) != 1 || got[0]["qualifiedId"] != "weknora.feishu/feishu" || got[0]["pluginId"] != "weknora.feishu" {
t.Fatalf("unexpected connectors: %+v", got)
}
}
func TestPluginHandlerErrors(t *testing.T) {
r := newPluginHandlerTestRouter(t)
if code := getJSON(t, r, "/plugins/acme.missing", nil); code != http.StatusNotFound {
t.Fatalf("missing plugin status = %d, want 404", code)
}
if code := getJSON(t, r, "/plugins/contributions?point=widgets", nil); code != http.StatusBadRequest {
t.Fatalf("unknown point status = %d, want 400", code)
}
var detail struct {
Data struct {
Enabled bool `json:"enabled"`
Instances []driver.InstanceStatus `json:"instances"`
} `json:"data"`
}
if code := getJSON(t, r, "/plugins/weknora.feishu", &detail); code != http.StatusOK {
t.Fatalf("get plugin status = %d", code)
}
if !detail.Data.Enabled || len(detail.Data.Instances) != 1 || detail.Data.Instances[0].State != driver.StateReady {
t.Fatalf("builtin plugin detail = %+v", detail.Data)
}
}
func TestPluginHandlerTogglesPluginsPerTenant(t *testing.T) {
reg := registry.New()
builtinPlugin := func(id string, required bool, point manifest.Point, localID string) *manifest.Manifest {
return &manifest.Manifest{
SchemaVersion: manifest.SchemaVersion, ID: id, Version: "1.0.0", Name: manifest.Text(id, nil),
Publisher: manifest.Publisher{ID: manifest.BuiltinPublisher}, Builtin: true, Required: required,
Runtime: manifest.Runtime{Type: manifest.RuntimeBuiltin},
Contributes: manifest.Contributions{
point: {{ID: localID, Name: manifest.Text(localID, nil)}},
},
}
}
for _, m := range []*manifest.Manifest{
builtinPlugin("weknora.notion", false, manifest.PointConnectors, "notion"),
builtinPlugin("weknora.agent-tools", true, manifest.PointTools, "thinking"),
} {
if err := reg.Register(m); err != nil {
t.Fatal(err)
}
}
settings := &plugintest.MemTenantSettings{}
h := NewPluginHandler(reg, tenancy.NewService(reg, settings), nil)
gin.SetMode(gin.TestMode)
r := gin.New()
r.Use(middleware.ErrorHandler(), func(c *gin.Context) { c.Set(types.TenantIDContextKey.String(), uint64(7)) })
r.PUT("/plugins/:id/enabled", h.SetPluginEnabled)
r.GET("/plugins/contributions", h.ListContributions)
put := func(id, body string) int {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPut, "/plugins/"+id+"/enabled", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
return w.Code
}
if code := put("weknora.notion", `{"enabled":false}`); code != http.StatusOK {
t.Fatalf("disable status = %d", code)
}
if code := put("weknora.agent-tools", `{"enabled":false}`); code != http.StatusBadRequest {
t.Fatalf("disabling a required plugin = %d, want 400", code)
}
if code := put("acme.missing", `{"enabled":false}`); code != http.StatusNotFound {
t.Fatalf("unknown plugin = %d, want 404", code)
}
if code := put("weknora.notion", `{}`); code != http.StatusBadRequest {
t.Fatalf("missing enabled = %d, want 400", code)
}
var contribs struct {
Data struct {
Contributions map[string][]map[string]any `json:"contributions"`
} `json:"data"`
}
if code := getJSON(t, r, "/plugins/contributions?point=connectors", &contribs); code != http.StatusOK {
t.Fatalf("contributions status = %d", code)
}
if got := contribs.Data.Contributions["connectors"]; len(got) != 1 || got[0]["enabled"] != false {
t.Fatalf("disabled plugin's contribution should read disabled: %v", got)
}
}