claudeup 6.5.1 → 6.6.0
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 +4 -4
- package/src/__tests__/active-closure.test.ts +184 -0
- package/src/__tests__/blackbox-active-closure.test.ts +119 -0
- package/src/__tests__/blackbox-convergence.test.ts +334 -0
- package/src/__tests__/blackbox-parity.test.ts +684 -0
- package/src/__tests__/blackbox-plugin-state.test.ts +523 -0
- package/src/__tests__/blackbox-tool-state.test.ts +669 -0
- package/src/__tests__/cli-apply-seams.test.ts +2 -0
- package/src/__tests__/cli-tool-commands.test.ts +1 -1
- package/src/__tests__/cli-tools-adapter.test.ts +294 -0
- package/src/__tests__/cli-update-view.test.ts +142 -2
- package/src/__tests__/content-drift.test.ts +60 -0
- package/src/__tests__/enabled-not-installed.test.ts +1 -2
- package/src/__tests__/fixtures/tool-probe/brew-list-versions-tmux.txt +2 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-behind-homebrew-spec.txt +29 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-tmux-current.txt +7 -0
- package/src/__tests__/fixtures/tool-probe/brew-outdated-unknown-formula.txt +6 -0
- package/src/__tests__/fixtures/tool-probe/go-version-m-tmux-mcp.txt +27 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-missing-scoped.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/npm-dist-tags-openai-codex.txt +4 -0
- package/src/__tests__/fixtures/tool-probe/pypi-cowsay.txt +3 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-browser-use.toml +10 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-git.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-cowsay-pinned.toml +8 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-graphifyy.toml +7 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-mcp.toml +6 -0
- package/src/__tests__/fixtures/tool-probe/uv-receipt-serena-agent.toml +9 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated-pinned.txt +5 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list-outdated.txt +11 -0
- package/src/__tests__/fixtures/tool-probe/uv-tool-list.txt +22 -0
- package/src/__tests__/{gitignore-prerun.test.ts → gitignore-check.test.ts} +8 -6
- package/src/__tests__/plugin-cli-argv.test.ts +1 -1
- package/src/__tests__/plugin-row-text.test.ts +336 -0
- package/src/__tests__/scope-action.test.ts +69 -19
- package/src/__tests__/shell-script-callers.test.ts +8 -8
- package/src/__tests__/tool-probe.test.ts +1400 -0
- package/src/__tests__/update-apply.test.ts +283 -1
- package/src/__tests__/update-plan.test.ts +773 -31
- package/src/__tests__/version-unknown-state.test.ts +388 -0
- package/src/cli/router.ts +0 -5
- package/src/cli/update-view.ts +92 -12
- package/src/cli/update.ts +114 -38
- package/src/services/active-closure.ts +117 -0
- package/src/services/catalog-cache-store.ts +6 -2
- package/src/services/claude-settings.ts +1 -173
- package/src/services/{gitignore-prerun.ts → gitignore-check.ts} +14 -10
- package/src/services/gitignore-fixer.ts +1 -1
- package/src/services/marketplace-refresh.ts +30 -0
- package/src/services/plugin-manager.ts +200 -283
- package/src/services/resolver.ts +7 -2
- package/src/services/tool-probe.ts +1637 -0
- package/src/services/toolchain.ts +97 -0
- package/src/services/update-engine.ts +220 -32
- package/src/services/update-plan.ts +640 -160
- package/src/services/version-snapshot.ts +1 -1
- package/src/types/bun.d.ts +6 -0
- package/src/ui/App.tsx +2 -27
- package/src/ui/adapters/cliToolsAdapter.ts +176 -0
- package/src/ui/adapters/pluginsAdapter.ts +57 -0
- package/src/ui/components/modals/ModalContainer.tsx +0 -26
- package/src/ui/hooks/index.ts +0 -1
- package/src/ui/hooks/useGitignoreModal.ts +2 -3
- package/src/ui/renderers/cliToolRenderers.tsx +108 -67
- package/src/ui/renderers/pluginRenderers.tsx +157 -98
- package/src/ui/renderers/pluginRowText.ts +70 -0
- package/src/ui/renderers/pluginStatusText.ts +56 -0
- package/src/ui/screens/CliToolsScreen.tsx +154 -280
- package/src/ui/screens/PluginsScreen.tsx +156 -140
- package/src/ui/state/types.ts +0 -7
- package/src/__tests__/dual-write-prevention.test.ts +0 -380
- package/src/__tests__/gap-fill-versions.test.ts +0 -389
- package/src/__tests__/plugin-version-check.test.ts +0 -762
- package/src/cli/claude.ts +0 -21
- package/src/prerunner/index.ts +0 -500
- package/src/services/plugin-version-check.ts +0 -360
- package/src/services/update-cache.ts +0 -78
- package/src/ui/components/modals/VersionMismatchModal.tsx +0 -138
- package/src/ui/hooks/useMismatchModal.ts +0 -89
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Version tracking tests
|
|
3
|
-
*
|
|
4
|
-
* The Claude CLI's `plugin install` command does NOT update `installedPluginVersions`
|
|
5
|
-
* in settings.json — it only manages `enabledPlugins`. Claudeup must save the version
|
|
6
|
-
* itself after a successful CLI install/update.
|
|
7
|
-
*
|
|
8
|
-
* PluginsScreen: no saveVersionAfterInstall helper (removed in v4.5.0), but each
|
|
9
|
-
* action handler calls saveVersionForScope after CLI success.
|
|
10
|
-
* Prerunner: calls saveGlobalInstalledPluginVersion after successful updatePlugin,
|
|
11
|
-
* but NOT when CLI is unavailable or update fails (no phantom state).
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test";
|
|
15
|
-
import * as fs from "node:fs";
|
|
16
|
-
import * as path from "node:path";
|
|
17
|
-
import * as url from "node:url";
|
|
18
|
-
import * as actualClaudeCli from "../services/claude-cli.js";
|
|
19
|
-
// Real modules captured before the mock.module() overrides below, so the mocks
|
|
20
|
-
// can spread the full export surface instead of stripping it.
|
|
21
|
-
import * as actualClaudeSettings from "../services/claude-settings.js";
|
|
22
|
-
import * as actualPluginManager from "../services/plugin-manager.js";
|
|
23
|
-
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
// PART 1: PluginsScreen source-level policy tests
|
|
26
|
-
//
|
|
27
|
-
// The dual-write calls in PluginsScreen are inside React event handlers.
|
|
28
|
-
// Rendering the full TUI component in a unit test would require the opentui
|
|
29
|
-
// terminal runtime. Instead we assert the source does NOT contain the
|
|
30
|
-
// forbidden call pattern — this is a code-policy test that:
|
|
31
|
-
// - FAILS before the fix (6 saveVersionAfterInstall calls follow CLI calls)
|
|
32
|
-
// - PASSES after the fix (those calls are removed)
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
|
|
35
|
-
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
|
36
|
-
const PLUGINS_SCREEN_PATH = path.resolve(
|
|
37
|
-
__dirname,
|
|
38
|
-
"../ui/screens/PluginsScreen.tsx",
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
describe("PluginsScreen — no dual-write after CLI operations", () => {
|
|
42
|
-
let source: string;
|
|
43
|
-
|
|
44
|
-
beforeEach(() => {
|
|
45
|
-
source = fs.readFileSync(PLUGINS_SCREEN_PATH, "utf8");
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it("does not call saveVersionAfterInstall immediately after cliUpdatePlugin", () => {
|
|
49
|
-
// Find every occurrence of cliUpdatePlugin in the source. Each one should
|
|
50
|
-
// NOT be immediately followed (within 3 lines) by saveVersionAfterInstall.
|
|
51
|
-
const lines = source.split("\n");
|
|
52
|
-
const violations: number[] = [];
|
|
53
|
-
|
|
54
|
-
for (let i = 0; i < lines.length; i++) {
|
|
55
|
-
if (lines[i].includes("cliUpdatePlugin(")) {
|
|
56
|
-
// Check the next 3 lines for a dual-write call
|
|
57
|
-
for (let j = i + 1; j <= Math.min(i + 3, lines.length - 1); j++) {
|
|
58
|
-
if (lines[j].includes("saveVersionAfterInstall(")) {
|
|
59
|
-
violations.push(i + 1); // 1-indexed line number
|
|
60
|
-
break;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
expect(violations).toEqual([]);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("does not call saveVersionAfterInstall immediately after cliInstallPlugin", () => {
|
|
70
|
-
const lines = source.split("\n");
|
|
71
|
-
const violations: number[] = [];
|
|
72
|
-
|
|
73
|
-
for (let i = 0; i < lines.length; i++) {
|
|
74
|
-
if (lines[i].includes("cliInstallPlugin(")) {
|
|
75
|
-
for (let j = i + 1; j <= Math.min(i + 3, lines.length - 1); j++) {
|
|
76
|
-
if (lines[j].includes("saveVersionAfterInstall(")) {
|
|
77
|
-
violations.push(i + 1);
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
expect(violations).toEqual([]);
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// ---------------------------------------------------------------------------
|
|
89
|
-
// PART 2: Prerunner — saveGlobalInstalledPluginVersion guarded by CLI success
|
|
90
|
-
//
|
|
91
|
-
// We mock the prerunner's module dependencies so we can call prerunClaude()
|
|
92
|
-
// directly and inspect call counts.
|
|
93
|
-
// ---------------------------------------------------------------------------
|
|
94
|
-
|
|
95
|
-
// Mutable state shared between mock factories and test assertions.
|
|
96
|
-
let mockSaveGlobal: ReturnType<typeof mock>;
|
|
97
|
-
let mockSaveProject: ReturnType<typeof mock>;
|
|
98
|
-
let mockSaveLocal: ReturnType<typeof mock>;
|
|
99
|
-
let mockUpdatePlugin: ReturnType<typeof mock>;
|
|
100
|
-
let mockRepairPlugin: ReturnType<typeof mock>;
|
|
101
|
-
let mockIsClaudeAvailable: ReturnType<typeof mock>;
|
|
102
|
-
let mockGetAvailablePlugins: ReturnType<typeof mock>;
|
|
103
|
-
|
|
104
|
-
// Spread the REAL modules and override only the functions under test, so this
|
|
105
|
-
// file's mock.module doesn't strip other exports the module graph needs (which
|
|
106
|
-
// breaks both this file in isolation and — because bun's mock.module is global
|
|
107
|
-
// and not restored across files — every later test file). The `import * as`
|
|
108
|
-
// bindings are evaluated before these mock.module() calls run, so they capture
|
|
109
|
-
// the real modules.
|
|
110
|
-
mock.module("../services/claude-settings.js", () => ({
|
|
111
|
-
...actualClaudeSettings,
|
|
112
|
-
recoverMarketplaceSettings: mock(() =>
|
|
113
|
-
Promise.resolve({ enabledAutoUpdate: [], removed: [], reregistered: [] }),
|
|
114
|
-
),
|
|
115
|
-
migrateMarketplaceRename: mock(() =>
|
|
116
|
-
Promise.resolve({
|
|
117
|
-
projectMigrated: 0,
|
|
118
|
-
globalMigrated: 0,
|
|
119
|
-
localMigrated: 0,
|
|
120
|
-
registryMigrated: 0,
|
|
121
|
-
knownMarketplacesMigrated: false,
|
|
122
|
-
}),
|
|
123
|
-
),
|
|
124
|
-
getGlobalEnabledPlugins: mock(() => Promise.resolve({})),
|
|
125
|
-
getEnabledPlugins: mock(() => Promise.resolve({})),
|
|
126
|
-
getLocalEnabledPlugins: mock(() => Promise.resolve({})),
|
|
127
|
-
saveGlobalInstalledPluginVersion: (...args: unknown[]) =>
|
|
128
|
-
mockSaveGlobal(...args),
|
|
129
|
-
saveLocalInstalledPluginVersion: (...args: unknown[]) =>
|
|
130
|
-
mockSaveLocal(...args),
|
|
131
|
-
readGlobalSettings: mock(() => Promise.resolve({ hooks: {} })),
|
|
132
|
-
writeGlobalSettings: mock(() => Promise.resolve()),
|
|
133
|
-
}));
|
|
134
|
-
|
|
135
|
-
mock.module("../services/claude-cli.js", () => ({
|
|
136
|
-
...actualClaudeCli,
|
|
137
|
-
updatePlugin: (...args: unknown[]) => mockUpdatePlugin(...args),
|
|
138
|
-
repairPlugin: (...args: unknown[]) => mockRepairPlugin(...args),
|
|
139
|
-
isClaudeAvailable: (...args: unknown[]) => mockIsClaudeAvailable(...args),
|
|
140
|
-
addMarketplace: mock(() => Promise.resolve()),
|
|
141
|
-
updateMarketplace: mock(() => Promise.resolve()),
|
|
142
|
-
}));
|
|
143
|
-
|
|
144
|
-
mock.module("../services/plugin-manager.js", () => ({
|
|
145
|
-
...actualPluginManager,
|
|
146
|
-
getAvailablePlugins: (...args: unknown[]) => mockGetAvailablePlugins(...args),
|
|
147
|
-
saveInstalledPluginVersion: (...args: unknown[]) => mockSaveProject(...args),
|
|
148
|
-
clearMarketplaceCache: mock(() => undefined),
|
|
149
|
-
}));
|
|
150
|
-
|
|
151
|
-
mock.module("../services/update-cache.js", () => ({
|
|
152
|
-
UpdateCache: class {
|
|
153
|
-
shouldCheckForUpdates = mock(() => Promise.resolve(true));
|
|
154
|
-
saveCheck = mock(() => Promise.resolve());
|
|
155
|
-
},
|
|
156
|
-
}));
|
|
157
|
-
|
|
158
|
-
mock.module("../services/claude-runner.js", () => ({
|
|
159
|
-
runClaude: mock(() => Promise.resolve(0)),
|
|
160
|
-
}));
|
|
161
|
-
|
|
162
|
-
mock.module("../data/marketplaces.js", () => ({
|
|
163
|
-
defaultMarketplaces: [],
|
|
164
|
-
}));
|
|
165
|
-
|
|
166
|
-
mock.module("../utils/string-utils.js", () => ({
|
|
167
|
-
parsePluginId: mock((_id: string) => null),
|
|
168
|
-
}));
|
|
169
|
-
|
|
170
|
-
mock.module("fs-extra", () => ({
|
|
171
|
-
default: {
|
|
172
|
-
pathExists: mock(() => Promise.resolve(false)),
|
|
173
|
-
readJson: mock(() => Promise.resolve({})),
|
|
174
|
-
writeJson: mock(() => Promise.resolve()),
|
|
175
|
-
ensureDir: mock(() => Promise.resolve()),
|
|
176
|
-
},
|
|
177
|
-
}));
|
|
178
|
-
|
|
179
|
-
// Now import the module under test (after mock.module registrations).
|
|
180
|
-
const { prerunClaude } = await import("../prerunner/index.js");
|
|
181
|
-
|
|
182
|
-
interface ScopeStatusLike {
|
|
183
|
-
enabled: boolean;
|
|
184
|
-
version?: string;
|
|
185
|
-
}
|
|
186
|
-
interface PluginLike {
|
|
187
|
-
id: string;
|
|
188
|
-
enabled: boolean;
|
|
189
|
-
hasUpdate: boolean;
|
|
190
|
-
contentStale?: boolean;
|
|
191
|
-
installedVersion: string;
|
|
192
|
-
version: string;
|
|
193
|
-
userScope?: ScopeStatusLike;
|
|
194
|
-
projectScope?: ScopeStatusLike;
|
|
195
|
-
localScope?: ScopeStatusLike;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Minimal PluginInfo-like object. The per-scope status matters: the prerunner
|
|
200
|
-
* updates the scopes that actually hold an outdated install, so a fixture with
|
|
201
|
-
* no scope status describes a plugin installed nowhere and is correctly skipped.
|
|
202
|
-
*/
|
|
203
|
-
function makePlugin(overrides: Partial<PluginLike> = {}): PluginLike {
|
|
204
|
-
return {
|
|
205
|
-
id: "test-plugin@magus",
|
|
206
|
-
enabled: true,
|
|
207
|
-
hasUpdate: true,
|
|
208
|
-
installedVersion: "1.0.0",
|
|
209
|
-
version: "1.1.0",
|
|
210
|
-
userScope: { enabled: true, version: "1.0.0" },
|
|
211
|
-
...overrides,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
describe("prerunner — saveGlobalInstalledPluginVersion call count", () => {
|
|
216
|
-
beforeEach(() => {
|
|
217
|
-
mockSaveGlobal = mock(() => Promise.resolve());
|
|
218
|
-
mockSaveProject = mock(() => Promise.resolve());
|
|
219
|
-
mockSaveLocal = mock(() => Promise.resolve());
|
|
220
|
-
mockUpdatePlugin = mock(() => Promise.resolve());
|
|
221
|
-
mockRepairPlugin = mock(() => Promise.resolve());
|
|
222
|
-
mockIsClaudeAvailable = mock(() => Promise.resolve(true));
|
|
223
|
-
mockGetAvailablePlugins = mock(() => Promise.resolve([makePlugin()]));
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("calls saveGlobalInstalledPluginVersion once after successful CLI update", async () => {
|
|
227
|
-
// CLI is available, updatePlugin succeeds.
|
|
228
|
-
// The CLI does NOT update installedPluginVersions, so claudeup must save
|
|
229
|
-
// the version after a successful update.
|
|
230
|
-
mockIsClaudeAvailable = mock(() => Promise.resolve(true));
|
|
231
|
-
mockUpdatePlugin = mock(() => Promise.resolve());
|
|
232
|
-
|
|
233
|
-
await prerunClaude(["--help"], { force: true });
|
|
234
|
-
|
|
235
|
-
expect(mockSaveGlobal).toHaveBeenCalledTimes(1);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* The prerunner used to call `updatePlugin(id, "user")` unconditionally.
|
|
240
|
-
* `hasUpdate` is computed from the CURRENT PROJECT's resolved version, but
|
|
241
|
-
* installs live per project in installed_plugins.json — 495 of 496 magus
|
|
242
|
-
* installs on the reporter's machine are project-scoped. So it detected
|
|
243
|
-
* project drift, updated user scope, reported success, and left the project
|
|
244
|
-
* untouched, re-detecting the identical drift on every subsequent run.
|
|
245
|
-
*/
|
|
246
|
-
it("updates the PROJECT scope when that is where the outdated install is", async () => {
|
|
247
|
-
mockGetAvailablePlugins = mock(() =>
|
|
248
|
-
Promise.resolve([
|
|
249
|
-
makePlugin({
|
|
250
|
-
userScope: undefined,
|
|
251
|
-
projectScope: { enabled: true, version: "1.0.0" },
|
|
252
|
-
}),
|
|
253
|
-
]),
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
await prerunClaude(["--help"], { force: true });
|
|
257
|
-
|
|
258
|
-
expect(mockUpdatePlugin).toHaveBeenCalledTimes(1);
|
|
259
|
-
expect(mockUpdatePlugin.mock.calls[0]?.[1]).toBe("project");
|
|
260
|
-
expect(mockSaveProject).toHaveBeenCalledTimes(1);
|
|
261
|
-
expect(mockSaveGlobal).toHaveBeenCalledTimes(0);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it("updates every scope that is outdated, not just the first", async () => {
|
|
265
|
-
mockGetAvailablePlugins = mock(() =>
|
|
266
|
-
Promise.resolve([
|
|
267
|
-
makePlugin({
|
|
268
|
-
userScope: { enabled: true, version: "1.0.0" },
|
|
269
|
-
projectScope: { enabled: true, version: "1.0.0" },
|
|
270
|
-
localScope: { enabled: true, version: "1.1.0" }, // already current
|
|
271
|
-
}),
|
|
272
|
-
]),
|
|
273
|
-
);
|
|
274
|
-
|
|
275
|
-
await prerunClaude(["--help"], { force: true });
|
|
276
|
-
|
|
277
|
-
const scopes = mockUpdatePlugin.mock.calls
|
|
278
|
-
.map((c: unknown[]) => c[1])
|
|
279
|
-
.sort();
|
|
280
|
-
expect(scopes).toEqual(["project", "user"]);
|
|
281
|
-
expect(mockSaveLocal).toHaveBeenCalledTimes(0);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* A content-stale plugin has no version bump to install over, and
|
|
286
|
-
* `claude plugin install` no-ops on "already installed" (verified against the
|
|
287
|
-
* real marketplace in autotest/plugin-system/probe-05). The prerunner must
|
|
288
|
-
* therefore repair — uninstall+install — not update.
|
|
289
|
-
*/
|
|
290
|
-
it("repairs a content-stale plugin instead of updating it", async () => {
|
|
291
|
-
mockGetAvailablePlugins = mock(() =>
|
|
292
|
-
Promise.resolve([
|
|
293
|
-
makePlugin({
|
|
294
|
-
hasUpdate: false,
|
|
295
|
-
contentStale: true,
|
|
296
|
-
installedVersion: "1.0.0",
|
|
297
|
-
version: "1.0.0",
|
|
298
|
-
userScope: undefined,
|
|
299
|
-
projectScope: { enabled: true, version: "1.0.0" },
|
|
300
|
-
}),
|
|
301
|
-
]),
|
|
302
|
-
);
|
|
303
|
-
|
|
304
|
-
await prerunClaude(["--help"], { force: true });
|
|
305
|
-
|
|
306
|
-
expect(mockRepairPlugin).toHaveBeenCalledTimes(1);
|
|
307
|
-
expect(mockRepairPlugin.mock.calls[0]?.[1]).toBe("project");
|
|
308
|
-
expect(mockUpdatePlugin).toHaveBeenCalledTimes(0);
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
it("prefers a real version update over a repair when both apply", async () => {
|
|
312
|
-
// A pending version bump reinstalls the files anyway, so repairing as well
|
|
313
|
-
// would double the work and the uninstall window.
|
|
314
|
-
mockGetAvailablePlugins = mock(() =>
|
|
315
|
-
Promise.resolve([makePlugin({ hasUpdate: true, contentStale: true })]),
|
|
316
|
-
);
|
|
317
|
-
|
|
318
|
-
await prerunClaude(["--help"], { force: true });
|
|
319
|
-
|
|
320
|
-
expect(mockUpdatePlugin).toHaveBeenCalledTimes(1);
|
|
321
|
-
expect(mockRepairPlugin).toHaveBeenCalledTimes(0);
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it("does not repair when the CLI is unavailable", async () => {
|
|
325
|
-
mockIsClaudeAvailable = mock(() => Promise.resolve(false));
|
|
326
|
-
mockGetAvailablePlugins = mock(() =>
|
|
327
|
-
Promise.resolve([
|
|
328
|
-
makePlugin({
|
|
329
|
-
hasUpdate: false,
|
|
330
|
-
contentStale: true,
|
|
331
|
-
version: "1.0.0",
|
|
332
|
-
projectScope: { enabled: true, version: "1.0.0" },
|
|
333
|
-
}),
|
|
334
|
-
]),
|
|
335
|
-
);
|
|
336
|
-
|
|
337
|
-
await prerunClaude(["--help"], { force: true });
|
|
338
|
-
|
|
339
|
-
expect(mockRepairPlugin).toHaveBeenCalledTimes(0);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it("does nothing when no scope holds an outdated install", async () => {
|
|
343
|
-
// hasUpdate can be true from a stale settings base while every real
|
|
344
|
-
// install is already current. Updating "somewhere" would be a guess.
|
|
345
|
-
mockGetAvailablePlugins = mock(() =>
|
|
346
|
-
Promise.resolve([
|
|
347
|
-
makePlugin({
|
|
348
|
-
userScope: { enabled: true, version: "1.1.0" },
|
|
349
|
-
projectScope: undefined,
|
|
350
|
-
}),
|
|
351
|
-
]),
|
|
352
|
-
);
|
|
353
|
-
|
|
354
|
-
await prerunClaude(["--help"], { force: true });
|
|
355
|
-
|
|
356
|
-
expect(mockUpdatePlugin).toHaveBeenCalledTimes(0);
|
|
357
|
-
expect(mockSaveGlobal).toHaveBeenCalledTimes(0);
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
it("does NOT call saveGlobalInstalledPluginVersion when CLI is unavailable", async () => {
|
|
361
|
-
// CLI unavailable — we must NOT write phantom state.
|
|
362
|
-
mockIsClaudeAvailable = mock(() => Promise.resolve(false));
|
|
363
|
-
|
|
364
|
-
await prerunClaude(["--help"], { force: true });
|
|
365
|
-
|
|
366
|
-
expect(mockSaveGlobal).toHaveBeenCalledTimes(0);
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
it("does NOT call saveGlobalInstalledPluginVersion when CLI updatePlugin throws", async () => {
|
|
370
|
-
// CLI available but updatePlugin fails — must NOT write phantom state.
|
|
371
|
-
mockIsClaudeAvailable = mock(() => Promise.resolve(true));
|
|
372
|
-
mockUpdatePlugin = mock(() =>
|
|
373
|
-
Promise.reject(new Error("CLI update failed")),
|
|
374
|
-
);
|
|
375
|
-
|
|
376
|
-
await prerunClaude(["--help"], { force: true });
|
|
377
|
-
|
|
378
|
-
expect(mockSaveGlobal).toHaveBeenCalledTimes(0);
|
|
379
|
-
});
|
|
380
|
-
});
|