返回 DeepSeek-Reasonix
plugin_packages_test.go
根目录 / internal / config / plugin_packages_test.go
1 package config
2
3 import (
4 "os"
5 "path/filepath"
6 "testing"
7
8 "reasonix/internal/pluginpkg"
9 )
10
11 func TestLoadMergesInstalledPluginSkillRootsAndMCP(t *testing.T) {
12 home := t.TempDir()
13 t.Setenv("REASONIX_HOME", home)
14 root := filepath.Join(home, "plugins", "superpowers")
15 writeConfigTestFile(t, filepath.Join(root, pluginpkg.NativeManifest), `{
16 "apiVersion": "reasonix.io/plugin/v2",
17 "name": "superpowers",
18 "version": "1.0.0",
19 "skills": "skills",
20 "mcpServers": {
21 "helper": { "command": "bin/helper" }
22 }
23 }`)
24 if err := pluginpkg.Upsert(home, pluginpkg.InstalledPlugin{
25 Name: "superpowers",
26 Root: "plugins/superpowers",
27 Version: "1.0.0",
28 ManifestKind: "reasonix",
29 Enabled: true,
30 }); err != nil {
31 t.Fatal(err)
32 }
33
34 cfg, err := LoadForRoot(t.TempDir())
35 if err != nil {
36 t.Fatal(err)
37 }
38 if len(cfg.Skills.Paths) == 0 || cfg.Skills.Paths[len(cfg.Skills.Paths)-1] != filepath.Join(root, "skills") {
39 t.Fatalf("skills paths = %#v", cfg.Skills.Paths)
40 }
41 owners := cfg.PluginPackageSkillOwners()[CanonicalSkillPath(filepath.Join(root, "skills"))]
42 if len(owners) != 1 || owners[0] != "superpowers" {
43 t.Fatalf("plugin skill owners = %#v, want superpowers", owners)
44 }
45 var found bool
46 for _, p := range cfg.Plugins {
47 if p.Name == "helper" {
48 found = true
49 if p.Command != filepath.Join(root, "bin", "helper") {
50 t.Fatalf("plugin command = %q", p.Command)
51 }
52 if p.Env["REASONIX_PLUGIN_NAME"] != "superpowers" {
53 t.Fatalf("plugin env = %#v", p.Env)
54 }
55 }
56 }
57 if !found {
58 t.Fatalf("plugin MCP server missing: %#v", cfg.Plugins)
59 }
60 if owner, ok := cfg.PluginPackageOwner("helper"); !ok || owner != "superpowers" {
61 t.Fatalf("plugin MCP owner = %q, %v; want superpowers, true", owner, ok)
62 }
63 }
64
65 func TestClaudePackageMCPExpandsRootAndDoesNotAutoStart(t *testing.T) {
66 home := t.TempDir()
67 t.Setenv("REASONIX_HOME", home)
68 root := filepath.Join(home, "plugins", "claude-mcp")
69 writeConfigTestFile(t, filepath.Join(root, pluginpkg.ClaudeManifest), `{"name":"claude-mcp"}`)
70 writeConfigTestFile(t, filepath.Join(root, ".mcp.json"), `{
71 "mcpServers": {
72 "Local Search": {
73 "command": "${CLAUDE_PLUGIN_ROOT}/bin/server",
74 "args": ["--root", "${CLAUDE_PLUGIN_ROOT}/data", "--workspace", "${CLAUDE_PROJECT_DIR}"],
75 "env": {"DATA_DIR": "${CLAUDE_PLUGIN_ROOT}/data"}
76 }
77 }
78 }`)
79 if err := pluginpkg.Upsert(home, pluginpkg.InstalledPlugin{Name: "claude-mcp", Root: "plugins/claude-mcp", ManifestKind: "claude", Enabled: true}); err != nil {
80 t.Fatal(err)
81 }
82 workspace := t.TempDir()
83 cfg, err := LoadForRoot(workspace)
84 if err != nil {
85 t.Fatal(err)
86 }
87 if len(cfg.Plugins) != 1 {
88 t.Fatalf("plugins = %#v", cfg.Plugins)
89 }
90 got := cfg.Plugins[0]
91 if got.Command != filepath.Join(root, "bin", "server") || got.Args[1] != filepath.Join(root, "data") || got.Env["DATA_DIR"] != filepath.Join(root, "data") {
92 t.Fatalf("Claude root was not expanded: %#v", got)
93 }
94 if got.Env["CLAUDE_PLUGIN_ROOT"] != root {
95 t.Fatalf("CLAUDE_PLUGIN_ROOT = %q", got.Env["CLAUDE_PLUGIN_ROOT"])
96 }
97 if got.Args[3] != workspace || got.Env["CLAUDE_PROJECT_DIR"] != workspace {
98 t.Fatalf("workspace expansion = %#v", got)
99 }
100 if got.ShouldAutoStart() {
101 t.Fatal("imported Claude MCP must require an explicit connection")
102 }
103 if len(cfg.AutoStartPlugins()) != 0 {
104 t.Fatalf("auto-start plugins = %#v", cfg.AutoStartPlugins())
105 }
106 }
107
108 func TestClaudePackageMCPDeduplicatesSameConnectionAcrossPackages(t *testing.T) {
109 home := t.TempDir()
110 t.Setenv("REASONIX_HOME", home)
111 for i, name := range []string{"legal-one", "legal-two"} {
112 root := filepath.Join(home, "plugins", name)
113 writeConfigTestFile(t, filepath.Join(root, pluginpkg.ClaudeManifest), `{"name":"`+name+`"}`)
114 writeConfigTestFile(t, filepath.Join(root, ".mcp.json"), `{
115 "mcpServers":{"飞书":{"type":"http","url":"https://open.feishu.cn/mcp","description":"package `+string(rune('A'+i))+` description"}}
116 }`)
117 if err := pluginpkg.Upsert(home, pluginpkg.InstalledPlugin{Name: name, Root: "plugins/" + name, ManifestKind: "claude", Enabled: true}); err != nil {
118 t.Fatal(err)
119 }
120 }
121 cfg, err := LoadForRoot(t.TempDir())
122 if err != nil {
123 t.Fatal(err)
124 }
125 if len(cfg.Plugins) != 1 || cfg.Plugins[0].URL != "https://open.feishu.cn/mcp" {
126 t.Fatalf("deduplicated plugins = %#v", cfg.Plugins)
127 }
128 }
129
130 func writeConfigTestFile(t *testing.T, path, body string) {
131 t.Helper()
132 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
133 t.Fatal(err)
134 }
135 if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
136 t.Fatal(err)
137 }
138 }
139
140 // TestCommandDirsIncludePluginPackageCommands pins the plugin-commands wiring:
141 // an enabled plugin package's command roots join command discovery at the
142 // before user/project entries, retaining package ownership, while a disabled
143 // package contributes nothing.
144 func TestCommandDirsIncludePluginPackageCommands(t *testing.T) {
145 home := t.TempDir()
146 t.Setenv("REASONIX_HOME", home)
147 root := filepath.Join(home, "plugins", "pwf")
148 writeConfigTestFile(t, filepath.Join(root, pluginpkg.ClaudeManifest), `{"name": "pwf"}`)
149 writeConfigTestFile(t, filepath.Join(root, "skills", "planner", "SKILL.md"), "---\ndescription: p\n---\nbody")
150 writeConfigTestFile(t, filepath.Join(root, "commands", "plan.md"), "---\ndescription: plan\n---\nPlan: $ARGUMENTS")
151 if err := pluginpkg.Upsert(home, pluginpkg.InstalledPlugin{
152 Name: "pwf",
153 Root: "plugins/pwf",
154 ManifestKind: "claude",
155 Enabled: true,
156 }); err != nil {
157 t.Fatal(err)
158 }
159
160 dirs := CommandDirsForRoot(t.TempDir())
161 want := filepath.Join(root, "commands")
162 if len(dirs) == 0 || dirs[0] != want {
163 t.Fatalf("CommandDirsForRoot = %#v, want plugin commands dir first (lowest priority): %s", dirs, want)
164 }
165 roots := CommandRootsForRoot(t.TempDir())
166 if len(roots) == 0 || roots[0].Path != want || roots[0].Plugin != "pwf" {
167 t.Fatalf("CommandRootsForRoot = %#v, want plugin ownership on first root", roots)
168 }
169
170 if err := pluginpkg.SetEnabled(home, "pwf", false); err != nil {
171 t.Fatal(err)
172 }
173 for _, dir := range CommandDirsForRoot(t.TempDir()) {
174 if dir == want {
175 t.Fatalf("disabled plugin's commands dir must not join discovery: %#v", dir)
176 }
177 }
178 }
179
180 // TestCommandDirsWithoutPluginState keeps the no-plugin fast path intact.
181 func TestCommandDirsWithoutPluginState(t *testing.T) {
182 home := t.TempDir()
183 t.Setenv("REASONIX_HOME", home)
184 if dirs := CommandDirsForRoot(t.TempDir()); len(dirs) == 0 {
185 t.Fatal("CommandDirsForRoot must still return the conventional dirs")
186 }
187 }
188
188 lines GO