magus-cli 7.10.1 → 7.10.3

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": "magus-cli",
3
- "version": "7.10.1",
3
+ "version": "7.10.3",
4
4
  "description": "TUI tool for managing Claude Code plugins, MCPs, and configuration",
5
5
  "type": "module",
6
6
  "main": "src/main.tsx",
@@ -65,8 +65,8 @@
65
65
  "typescript": "^5.6.3"
66
66
  },
67
67
  "optionalDependencies": {
68
- "magus-cli-darwin-arm64": "7.10.1",
69
- "magus-cli-darwin-x64": "7.10.1",
70
- "magus-cli-linux-x64": "7.10.1"
68
+ "magus-cli-darwin-arm64": "7.10.3",
69
+ "magus-cli-darwin-x64": "7.10.3",
70
+ "magus-cli-linux-x64": "7.10.3"
71
71
  }
72
72
  }
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Blind tests, contract C16: a tool whose declared installer does not own the
3
+ * file PATH runs is adopted by the node installer (bun or npm) when that file
4
+ * lies in bun's global tree or an npm global prefix. Otherwise it stays
5
+ * external / not-managed with a note that names no path.
6
+ *
7
+ * Written from the spec only. Every fact reaches `probeTool` through a
8
+ * scripted fake `ToolProbeIO`.
9
+ */
10
+ import { describe, expect, test } from "bun:test";
11
+ import {
12
+ type ExecResult,
13
+ type FetchJsonResult,
14
+ type ToolProbeIO,
15
+ classifyTool,
16
+ probeTool,
17
+ } from "../../services/tool-probe.js";
18
+ import type { BinRequirement } from "../../types/index.js";
19
+
20
+ interface FakeSetup {
21
+ exec?: Record<string, Partial<ExecResult>>;
22
+ fetch?: Record<string, FetchJsonResult>;
23
+ files?: Record<string, string>;
24
+ real?: Record<string, string>;
25
+ onPath?: Record<string, string>;
26
+ env?: Record<string, string>;
27
+ }
28
+
29
+ function fakeIO(setup: FakeSetup): ToolProbeIO & { calls: string[] } {
30
+ const calls: string[] = [];
31
+ return {
32
+ calls,
33
+ exec: async (command) => {
34
+ const key = [command.cmd, ...command.args].join(" ");
35
+ calls.push(key);
36
+ const h = setup.exec?.[key];
37
+ if (h === undefined) {
38
+ return {
39
+ code: null,
40
+ stdout: "",
41
+ stderr: "",
42
+ timedOut: false,
43
+ missing: true,
44
+ };
45
+ }
46
+ return {
47
+ code: 0,
48
+ stdout: "",
49
+ stderr: "",
50
+ timedOut: false,
51
+ missing: false,
52
+ ...h,
53
+ };
54
+ },
55
+ fetchJson: async (url) => {
56
+ const r = setup.fetch?.[url];
57
+ return r ?? { ok: false, reason: "returned 404" };
58
+ },
59
+ readJson: async (file) => {
60
+ const t = setup.files?.[file];
61
+ if (t === undefined) return null;
62
+ try {
63
+ return JSON.parse(t);
64
+ } catch {
65
+ return null;
66
+ }
67
+ },
68
+ readText: async (file) => setup.files?.[file] ?? null,
69
+ readlink: async () => null,
70
+ realpath: async (file) => setup.real?.[file] ?? null,
71
+ resolveExecutable: async (name) => setup.onPath?.[name] ?? null,
72
+ env: setup.env ?? {},
73
+ homedir: "/Users/j",
74
+ };
75
+ }
76
+
77
+ const BUN_ROOT = "/Users/j/.bun/install/global/node_modules";
78
+ const BREW_ENV = { HOMEBREW_PREFIX: "/opt/homebrew" };
79
+ const CELLAR = { "/opt/homebrew/Cellar": "/opt/homebrew/Cellar" };
80
+
81
+ const distTags = (pkg: string, latest: string) => ({
82
+ [`https://registry.npmjs.org/-/package/${pkg.replace("/", "%2F")}/dist-tags`]:
83
+ { ok: true, body: { latest } } as FetchJsonResult,
84
+ });
85
+
86
+ const pkgJson = (name: string, version: string) =>
87
+ JSON.stringify({ name, version });
88
+
89
+ /** A tool whose PATH entry resolves into bun's global tree. */
90
+ function bunWorld(
91
+ bin: string,
92
+ pkg: string,
93
+ installed: string,
94
+ latest: string,
95
+ ): FakeSetup {
96
+ const pkgDir = `${BUN_ROOT}/${pkg}`;
97
+ const file = `${pkgDir}/dist/index.js`;
98
+ const onPath = `/Users/j/.bun/bin/${bin}`;
99
+ return {
100
+ env: BREW_ENV,
101
+ onPath: { [bin]: onPath },
102
+ real: {
103
+ [onPath]: file,
104
+ [file]: file,
105
+ [pkgDir]: pkgDir,
106
+ [BUN_ROOT]: BUN_ROOT,
107
+ ...CELLAR,
108
+ },
109
+ files: { [`${pkgDir}/package.json`]: pkgJson(pkg, installed) },
110
+ fetch: distTags(pkg, latest),
111
+ };
112
+ }
113
+
114
+ const MNEMEX: BinRequirement = {
115
+ name: "mnemex",
116
+ via: "brew",
117
+ formula: "madappgang/tap/mnemex",
118
+ };
119
+
120
+ describe("C16 case 1: brew spec, file in bun's global tree", () => {
121
+ test("managed, versions read, bun upgrade command, state upgrade", async () => {
122
+ const f = await probeTool(
123
+ MNEMEX,
124
+ fakeIO(bunWorld("mnemex", "mnemex", "0.36.1", "0.36.2")),
125
+ );
126
+ expect(f.managed).toBe(true);
127
+ expect(f.installed).toBe("0.36.1");
128
+ expect(f.latest).toBe("0.36.2");
129
+ expect(f.upgradeCommand).toEqual({
130
+ cmd: "bun",
131
+ args: ["install", "-g", "mnemex@latest"],
132
+ });
133
+ const s = classifyTool(MNEMEX, f);
134
+ expect(s.state).toBe("upgrade");
135
+ expect(s.target).toBe("0.36.2");
136
+ });
137
+ });
138
+
139
+ describe("C16 case 2: latest equals installed", () => {
140
+ test("state is current", async () => {
141
+ const f = await probeTool(
142
+ MNEMEX,
143
+ fakeIO(bunWorld("mnemex", "mnemex", "0.36.2", "0.36.2")),
144
+ );
145
+ expect(f.managed).toBe(true);
146
+ expect(classifyTool(MNEMEX, f).state).toBe("current");
147
+ });
148
+ });
149
+
150
+ describe("C16 case 3: scoped package under bun's tree", () => {
151
+ test("the package is @scope/tool in the upgrade command", async () => {
152
+ const spec: BinRequirement = {
153
+ name: "tool",
154
+ via: "brew",
155
+ formula: "madappgang/tap/tool",
156
+ };
157
+ const f = await probeTool(
158
+ spec,
159
+ fakeIO(bunWorld("tool", "@scope/tool", "1.0.0", "1.1.0")),
160
+ );
161
+ expect(f.managed).toBe(true);
162
+ expect(f.installed).toBe("1.0.0");
163
+ expect(f.latest).toBe("1.1.0");
164
+ expect(f.upgradeCommand).toEqual({
165
+ cmd: "bun",
166
+ args: ["install", "-g", "@scope/tool@latest"],
167
+ });
168
+ expect(classifyTool(spec, f).state).toBe("upgrade");
169
+ });
170
+ });
171
+
172
+ describe("C16 case 4: npm global prefix", () => {
173
+ test("brew spec adopted by npm under the root npm reports", async () => {
174
+ const root = "/usr/local/lib/node_modules";
175
+ const pkgDir = `${root}/claudish`;
176
+ const file = `${pkgDir}/dist/index.js`;
177
+ const spec: BinRequirement = {
178
+ name: "claudish",
179
+ via: "brew",
180
+ formula: "madappgang/tap/claudish",
181
+ };
182
+ const f = await probeTool(
183
+ spec,
184
+ fakeIO({
185
+ env: BREW_ENV,
186
+ onPath: { claudish: "/usr/local/bin/claudish" },
187
+ real: {
188
+ "/usr/local/bin/claudish": file,
189
+ [file]: file,
190
+ [pkgDir]: pkgDir,
191
+ [root]: root,
192
+ ...CELLAR,
193
+ },
194
+ files: { [`${pkgDir}/package.json`]: pkgJson("claudish", "10.0.0") },
195
+ exec: { "npm root -g": { stdout: `${root}\n` } },
196
+ fetch: distTags("claudish", "10.1.0"),
197
+ }),
198
+ );
199
+ expect(f.managed).toBe(true);
200
+ expect(f.installed).toBe("10.0.0");
201
+ expect(f.latest).toBe("10.1.0");
202
+ expect(f.upgradeCommand).toEqual({
203
+ cmd: "npm",
204
+ args: ["install", "-g", "claudish@latest"],
205
+ });
206
+ const s = classifyTool(spec, f);
207
+ expect(s.state).toBe("upgrade");
208
+ expect(s.target).toBe("10.1.0");
209
+ });
210
+ });
211
+
212
+ describe("C16 case 5: pinned npm spec, file in bun's tree", () => {
213
+ const spec: BinRequirement = {
214
+ name: "tool",
215
+ via: "npm",
216
+ package: "tool",
217
+ version: "1.0.0",
218
+ };
219
+ test("managed, target is the pin, command carries tool@1.0.0", async () => {
220
+ const f = await probeTool(
221
+ spec,
222
+ fakeIO({
223
+ ...bunWorld("tool", "tool", "0.9.0", "2.0.0"),
224
+ exec: { "npm root -g": { stdout: "/usr/local/lib/node_modules\n" } },
225
+ }),
226
+ );
227
+ expect(f.managed).toBe(true);
228
+ expect(f.installed).toBe("0.9.0");
229
+ const args = f.upgradeCommand?.args ?? [];
230
+ expect(args).toContain("tool@1.0.0");
231
+ expect(args.some((a) => a.includes("@latest"))).toBe(false);
232
+ const s = classifyTool(spec, f);
233
+ expect(s.state).toBe("upgrade");
234
+ expect(s.target).toBe("1.0.0");
235
+ });
236
+ });
237
+
238
+ describe("C16 case 6: file in no node tree, brew spec", () => {
239
+ const spec: BinRequirement = { name: "uv", via: "brew", formula: "uv" };
240
+ const world: FakeSetup = {
241
+ env: BREW_ENV,
242
+ onPath: { uv: "/Users/j/.local/bin/uv" },
243
+ real: {
244
+ "/Users/j/.local/bin/uv": "/Users/j/.local/bin/uv",
245
+ ...CELLAR,
246
+ },
247
+ };
248
+ test("external / not-managed, note names who did not install it", async () => {
249
+ const f = await probeTool(spec, fakeIO(world));
250
+ const s = classifyTool(spec, f);
251
+ expect(s.state).toBe("external");
252
+ expect(s.reason).toBe("not-managed");
253
+ expect(s.note).toBe("installed by an installer magus does not update");
254
+ expect(s.note?.includes("/")).toBe(false);
255
+ });
256
+ test("an npm spec gets the same note: it names who installed it, never the spec installer", async () => {
257
+ const npmSpec: BinRequirement = { name: "uv", via: "npm", package: "uv" };
258
+ const f = await probeTool(
259
+ npmSpec,
260
+ fakeIO({
261
+ ...world,
262
+ exec: { "npm root -g": { stdout: "/usr/local/lib/node_modules\n" } },
263
+ }),
264
+ );
265
+ const s = classifyTool(npmSpec, f);
266
+ expect(s.state).toBe("external");
267
+ expect(s.note).toBe("installed by an installer magus does not update");
268
+ });
269
+ test("a pinned spec in that situation stays unknown", async () => {
270
+ const pinned: BinRequirement = { ...spec, version: "1.0.0" };
271
+ const f = await probeTool(pinned, fakeIO(world));
272
+ expect(classifyTool(pinned, f).state).toBe("unknown");
273
+ });
274
+ });
275
+
276
+ describe("C16 case 7: operating-system file", () => {
277
+ test("zsh in /bin is external with the operating-system note", async () => {
278
+ const spec: BinRequirement = { name: "zsh", via: "brew", formula: "zsh" };
279
+ const f = await probeTool(
280
+ spec,
281
+ fakeIO({
282
+ env: BREW_ENV,
283
+ onPath: { zsh: "/bin/zsh" },
284
+ real: { "/bin/zsh": "/bin/zsh", ...CELLAR },
285
+ }),
286
+ );
287
+ const s = classifyTool(spec, f);
288
+ expect(s.state).toBe("external");
289
+ expect(s.reason).toBe("not-managed");
290
+ expect(s.note).toBe("comes with the operating system");
291
+ });
292
+ });
293
+
294
+ describe("C16 case 8: a Homebrew keg is never adopted as an npm install", () => {
295
+ test("npm spec, file in a keg's libexec/lib/node_modules", async () => {
296
+ const keg = "/opt/homebrew/Cellar/gemini-cli/0.46.0/libexec";
297
+ const pkgDir = `${keg}/lib/node_modules/gemini-pkg`;
298
+ const file = `${pkgDir}/bundle/gemini.js`;
299
+ const spec: BinRequirement = {
300
+ name: "gemini",
301
+ via: "npm",
302
+ package: "gemini-pkg",
303
+ };
304
+ const f = await probeTool(
305
+ spec,
306
+ fakeIO({
307
+ env: BREW_ENV,
308
+ onPath: { gemini: "/opt/homebrew/bin/gemini" },
309
+ real: {
310
+ "/opt/homebrew/bin/gemini": file,
311
+ [file]: file,
312
+ [pkgDir]: pkgDir,
313
+ ...CELLAR,
314
+ },
315
+ files: { [`${pkgDir}/package.json`]: pkgJson("gemini-pkg", "0.46.0") },
316
+ exec: { "npm root -g": { stdout: "/opt/homebrew/lib/node_modules\n" } },
317
+ fetch: distTags("gemini-pkg", "0.50.0"),
318
+ }),
319
+ );
320
+ expect(f.upgradeCommand?.cmd).not.toBe("npm");
321
+ expect(classifyTool(spec, f).state).not.toBe("upgrade");
322
+ });
323
+ });
@@ -0,0 +1,271 @@
1
+ import {
2
+ afterAll,
3
+ beforeAll,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ test,
8
+ } from "bun:test";
9
+ import { execFileSync } from "node:child_process";
10
+ import os from "node:os";
11
+ import path from "node:path";
12
+ import fs from "fs-extra";
13
+ import {
14
+ loadedVersionHere,
15
+ repairStaleLoad,
16
+ } from "../../services/claude-settings.js";
17
+ import {
18
+ type PluginApplyDeps,
19
+ applyPlugins,
20
+ } from "../../services/update-engine.js";
21
+ import type {
22
+ InstalledPluginEntry,
23
+ InstalledPluginsRegistry,
24
+ } from "../../types/index.js";
25
+
26
+ /**
27
+ * Written blind from a spec. The row Claude Code picks for `plugin update`
28
+ * already holds the target, so the update writes nothing and no row moves.
29
+ * magus must treat it as the same misdirection as when a row moves.
30
+ */
31
+
32
+ const GIT_CFG = [
33
+ "-c",
34
+ "user.name=test",
35
+ "-c",
36
+ "user.email=test@example.com",
37
+ "-c",
38
+ "commit.gpgsign=false",
39
+ "-c",
40
+ "init.defaultBranch=main",
41
+ ];
42
+ const git = (cwd: string, ...args: string[]) =>
43
+ execFileSync("git", [...GIT_CFG, ...args], { cwd, encoding: "utf8" }).trim();
44
+
45
+ const PLUGIN = "madbench@magus";
46
+ const SAME_REPO_DETAIL =
47
+ "project → 0.5.0 · Claude Code updated another folder of this repository instead of this one — this folder now loads v0.5.0 too";
48
+
49
+ let base: string;
50
+ let mainTree: string;
51
+ let here: string;
52
+ let gone: string;
53
+ let otherRepo: string;
54
+ let root: string;
55
+ let registryFile: string;
56
+
57
+ const cacheFor = (version: string) =>
58
+ path.join(root, "cache", "magus", "madbench", version);
59
+
60
+ const row = (
61
+ over: Partial<InstalledPluginEntry> & { version: string },
62
+ ): InstalledPluginEntry => ({
63
+ scope: "project",
64
+ installPath: cacheFor(over.version),
65
+ installedAt: "2026-01-01T00:00:00.000Z",
66
+ lastUpdated: "2026-01-01T00:00:00.000Z",
67
+ ...over,
68
+ });
69
+
70
+ async function writeRegistry(rows: InstalledPluginEntry[]): Promise<void> {
71
+ const registry: InstalledPluginsRegistry = {
72
+ version: 2,
73
+ plugins: { [PLUGIN]: rows },
74
+ };
75
+ await fs.outputJson(registryFile, registry, { spaces: 2 });
76
+ }
77
+
78
+ const readRegistry = (): Promise<InstalledPluginsRegistry> =>
79
+ fs.readJson(registryFile);
80
+
81
+ const versionsByPath = async () =>
82
+ (await readRegistry()).plugins[PLUGIN].map((e) => [e.projectPath, e.version]);
83
+
84
+ const hereVersion = async () =>
85
+ (await readRegistry()).plugins[PLUGIN].find((e) => e.projectPath === here)
86
+ ?.version;
87
+
88
+ beforeAll(async () => {
89
+ base = await fs.realpath(
90
+ await fs.mkdtemp(path.join(os.tmpdir(), "magus-c17-")),
91
+ );
92
+ mainTree = path.join(base, "magus-src");
93
+ here = path.join(mainTree, ".claude", "worktrees", "madbench");
94
+ gone = path.join(mainTree, ".claude", "worktrees", "gone");
95
+ otherRepo = path.join(base, "unrelated");
96
+
97
+ await fs.ensureDir(mainTree);
98
+ git(mainTree, "init", "-q");
99
+ await fs.writeFile(path.join(mainTree, "README.md"), "hello\n");
100
+ git(mainTree, "add", "-A");
101
+ git(mainTree, "commit", "-qm", "initial");
102
+ git(mainTree, "worktree", "add", "-q", "-b", "madbench", here);
103
+ git(mainTree, "worktree", "add", "-q", "-b", "gone", gone);
104
+ await fs.remove(gone);
105
+
106
+ await fs.ensureDir(otherRepo);
107
+ git(otherRepo, "init", "-q");
108
+ });
109
+
110
+ afterAll(async () => {
111
+ if (base) await fs.remove(base);
112
+ });
113
+
114
+ beforeEach(async () => {
115
+ root = await fs.mkdtemp(path.join(base, "plugins-"));
116
+ registryFile = path.join(root, "installed_plugins.json");
117
+ for (const v of ["0.3.0", "0.4.0", "0.5.0", "0.6.0"]) {
118
+ await fs.ensureDir(cacheFor(v));
119
+ }
120
+ });
121
+
122
+ /** `claude plugin update` that finds its row already on the target: writes nothing. */
123
+ function deps(): PluginApplyDeps {
124
+ return {
125
+ install: async () => {},
126
+ update: async () => {},
127
+ repair: async () => {},
128
+ readInstalled: async () =>
129
+ (await readRegistry()).plugins[PLUGIN].find((e) => e.projectPath === here)
130
+ ?.version ?? null,
131
+ saveInstalled: async () => {},
132
+ registryRows: async () => (await readRegistry()).plugins[PLUGIN],
133
+ repairStaleLoad: (id, target, projectPath) =>
134
+ repairStaleLoad(id, target, projectPath, root),
135
+ loadedVersion: (id, projectPath) =>
136
+ loadedVersionHere(id, projectPath, root),
137
+ };
138
+ }
139
+
140
+ const item = (target: string) => ({
141
+ pluginId: PLUGIN,
142
+ pinned: "latest",
143
+ installed: "0.4.0",
144
+ available: target,
145
+ target,
146
+ action: "update" as const,
147
+ reason: "behind" as const,
148
+ scopes: ["project" as const],
149
+ beforeVersions: { project: "0.4.0" },
150
+ });
151
+
152
+ function collect() {
153
+ const details: string[] = [];
154
+ return {
155
+ details,
156
+ reporter: {
157
+ begin: () => {},
158
+ finish: (_ok: boolean, _name: string, detail: string) =>
159
+ details.push(detail),
160
+ detach: <T>(work: () => T) => work(),
161
+ },
162
+ };
163
+ }
164
+
165
+ const NO_CHANGE = `no change — the installer exited 0 but ${PLUGIN} is still 0.4.0`;
166
+
167
+ describe("a misdirected update whose row already held the target", () => {
168
+ test("1: another row of the repository is at the target — this row follows, counted as changed", async () => {
169
+ await writeRegistry([
170
+ row({ version: "0.5.0", projectPath: mainTree }),
171
+ row({ version: "0.4.0", projectPath: here }),
172
+ ]);
173
+ const { details, reporter } = collect();
174
+
175
+ const result = await applyPlugins([item("0.5.0")], here, deps(), reporter);
176
+
177
+ expect(result.ok).toBe(1);
178
+ expect(result.unchanged).toEqual([]);
179
+ expect(await versionsByPath()).toEqual([
180
+ [mainTree, "0.5.0"],
181
+ [here, "0.5.0"],
182
+ ]);
183
+ expect(details).toEqual([SAME_REPO_DETAIL]);
184
+ expect(await loadedVersionHere(PLUGIN, here, root)).toBe("0.5.0");
185
+ });
186
+
187
+ test("2: several other rows at the target — still one folder, 'another folder'", async () => {
188
+ await writeRegistry([
189
+ row({ version: "0.5.0", projectPath: gone }),
190
+ row({ version: "0.5.0", projectPath: mainTree }),
191
+ row({ version: "0.4.0", projectPath: here }),
192
+ ]);
193
+ const { details, reporter } = collect();
194
+
195
+ const result = await applyPlugins([item("0.5.0")], here, deps(), reporter);
196
+
197
+ expect(result.ok).toBe(1);
198
+ expect(result.unchanged).toEqual([]);
199
+ expect(await hereVersion()).toBe("0.5.0");
200
+ expect(details).toEqual([SAME_REPO_DETAIL]);
201
+ expect(details[0]).not.toMatch(/other folders/);
202
+ });
203
+
204
+ test("3 (negative a): the only row at the target is another repository's — no follow", async () => {
205
+ await writeRegistry([
206
+ row({ version: "0.5.0", projectPath: otherRepo }),
207
+ row({ version: "0.4.0", projectPath: mainTree }),
208
+ row({ version: "0.4.0", projectPath: here }),
209
+ ]);
210
+ const { details, reporter } = collect();
211
+
212
+ const result = await applyPlugins([item("0.5.0")], here, deps(), reporter);
213
+
214
+ expect(result.unchanged).toEqual([PLUGIN]);
215
+ expect(result.ok).toBe(0);
216
+ expect(await hereVersion()).toBe("0.4.0");
217
+ expect(details).toEqual([NO_CHANGE]);
218
+ });
219
+
220
+ test("4 (negative b): other repository rows are NEWER than the target — no follow", async () => {
221
+ await writeRegistry([
222
+ row({ version: "0.6.0", projectPath: mainTree }),
223
+ row({ version: "0.4.0", projectPath: here }),
224
+ ]);
225
+ const { details, reporter } = collect();
226
+
227
+ const result = await applyPlugins([item("0.5.0")], here, deps(), reporter);
228
+
229
+ expect(result.unchanged).toEqual([PLUGIN]);
230
+ expect(await hereVersion()).toBe("0.4.0");
231
+ expect(await versionsByPath()).toEqual([
232
+ [mainTree, "0.6.0"],
233
+ [here, "0.4.0"],
234
+ ]);
235
+ expect(details).toEqual([NO_CHANGE]);
236
+ });
237
+
238
+ test("4b (negative b): other repository rows are OLDER than the target — no follow", async () => {
239
+ await writeRegistry([
240
+ row({ version: "0.3.0", projectPath: mainTree }),
241
+ row({ version: "0.4.0", projectPath: here }),
242
+ ]);
243
+ const { details, reporter } = collect();
244
+
245
+ await applyPlugins([item("0.5.0")], here, deps(), reporter);
246
+
247
+ // The older row is not the target, so this row does not follow it. (The
248
+ // stale-load repair that raises that older row to this one's version is
249
+ // older behaviour, tested on its own.)
250
+ expect(await hereVersion()).toBe("0.4.0");
251
+ expect(details.join("\n")).not.toContain("another folder");
252
+ });
253
+
254
+ test("5: the target is not downloaded — could not follow, row stays", async () => {
255
+ await writeRegistry([
256
+ row({ version: "0.5.0", projectPath: mainTree }),
257
+ row({ version: "0.4.0", projectPath: here }),
258
+ ]);
259
+ await fs.remove(cacheFor("0.5.0"));
260
+ const { details, reporter } = collect();
261
+
262
+ const result = await applyPlugins([item("0.5.0")], here, deps(), reporter);
263
+
264
+ expect(result.unchanged).toEqual([PLUGIN]);
265
+ expect(await hereVersion()).toBe("0.4.0");
266
+ expect(details).toHaveLength(1);
267
+ expect(details[0]).toContain(
268
+ `no change — the installer exited 0 but ${PLUGIN} is still 0.4.0 (Claude Code updated another folder of this repository instead of this one, and this folder could not follow: v0.5.0 is not downloaded — run magus install)`,
269
+ );
270
+ });
271
+ });