claudeup 4.10.2 → 4.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claudeup",
3
- "version": "4.10.2",
3
+ "version": "4.11.1",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Tests for plugin-setup.ts: go package manager and required binary validator.
3
+ *
4
+ * Tests extractGoBinaryName (pure function), and uses module mocking to
5
+ * verify installPluginDeps() and checkMissingDeps() behavior for `go`
6
+ * and `required` setup keys.
7
+ */
8
+
9
+ import { describe, it, expect, mock, beforeEach } from "bun:test";
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Mocks — must be set up BEFORE any import from plugin-setup
13
+ // ---------------------------------------------------------------------------
14
+
15
+ // Track which commands are "available" and what execFile calls return
16
+ let availableBinaries: Set<string>;
17
+ let execResults: Map<
18
+ string,
19
+ { stdout: string; stderr: string; error?: boolean }
20
+ >;
21
+
22
+ // Mock command-utils BEFORE importing plugin-setup
23
+ mock.module("../utils/command-utils.js", () => ({
24
+ which: async (cmd: string) => {
25
+ return availableBinaries.has(cmd) ? `/usr/bin/${cmd}` : null;
26
+ },
27
+ }));
28
+
29
+ // Mock child_process execFile — promisify wraps this callback-based API
30
+ mock.module("node:child_process", () => ({
31
+ execFile: (
32
+ cmd: string,
33
+ args: string[],
34
+ opts: unknown,
35
+ cb?: (
36
+ err: Error | null,
37
+ result: { stdout: string; stderr: string },
38
+ ) => void,
39
+ ) => {
40
+ const key = `${cmd} ${args.join(" ")}`;
41
+
42
+ // Find matching result by prefix
43
+ let result = execResults.get(key);
44
+ if (!result) {
45
+ for (const [k, v] of execResults.entries()) {
46
+ if (key.startsWith(k)) {
47
+ result = v;
48
+ break;
49
+ }
50
+ }
51
+ }
52
+
53
+ if (!result) {
54
+ result = { stdout: "", stderr: "", error: false };
55
+ }
56
+
57
+ if (typeof cb === "function") {
58
+ if (result.error) {
59
+ const err = new Error(result.stderr) as Error & {
60
+ stdout: string;
61
+ stderr: string;
62
+ };
63
+ err.stdout = result.stdout;
64
+ err.stderr = result.stderr;
65
+ cb(err, { stdout: result.stdout, stderr: result.stderr });
66
+ } else {
67
+ cb(null, { stdout: result.stdout, stderr: result.stderr });
68
+ }
69
+ }
70
+ },
71
+ }));
72
+
73
+ // Import AFTER mocks are set up
74
+ const {
75
+ extractGoBinaryName,
76
+ installPluginDeps,
77
+ checkMissingDeps,
78
+ } = await import("../services/plugin-setup.js");
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // 1. Pure function tests — extractGoBinaryName
82
+ // ---------------------------------------------------------------------------
83
+
84
+ describe("extractGoBinaryName", () => {
85
+ it("extracts binary name from module@version", () => {
86
+ expect(extractGoBinaryName("github.com/MadAppGang/tmux-mcp@latest")).toBe(
87
+ "tmux-mcp",
88
+ );
89
+ });
90
+
91
+ it("extracts binary name from module@semver", () => {
92
+ expect(extractGoBinaryName("github.com/user/tool@v1.2.3")).toBe("tool");
93
+ });
94
+
95
+ it("extracts binary name from module without version", () => {
96
+ expect(extractGoBinaryName("github.com/user/tool")).toBe("tool");
97
+ });
98
+
99
+ it("handles single-segment package name", () => {
100
+ expect(extractGoBinaryName("mytool")).toBe("mytool");
101
+ });
102
+
103
+ it("handles module with nested path and version", () => {
104
+ expect(
105
+ extractGoBinaryName("github.com/org/repo/cmd/mycli@v2.0.0"),
106
+ ).toBe("mycli");
107
+ });
108
+
109
+ it("handles empty string gracefully", () => {
110
+ expect(extractGoBinaryName("")).toBe("");
111
+ });
112
+ });
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // 2. Integration tests — installPluginDeps with go packages
116
+ // ---------------------------------------------------------------------------
117
+
118
+ describe("installPluginDeps — go packages", () => {
119
+ beforeEach(() => {
120
+ availableBinaries = new Set(["go"]); // go is available by default
121
+ execResults = new Map();
122
+ });
123
+
124
+ it("skips go packages when binary already exists", async () => {
125
+ availableBinaries.add("tmux-mcp");
126
+
127
+ const result = await installPluginDeps({
128
+ go: ["github.com/MadAppGang/tmux-mcp@latest"],
129
+ });
130
+
131
+ expect(result.skipped).toContain(
132
+ "go:github.com/MadAppGang/tmux-mcp@latest",
133
+ );
134
+ expect(result.installed).toHaveLength(0);
135
+ expect(result.failed).toHaveLength(0);
136
+ });
137
+
138
+ it("installs go packages when binary not found", async () => {
139
+ // go is available but tmux-mcp is not
140
+ execResults.set("/usr/bin/go install", {
141
+ stdout: "",
142
+ stderr: "",
143
+ });
144
+
145
+ const result = await installPluginDeps({
146
+ go: ["github.com/MadAppGang/tmux-mcp@latest"],
147
+ });
148
+
149
+ expect(result.installed).toContain(
150
+ "go:github.com/MadAppGang/tmux-mcp@latest",
151
+ );
152
+ expect(result.failed).toHaveLength(0);
153
+ });
154
+
155
+ it("reports failure when go is not available", async () => {
156
+ availableBinaries.delete("go");
157
+
158
+ const result = await installPluginDeps({
159
+ go: ["github.com/MadAppGang/tmux-mcp@latest"],
160
+ });
161
+
162
+ expect(result.failed).toHaveLength(1);
163
+ expect(result.failed[0].pkg).toBe(
164
+ "go:github.com/MadAppGang/tmux-mcp@latest",
165
+ );
166
+ expect(result.failed[0].error).toBe("go not found");
167
+ });
168
+
169
+ it("reports failure when go install fails", async () => {
170
+ execResults.set("/usr/bin/go install", {
171
+ stdout: "",
172
+ stderr: "build error: something went wrong",
173
+ error: true,
174
+ });
175
+
176
+ const result = await installPluginDeps({
177
+ go: ["github.com/MadAppGang/tmux-mcp@latest"],
178
+ });
179
+
180
+ expect(result.failed).toHaveLength(1);
181
+ expect(result.failed[0].error).toContain("something went wrong");
182
+ });
183
+ });
184
+
185
+ // ---------------------------------------------------------------------------
186
+ // 3. Integration tests — installPluginDeps with required binaries
187
+ // ---------------------------------------------------------------------------
188
+
189
+ describe("installPluginDeps — required binaries", () => {
190
+ beforeEach(() => {
191
+ availableBinaries = new Set();
192
+ execResults = new Map();
193
+ });
194
+
195
+ it("skips required binaries that are present", async () => {
196
+ availableBinaries.add("tmux");
197
+
198
+ const result = await installPluginDeps({
199
+ required: ["tmux"],
200
+ });
201
+
202
+ expect(result.skipped).toContain("required:tmux");
203
+ expect(result.failed).toHaveLength(0);
204
+ });
205
+
206
+ it("reports failure for missing required binaries", async () => {
207
+ const result = await installPluginDeps({
208
+ required: ["tmux"],
209
+ });
210
+
211
+ expect(result.failed).toHaveLength(1);
212
+ expect(result.failed[0].pkg).toBe("required:tmux");
213
+ expect(result.failed[0].error).toContain(
214
+ "Required binary 'tmux' not found in PATH",
215
+ );
216
+ expect(result.failed[0].error).toContain("install it manually");
217
+ });
218
+
219
+ it("handles multiple required binaries with mixed availability", async () => {
220
+ availableBinaries.add("tmux");
221
+ // ffmpeg is not available
222
+
223
+ const result = await installPluginDeps({
224
+ required: ["tmux", "ffmpeg"],
225
+ });
226
+
227
+ expect(result.skipped).toContain("required:tmux");
228
+ expect(result.failed).toHaveLength(1);
229
+ expect(result.failed[0].pkg).toBe("required:ffmpeg");
230
+ });
231
+ });
232
+
233
+ // ---------------------------------------------------------------------------
234
+ // 4. checkMissingDeps tests — go packages
235
+ // ---------------------------------------------------------------------------
236
+
237
+ describe("checkMissingDeps — go packages", () => {
238
+ beforeEach(() => {
239
+ availableBinaries = new Set();
240
+ execResults = new Map();
241
+ });
242
+
243
+ it("detects missing go packages when binary not in PATH", async () => {
244
+ const missing = await checkMissingDeps({
245
+ go: ["github.com/MadAppGang/tmux-mcp@latest"],
246
+ });
247
+
248
+ expect(missing.go).toEqual(["github.com/MadAppGang/tmux-mcp@latest"]);
249
+ });
250
+
251
+ it("does not report go packages when binary exists", async () => {
252
+ availableBinaries.add("tmux-mcp");
253
+
254
+ const missing = await checkMissingDeps({
255
+ go: ["github.com/MadAppGang/tmux-mcp@latest"],
256
+ });
257
+
258
+ expect(missing.go).toBeUndefined();
259
+ });
260
+ });
261
+
262
+ // ---------------------------------------------------------------------------
263
+ // 5. checkMissingDeps tests — required binaries
264
+ // ---------------------------------------------------------------------------
265
+
266
+ describe("checkMissingDeps — required binaries", () => {
267
+ beforeEach(() => {
268
+ availableBinaries = new Set();
269
+ execResults = new Map();
270
+ });
271
+
272
+ it("detects missing required binaries", async () => {
273
+ const missing = await checkMissingDeps({
274
+ required: ["tmux", "ffmpeg"],
275
+ });
276
+
277
+ expect(missing.required).toEqual(["tmux", "ffmpeg"]);
278
+ });
279
+
280
+ it("does not report present required binaries", async () => {
281
+ availableBinaries.add("tmux");
282
+ availableBinaries.add("ffmpeg");
283
+
284
+ const missing = await checkMissingDeps({
285
+ required: ["tmux", "ffmpeg"],
286
+ });
287
+
288
+ expect(missing.required).toBeUndefined();
289
+ });
290
+
291
+ it("reports only missing required binaries", async () => {
292
+ availableBinaries.add("tmux");
293
+
294
+ const missing = await checkMissingDeps({
295
+ required: ["tmux", "ffmpeg"],
296
+ });
297
+
298
+ expect(missing.required).toEqual(["ffmpeg"]);
299
+ });
300
+ });