Complete the remaining M8 extensibility deliverables:
- MCP client with JSON-RPC 2.0 over stdio transport, protocol
lifecycle (initialize/tools-list/tools-call), and process group
management for clean shutdown
- MCP tool adapter implementing tool.Tool with mcp__{server}__{tool}
naming convention and replace_default for swapping built-in tools
- MCP manager for multi-server orchestration with parallel startup,
tool discovery, and registry integration
- Plugin system with plugin.json manifest (name/version/capabilities),
directory-based discovery (global + project scopes with precedence),
loader that merges skills/hooks/MCP configs into existing registries,
and install/uninstall/list lifecycle manager
- Config additions: MCPServerConfig, PluginsSection with opt-in/opt-out
enabled/disabled resolution
- TUI /plugins command for listing installed plugins
- 54 tests across internal/mcp and internal/plugin packages
161 lines
4.3 KiB
Go
161 lines
4.3 KiB
Go
package plugin
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestManager_Install(t *testing.T) {
|
|
dir := t.TempDir()
|
|
globalDir := filepath.Join(dir, "global")
|
|
projectDir := filepath.Join(dir, "project")
|
|
os.MkdirAll(globalDir, 0o755)
|
|
os.MkdirAll(projectDir, 0o755)
|
|
|
|
// Create a source plugin directory.
|
|
srcDir := filepath.Join(dir, "src", "my-plugin")
|
|
os.MkdirAll(srcDir, 0o755)
|
|
m := Manifest{Name: "my-plugin", Version: "1.0.0", Description: "Test plugin"}
|
|
data, _ := marshalJSON(m)
|
|
os.WriteFile(filepath.Join(srcDir, "plugin.json"), data, 0o644)
|
|
|
|
mgr := NewManager(globalDir, projectDir, testLogger())
|
|
|
|
// Install to user scope.
|
|
if err := mgr.Install(srcDir, "user"); err != nil {
|
|
t.Fatalf("Install: %v", err)
|
|
}
|
|
|
|
// Verify the plugin was copied.
|
|
installed := filepath.Join(globalDir, "my-plugin", "plugin.json")
|
|
if _, err := os.Stat(installed); err != nil {
|
|
t.Errorf("installed manifest not found: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestManager_Install_ProjectScope(t *testing.T) {
|
|
dir := t.TempDir()
|
|
globalDir := filepath.Join(dir, "global")
|
|
projectDir := filepath.Join(dir, "project")
|
|
os.MkdirAll(globalDir, 0o755)
|
|
os.MkdirAll(projectDir, 0o755)
|
|
|
|
srcDir := filepath.Join(dir, "src", "proj-plugin")
|
|
os.MkdirAll(srcDir, 0o755)
|
|
m := Manifest{Name: "proj-plugin", Version: "1.0.0"}
|
|
data, _ := marshalJSON(m)
|
|
os.WriteFile(filepath.Join(srcDir, "plugin.json"), data, 0o644)
|
|
|
|
mgr := NewManager(globalDir, projectDir, testLogger())
|
|
|
|
if err := mgr.Install(srcDir, "project"); err != nil {
|
|
t.Fatalf("Install: %v", err)
|
|
}
|
|
|
|
installed := filepath.Join(projectDir, "proj-plugin", "plugin.json")
|
|
if _, err := os.Stat(installed); err != nil {
|
|
t.Errorf("installed manifest not found: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestManager_Install_AlreadyInstalled(t *testing.T) {
|
|
dir := t.TempDir()
|
|
globalDir := filepath.Join(dir, "global")
|
|
os.MkdirAll(globalDir, 0o755)
|
|
|
|
srcDir := filepath.Join(dir, "src", "dup")
|
|
os.MkdirAll(srcDir, 0o755)
|
|
m := Manifest{Name: "dup", Version: "1.0.0"}
|
|
data, _ := marshalJSON(m)
|
|
os.WriteFile(filepath.Join(srcDir, "plugin.json"), data, 0o644)
|
|
|
|
mgr := NewManager(globalDir, filepath.Join(dir, "project"), testLogger())
|
|
|
|
// First install.
|
|
mgr.Install(srcDir, "user")
|
|
|
|
// Second install should fail.
|
|
err := mgr.Install(srcDir, "user")
|
|
if err == nil {
|
|
t.Error("expected ErrAlreadyInstalled")
|
|
}
|
|
}
|
|
|
|
func TestManager_Install_NoManifest(t *testing.T) {
|
|
dir := t.TempDir()
|
|
globalDir := filepath.Join(dir, "global")
|
|
os.MkdirAll(globalDir, 0o755)
|
|
|
|
srcDir := filepath.Join(dir, "src", "empty")
|
|
os.MkdirAll(srcDir, 0o755)
|
|
|
|
mgr := NewManager(globalDir, filepath.Join(dir, "project"), testLogger())
|
|
err := mgr.Install(srcDir, "user")
|
|
if err == nil {
|
|
t.Error("expected error for missing manifest")
|
|
}
|
|
}
|
|
|
|
func TestManager_Uninstall(t *testing.T) {
|
|
dir := t.TempDir()
|
|
globalDir := filepath.Join(dir, "global")
|
|
os.MkdirAll(globalDir, 0o755)
|
|
|
|
// Pre-install a plugin.
|
|
pluginDir := filepath.Join(globalDir, "to-remove")
|
|
os.MkdirAll(pluginDir, 0o755)
|
|
m := Manifest{Name: "to-remove", Version: "1.0.0"}
|
|
data, _ := marshalJSON(m)
|
|
os.WriteFile(filepath.Join(pluginDir, "plugin.json"), data, 0o644)
|
|
|
|
mgr := NewManager(globalDir, filepath.Join(dir, "project"), testLogger())
|
|
|
|
if err := mgr.Uninstall("to-remove", "user"); err != nil {
|
|
t.Fatalf("Uninstall: %v", err)
|
|
}
|
|
|
|
if _, err := os.Stat(pluginDir); !os.IsNotExist(err) {
|
|
t.Error("plugin directory should be removed")
|
|
}
|
|
}
|
|
|
|
func TestManager_Uninstall_NotFound(t *testing.T) {
|
|
dir := t.TempDir()
|
|
mgr := NewManager(filepath.Join(dir, "global"), filepath.Join(dir, "project"), testLogger())
|
|
|
|
err := mgr.Uninstall("nonexistent", "user")
|
|
if err == nil {
|
|
t.Error("expected ErrNotFound")
|
|
}
|
|
}
|
|
|
|
func TestManager_List(t *testing.T) {
|
|
dir := t.TempDir()
|
|
globalDir := filepath.Join(dir, "global")
|
|
projectDir := filepath.Join(dir, "project")
|
|
|
|
writePlugin(t, globalDir, "global-plugin", "1.0.0", nil)
|
|
writePlugin(t, projectDir, "project-plugin", "2.0.0", nil)
|
|
|
|
mgr := NewManager(globalDir, projectDir, testLogger())
|
|
|
|
infos, err := mgr.List()
|
|
if err != nil {
|
|
t.Fatalf("List: %v", err)
|
|
}
|
|
|
|
if len(infos) != 2 {
|
|
t.Fatalf("expected 2 plugins, got %d", len(infos))
|
|
}
|
|
|
|
// Check that both scopes are represented.
|
|
scopes := map[string]bool{}
|
|
for _, info := range infos {
|
|
scopes[info.Scope] = true
|
|
}
|
|
if !scopes["user"] || !scopes["project"] {
|
|
t.Errorf("expected both scopes, got %v", scopes)
|
|
}
|
|
}
|