magus-cli 0.0.0-placeholder → 7.1.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/README.md +309 -0
- package/bin/magus.js +170 -0
- package/package.json +67 -3
- package/scripts/build-binaries.ts +113 -0
- package/scripts/capture-builtin.ts +412 -0
- package/scripts/check-optional-deps.ts +49 -0
- package/scripts/test-isolated.ts +49 -0
- package/scripts/verify-binary.ts +95 -0
- package/scripts/verify-community-registry.ts +272 -0
- package/src/__tests__/active-closure.test.ts +184 -0
- package/src/__tests__/alias-adopt.test.ts +365 -0
- package/src/__tests__/alias-parser.test.ts +417 -0
- package/src/__tests__/alias-shell-writer.test.ts +681 -0
- package/src/__tests__/alias-store.test.ts +163 -0
- package/src/__tests__/binary-signing.test.ts +120 -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 +725 -0
- package/src/__tests__/blackbox-plugin-state.test.ts +545 -0
- package/src/__tests__/blackbox-tool-state.test.ts +669 -0
- package/src/__tests__/cache-provenance.test.ts +84 -0
- package/src/__tests__/capture-builtin.test.ts +92 -0
- package/src/__tests__/catalog-cache-store.test.ts +309 -0
- package/src/__tests__/catalog-notice.test.ts +155 -0
- package/src/__tests__/cli-ansi.test.ts +297 -0
- package/src/__tests__/cli-apply-seams.test.ts +288 -0
- package/src/__tests__/cli-live.test.ts +391 -0
- package/src/__tests__/cli-router.test.ts +240 -0
- package/src/__tests__/cli-tool-commands.test.ts +239 -0
- package/src/__tests__/cli-tools-adapter.test.ts +294 -0
- package/src/__tests__/cli-update-view.test.ts +524 -0
- package/src/__tests__/community-fetch.test.ts +545 -0
- package/src/__tests__/community-registry.test.ts +269 -0
- package/src/__tests__/community-staleness.test.ts +720 -0
- package/src/__tests__/component-badge.test.ts +98 -0
- package/src/__tests__/component-icons.test.ts +94 -0
- package/src/__tests__/content-drift.test.ts +353 -0
- package/src/__tests__/conventions-integration.test.ts +830 -0
- package/src/__tests__/conventions-manager.test.ts +1236 -0
- package/src/__tests__/doctor-bins.test.ts +43 -0
- package/src/__tests__/doctor.test.ts +463 -0
- package/src/__tests__/dotenv.test.ts +195 -0
- package/src/__tests__/enabled-not-installed.test.ts +94 -0
- package/src/__tests__/file-locking.test.ts +208 -0
- 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__/footer-hints.test.ts +40 -0
- package/src/__tests__/format-command.test.ts +78 -0
- package/src/__tests__/git-worktree.test.ts +108 -0
- package/src/__tests__/github-budget.test.ts +206 -0
- package/src/__tests__/gitignore-check.test.ts +71 -0
- package/src/__tests__/gitignore-detector.test.ts +176 -0
- package/src/__tests__/gitignore-fixer.test.ts +317 -0
- package/src/__tests__/gitignore-resolver.test.ts +193 -0
- package/src/__tests__/gitignore-service.test.ts +137 -0
- package/src/__tests__/go-module.test.ts +43 -0
- package/src/__tests__/hook-import-policy.test.ts +90 -0
- package/src/__tests__/hook-process.test.ts +256 -0
- package/src/__tests__/hook-registration.test.ts +224 -0
- package/src/__tests__/install-command.test.ts +81 -0
- package/src/__tests__/install-plan.test.ts +104 -0
- package/src/__tests__/launcher-signal.test.ts +308 -0
- package/src/__tests__/local-marketplace-scan.test.ts +248 -0
- package/src/__tests__/manifest.test.ts +281 -0
- package/src/__tests__/markdown-renderer.test.ts +142 -0
- package/src/__tests__/marketplace-badge.test.ts +140 -0
- package/src/__tests__/marketplace-refresh.test.ts +340 -0
- package/src/__tests__/marketplace-version.test.ts +84 -0
- package/src/__tests__/marketplaces.test.ts +128 -0
- package/src/__tests__/mate-availability.test.ts +156 -0
- package/src/__tests__/mate-catalog.test.ts +295 -0
- package/src/__tests__/mcp-registry.test.ts +158 -0
- package/src/__tests__/model-visuals.test.tsx +2466 -0
- package/src/__tests__/models-adapter.test.ts +332 -0
- package/src/__tests__/models-cli.test.ts +273 -0
- package/src/__tests__/models-core.test.ts +802 -0
- package/src/__tests__/models-manager.test.ts +500 -0
- package/src/__tests__/models-presets-marketplace.test.ts +168 -0
- package/src/__tests__/models-screen-state.test.ts +315 -0
- package/src/__tests__/open-file.test.ts +59 -0
- package/src/__tests__/owned-marketplaces.test.ts +93 -0
- package/src/__tests__/plugin-checkout.test.ts +66 -0
- package/src/__tests__/plugin-cli-argv.test.ts +109 -0
- package/src/__tests__/plugin-contents.test.ts +371 -0
- package/src/__tests__/plugin-manager-fallback.test.ts +320 -0
- package/src/__tests__/plugin-releases.test.ts +98 -0
- package/src/__tests__/plugin-requires.test.ts +187 -0
- package/src/__tests__/plugin-row-text.test.ts +345 -0
- package/src/__tests__/plugin-setup.test.ts +1016 -0
- package/src/__tests__/profile-adopt.test.ts +177 -0
- package/src/__tests__/profile-command.test.ts +110 -0
- package/src/__tests__/profile-materializer.test.ts +238 -0
- package/src/__tests__/profile-sync.test.ts +154 -0
- package/src/__tests__/registry-version-resolution.test.ts +88 -0
- package/src/__tests__/resolve-executable.test.ts +42 -0
- package/src/__tests__/resolver.test.ts +351 -0
- package/src/__tests__/scope-action.test.ts +134 -0
- package/src/__tests__/scope-squares.test.tsx +164 -0
- package/src/__tests__/selection-scope.test.ts +100 -0
- package/src/__tests__/settings-file.test.ts +179 -0
- package/src/__tests__/shell-script-callers.test.ts +92 -0
- package/src/__tests__/skill-install-files.test.ts +136 -0
- package/src/__tests__/skills-catalog-client.test.ts +48 -0
- package/src/__tests__/style-wrap.test.ts +220 -0
- package/src/__tests__/styles-manager.test.ts +1098 -0
- package/src/__tests__/styles-origins.test.ts +414 -0
- package/src/__tests__/styles-screen-state.test.ts +457 -0
- package/src/__tests__/styles-status-line.test.ts +72 -0
- package/src/__tests__/styles-sync.test.ts +452 -0
- package/src/__tests__/symlink-manager.test.ts +180 -0
- package/src/__tests__/tabbar-layout.test.ts +100 -0
- package/src/__tests__/terminology-filler.test.ts +214 -0
- package/src/__tests__/theme-adaptive-colors.test.ts +354 -0
- package/src/__tests__/theme-env.test.ts +310 -0
- package/src/__tests__/theme-resolve.test.ts +380 -0
- package/src/__tests__/tool-probe.test.ts +1400 -0
- package/src/__tests__/toolchain.test.ts +232 -0
- package/src/__tests__/ui-version-writers.test.ts +88 -0
- package/src/__tests__/update-apply.test.ts +637 -0
- package/src/__tests__/update-plan.test.ts +1396 -0
- package/src/__tests__/uppercase-keybindings.test.ts +101 -0
- package/src/__tests__/useGitignoreModal.test.ts +79 -0
- package/src/__tests__/version-check-refresh.test.ts +45 -0
- package/src/__tests__/version-snapshot.test.ts +212 -0
- package/src/__tests__/version-unknown-state.test.ts +403 -0
- package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
- package/src/cli/ansi.ts +528 -0
- package/src/cli/bootstrap.ts +134 -0
- package/src/cli/doctor.ts +218 -0
- package/src/cli/hook.ts +129 -0
- package/src/cli/install.ts +382 -0
- package/src/cli/live.ts +374 -0
- package/src/cli/models.ts +297 -0
- package/src/cli/profile.ts +197 -0
- package/src/cli/prompt.ts +32 -0
- package/src/cli/router.ts +264 -0
- package/src/cli/update-view.ts +717 -0
- package/src/cli/update.ts +600 -0
- package/src/cli/upgrade.ts +97 -0
- package/src/data/alias-flags.ts +301 -0
- package/src/data/cli-tools.ts +146 -0
- package/src/data/community-styles.ts +531 -0
- package/src/data/gitignore-defaults.ts +33 -0
- package/src/data/gitignore-reasons.ts +93 -0
- package/src/data/gitignore-templates.ts +25 -0
- package/src/data/marketplaces.ts +278 -0
- package/src/data/mcp-servers.ts +51 -0
- package/src/data/models-presets.ts +329 -0
- package/src/data/predefined-profiles.ts +252 -0
- package/src/data/settings-catalog.ts +714 -0
- package/src/data/skill-repos.ts +91 -0
- package/src/data/styles/asd-ste100.md +38 -0
- package/src/data/styles/calibrated.md +24 -0
- package/src/data/styles/direct.md +23 -0
- package/src/data/styles/evidence-first.md +22 -0
- package/src/data/styles/explanatory.md +23 -0
- package/src/data/styles/index.ts +57 -0
- package/src/data/styles/no-slop.md +32 -0
- package/src/data/styles/plain-language.md +23 -0
- package/src/data/styles/structured.md +30 -0
- package/src/data/styles/terminology.md +28 -0
- package/src/data/styles/terse.md +20 -0
- package/src/main.tsx +110 -0
- package/src/markdown.d.ts +13 -0
- package/src/opentui.d.ts +203 -0
- package/src/services/active-closure.ts +117 -0
- package/src/services/alias-settings.ts +60 -0
- package/src/services/alias-shell-writer.ts +1244 -0
- package/src/services/alias-store.ts +172 -0
- package/src/services/binary-signing.ts +78 -0
- package/src/services/catalog-cache-store.ts +316 -0
- package/src/services/claude-cli.ts +304 -0
- package/src/services/claude-runner.ts +31 -0
- package/src/services/claude-settings.ts +1564 -0
- package/src/services/cli-tool-commands.ts +133 -0
- package/src/services/community-fetcher.ts +90 -0
- package/src/services/community-styles.ts +1194 -0
- package/src/services/content-drift.ts +208 -0
- package/src/services/conventions-manager.ts +945 -0
- package/src/services/doctor-bins.ts +63 -0
- package/src/services/doctor.ts +570 -0
- package/src/services/dotenv.ts +181 -0
- package/src/services/git-worktree.ts +129 -0
- package/src/services/github-budget.ts +274 -0
- package/src/services/gitignore-check.ts +63 -0
- package/src/services/gitignore-detector.ts +167 -0
- package/src/services/gitignore-fixer.ts +280 -0
- package/src/services/gitignore-resolver.ts +208 -0
- package/src/services/gitignore-service.ts +167 -0
- package/src/services/hook-registration.ts +218 -0
- package/src/services/install-plan.ts +107 -0
- package/src/services/local-marketplace.ts +657 -0
- package/src/services/manifest.ts +250 -0
- package/src/services/marketplace-catalog-git.ts +173 -0
- package/src/services/marketplace-catalog.ts +129 -0
- package/src/services/marketplace-fetcher.ts +400 -0
- package/src/services/marketplace-refresh.ts +245 -0
- package/src/services/marketplace-sync.ts +145 -0
- package/src/services/mate-availability.ts +133 -0
- package/src/services/mate-catalog.ts +265 -0
- package/src/services/mcp-registry.ts +171 -0
- package/src/services/models-core.ts +969 -0
- package/src/services/models-manager.ts +606 -0
- package/src/services/plugin-checkout.ts +186 -0
- package/src/services/plugin-manager.ts +847 -0
- package/src/services/plugin-mcp-config.ts +242 -0
- package/src/services/plugin-releases.ts +47 -0
- package/src/services/plugin-requires.ts +227 -0
- package/src/services/plugin-setup.ts +965 -0
- package/src/services/profile-adopt.ts +206 -0
- package/src/services/profile-materializer.ts +133 -0
- package/src/services/profile-sync.ts +158 -0
- package/src/services/profiles.ts +225 -0
- package/src/services/resolver.ts +377 -0
- package/src/services/settings-file.ts +69 -0
- package/src/services/settings-manager.ts +307 -0
- package/src/services/skills-manager.ts +1111 -0
- package/src/services/skillsmp-client.ts +193 -0
- package/src/services/styles-manager.ts +1294 -0
- package/src/services/symlink-manager.ts +189 -0
- package/src/services/terminology-filler.ts +266 -0
- package/src/services/tool-probe.ts +1637 -0
- package/src/services/toolchain.ts +431 -0
- package/src/services/update-engine.ts +691 -0
- package/src/services/update-plan.ts +820 -0
- package/src/services/version-check.ts +127 -0
- package/src/services/version-snapshot.ts +189 -0
- package/src/tui.tsx +112 -0
- package/src/types/bun.d.ts +27 -0
- package/src/types/gitignore.ts +68 -0
- package/src/types/index.ts +447 -0
- package/src/ui/App.tsx +535 -0
- package/src/ui/adapters/catalogNotice.ts +122 -0
- package/src/ui/adapters/cliToolsAdapter.ts +176 -0
- package/src/ui/adapters/modelsAdapter.ts +213 -0
- package/src/ui/adapters/pluginsAdapter.ts +413 -0
- package/src/ui/adapters/settingsAdapter.ts +168 -0
- package/src/ui/adapters/skillsAdapter.ts +229 -0
- package/src/ui/adapters/stylesAdapter.ts +403 -0
- package/src/ui/components/CategoryHeader.tsx +64 -0
- package/src/ui/components/EmptyFilterState.tsx +32 -0
- package/src/ui/components/FlagDetailEditor.tsx +0 -0
- package/src/ui/components/ScopeIndicator.tsx +63 -0
- package/src/ui/components/ScrollableList.tsx +95 -0
- package/src/ui/components/SearchInput.tsx +57 -0
- package/src/ui/components/StyledText.tsx +75 -0
- package/src/ui/components/TabBar.tsx +101 -0
- package/src/ui/components/layout/FooterHints.tsx +78 -0
- package/src/ui/components/layout/Panel.tsx +63 -0
- package/src/ui/components/layout/ProgressBar.tsx +44 -0
- package/src/ui/components/layout/ScopeTabs.tsx +54 -0
- package/src/ui/components/layout/ScreenLayout.tsx +321 -0
- package/src/ui/components/layout/index.ts +9 -0
- package/src/ui/components/modals/ConfirmModal.tsx +62 -0
- package/src/ui/components/modals/InputModal.tsx +75 -0
- package/src/ui/components/modals/LoadingModal.tsx +51 -0
- package/src/ui/components/modals/MessageModal.tsx +66 -0
- package/src/ui/components/modals/ModalContainer.tsx +159 -0
- package/src/ui/components/modals/SelectModal.tsx +90 -0
- package/src/ui/components/modals/index.ts +6 -0
- package/src/ui/components/primitives/ActionHints.tsx +44 -0
- package/src/ui/components/primitives/DetailSection.tsx +23 -0
- package/src/ui/components/primitives/KeyValueLine.tsx +26 -0
- package/src/ui/components/primitives/ListCategoryRow.tsx +45 -0
- package/src/ui/components/primitives/MetaText.tsx +40 -0
- package/src/ui/components/primitives/ScopeDetail.tsx +67 -0
- package/src/ui/components/primitives/ScopeSquares.tsx +58 -0
- package/src/ui/components/primitives/SelectableRow.tsx +30 -0
- package/src/ui/components/primitives/index.ts +9 -0
- package/src/ui/hooks/index.ts +2 -0
- package/src/ui/hooks/useGitignoreModal.ts +95 -0
- package/src/ui/hooks/useKeyboard.ts +24 -0
- package/src/ui/hooks/useKeyboardHandler.ts +63 -0
- package/src/ui/registry.ts +33 -0
- package/src/ui/renderers/cliToolRenderers.tsx +326 -0
- package/src/ui/renderers/gitignoreRenderers.tsx +80 -0
- package/src/ui/renderers/markdownRenderer.tsx +302 -0
- package/src/ui/renderers/mcpRenderers.tsx +149 -0
- package/src/ui/renderers/modelRenderers.tsx +1309 -0
- package/src/ui/renderers/modelVisuals.tsx +1402 -0
- package/src/ui/renderers/pluginRenderers.tsx +1089 -0
- package/src/ui/renderers/pluginRowText.ts +70 -0
- package/src/ui/renderers/pluginStatusText.ts +56 -0
- package/src/ui/renderers/profileRenderers.tsx +406 -0
- package/src/ui/renderers/settingsRenderers.tsx +218 -0
- package/src/ui/renderers/skillRenderers.tsx +479 -0
- package/src/ui/renderers/styleRenderers.tsx +813 -0
- package/src/ui/screens/AliasScreen.tsx +1558 -0
- package/src/ui/screens/CliToolsScreen.tsx +367 -0
- package/src/ui/screens/EnvVarsScreen.tsx +221 -0
- package/src/ui/screens/GitignoreScreen.tsx +562 -0
- package/src/ui/screens/McpRegistryScreen.tsx +366 -0
- package/src/ui/screens/McpScreen.tsx +256 -0
- package/src/ui/screens/ModelsScreen.tsx +542 -0
- package/src/ui/screens/PluginsScreen.tsx +1423 -0
- package/src/ui/screens/ProfilesScreen.tsx +438 -0
- package/src/ui/screens/SkillsScreen.tsx +853 -0
- package/src/ui/screens/StylesScreen.tsx +1064 -0
- package/src/ui/screens/index.ts +11 -0
- package/src/ui/state/AppContext.tsx +233 -0
- package/src/ui/state/DimensionsContext.tsx +154 -0
- package/src/ui/state/reducer.ts +810 -0
- package/src/ui/state/types.ts +503 -0
- package/src/ui/theme-mode.ts +440 -0
- package/src/ui/theme-resolve.ts +147 -0
- package/src/ui/theme.ts +175 -0
- package/src/utils/clipboard.ts +58 -0
- package/src/utils/command-utils.ts +149 -0
- package/src/utils/config-dir.ts +45 -0
- package/src/utils/file-locking.ts +146 -0
- package/src/utils/fuzzy-search.ts +138 -0
- package/src/utils/go-module.ts +36 -0
- package/src/utils/open-file.ts +84 -0
- package/src/utils/run.ts +67 -0
- package/src/utils/string-utils.ts +88 -0
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for the `magus update` APPLY path.
|
|
3
|
+
*
|
|
4
|
+
* Every defect a multi-model review found in this feature lived here, in the
|
|
5
|
+
* half that had no coverage, while the pure planner beside it had forty tests.
|
|
6
|
+
* These lock the apply path's contract so that gap cannot silently reopen.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
10
|
+
import type { PluginScope } from "../services/claude-cli.js";
|
|
11
|
+
import type { ToolFacts } from "../services/tool-probe.js";
|
|
12
|
+
import {
|
|
13
|
+
type ApplyReporter,
|
|
14
|
+
type PluginApplyDeps,
|
|
15
|
+
applyBins,
|
|
16
|
+
applyPlan,
|
|
17
|
+
applyPlugins,
|
|
18
|
+
installPluginInScope,
|
|
19
|
+
updatePluginInScope,
|
|
20
|
+
} from "../services/update-engine.js";
|
|
21
|
+
import type {
|
|
22
|
+
BinUpdateItem,
|
|
23
|
+
PluginUpdateItem,
|
|
24
|
+
UpdatePlan,
|
|
25
|
+
} from "../services/update-plan.js";
|
|
26
|
+
import type { ResolvedBin } from "../types/index.js";
|
|
27
|
+
import { noMoveReasons } from "../ui/adapters/pluginsAdapter.js";
|
|
28
|
+
|
|
29
|
+
const PROJECT = "/tmp/project";
|
|
30
|
+
|
|
31
|
+
/** Records every call, and lets a test dictate what the registry reads back. */
|
|
32
|
+
function stubDeps(
|
|
33
|
+
installedAfter: string | null | ((scope: PluginScope) => string | null),
|
|
34
|
+
) {
|
|
35
|
+
const saved: Array<{ id: string; version: string; scope: PluginScope }> = [];
|
|
36
|
+
const installed: Array<{ id: string; scope: PluginScope }> = [];
|
|
37
|
+
const updated: Array<{ id: string; scope: PluginScope }> = [];
|
|
38
|
+
const repaired: Array<{ id: string; scope: PluginScope }> = [];
|
|
39
|
+
|
|
40
|
+
const deps: PluginApplyDeps = {
|
|
41
|
+
install: mock(async (id: string, scope: PluginScope) => {
|
|
42
|
+
installed.push({ id, scope });
|
|
43
|
+
}),
|
|
44
|
+
update: mock(async (id: string, scope: PluginScope) => {
|
|
45
|
+
updated.push({ id, scope });
|
|
46
|
+
}),
|
|
47
|
+
repair: mock(async (id: string, scope: PluginScope) => {
|
|
48
|
+
repaired.push({ id, scope });
|
|
49
|
+
}),
|
|
50
|
+
readInstalled: mock(async (_id: string, scope: PluginScope) =>
|
|
51
|
+
typeof installedAfter === "function"
|
|
52
|
+
? installedAfter(scope)
|
|
53
|
+
: installedAfter,
|
|
54
|
+
),
|
|
55
|
+
saveInstalled: mock(
|
|
56
|
+
async (id: string, version: string, scope: PluginScope) => {
|
|
57
|
+
saved.push({ id, version, scope });
|
|
58
|
+
},
|
|
59
|
+
),
|
|
60
|
+
};
|
|
61
|
+
return { deps, saved, installed, updated, repaired };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function item(over: Partial<PluginUpdateItem>): PluginUpdateItem {
|
|
65
|
+
return {
|
|
66
|
+
pluginId: "dev@magus",
|
|
67
|
+
pinned: "latest",
|
|
68
|
+
installed: "1.0.0",
|
|
69
|
+
available: "2.0.0",
|
|
70
|
+
target: "2.0.0",
|
|
71
|
+
action: "update",
|
|
72
|
+
reason: "behind",
|
|
73
|
+
scopes: ["project"],
|
|
74
|
+
beforeVersions: { project: "1.0.0" },
|
|
75
|
+
...over,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let logs: string[];
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
logs = [];
|
|
82
|
+
console.log = ((...a: unknown[]) => logs.push(a.join(" "))) as any;
|
|
83
|
+
console.warn = ((...a: unknown[]) => logs.push(a.join(" "))) as any;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("what gets RECORDED is what got installed", () => {
|
|
87
|
+
test("records the version read back, not the version we asked for", async () => {
|
|
88
|
+
// `claude plugin install` accepts no version — it installs the newest.
|
|
89
|
+
// Recording `target` instead of the read-back made the registry claim a
|
|
90
|
+
// version that was never on disk.
|
|
91
|
+
const { deps, saved } = stubDeps("9.9.9");
|
|
92
|
+
await applyPlugins([item({ target: "2.0.0" })], PROJECT, deps);
|
|
93
|
+
expect(saved).toEqual([
|
|
94
|
+
{ id: "dev@magus", version: "9.9.9", scope: "project" },
|
|
95
|
+
]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("an exact pin the installer could not honour is reported, not hidden", async () => {
|
|
99
|
+
const { deps, saved } = stubDeps("9.9.9");
|
|
100
|
+
const result = await applyPlugins(
|
|
101
|
+
[item({ pinned: "4.6.1", target: "4.6.1" })],
|
|
102
|
+
PROJECT,
|
|
103
|
+
deps,
|
|
104
|
+
);
|
|
105
|
+
// The pin is NOT written as if satisfied...
|
|
106
|
+
expect(saved).toEqual([
|
|
107
|
+
{ id: "dev@magus", version: "9.9.9", scope: "project" },
|
|
108
|
+
]);
|
|
109
|
+
// ...and the shortfall is surfaced to the caller.
|
|
110
|
+
expect(result.mismatches).toEqual([
|
|
111
|
+
{
|
|
112
|
+
pluginId: "dev@magus",
|
|
113
|
+
pinned: "4.6.1",
|
|
114
|
+
actual: "9.9.9",
|
|
115
|
+
scope: "project",
|
|
116
|
+
},
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("a satisfied exact pin produces no mismatch", async () => {
|
|
121
|
+
const { deps } = stubDeps("4.6.1");
|
|
122
|
+
const result = await applyPlugins(
|
|
123
|
+
[item({ pinned: "4.6.1", target: "4.6.1" })],
|
|
124
|
+
PROJECT,
|
|
125
|
+
deps,
|
|
126
|
+
);
|
|
127
|
+
expect(result.mismatches).toEqual([]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("a 'latest' pin never counts as a mismatch", async () => {
|
|
131
|
+
const { deps } = stubDeps("9.9.9");
|
|
132
|
+
const result = await applyPlugins(
|
|
133
|
+
[item({ pinned: "latest", target: "2.0.0" })],
|
|
134
|
+
PROJECT,
|
|
135
|
+
deps,
|
|
136
|
+
);
|
|
137
|
+
expect(result.mismatches).toEqual([]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("nothing is recorded when the registry has no version to read back", async () => {
|
|
141
|
+
// Writing a guess here would be the same fabrication in another costume.
|
|
142
|
+
const { deps, saved } = stubDeps(null);
|
|
143
|
+
await applyPlugins([item({})], PROJECT, deps);
|
|
144
|
+
expect(saved).toEqual([]);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("scope handling", () => {
|
|
149
|
+
test("a fresh install goes to project scope only", async () => {
|
|
150
|
+
const { deps, installed } = stubDeps("2.0.0");
|
|
151
|
+
await applyPlugins(
|
|
152
|
+
[item({ action: "install", installed: null, scopes: [] })],
|
|
153
|
+
PROJECT,
|
|
154
|
+
deps,
|
|
155
|
+
);
|
|
156
|
+
expect(installed).toEqual([{ id: "dev@magus", scope: "project" }]);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Three actions, three Claude Code commands, each a silent no-op in the
|
|
160
|
+
// other two cases. Folding `install` and `update` together is what made
|
|
161
|
+
// every magus update path exit 0 having changed nothing: measured on
|
|
162
|
+
// Claude Code 2.1.252, `claude plugin install designer@magus --scope
|
|
163
|
+
// project` answers "already installed" and leaves 0.6.0 in place while
|
|
164
|
+
// 0.6.1 is published, and `claude plugin update` is the command that moves
|
|
165
|
+
// it. The reverse holds too — `update` on a plugin absent from the scope
|
|
166
|
+
// answers "already at the latest version" and installs nothing — so this
|
|
167
|
+
// asserts BOTH directions, not just the one that was broken.
|
|
168
|
+
|
|
169
|
+
test("an update runs `update`, never `install`", async () => {
|
|
170
|
+
const { deps, installed, updated } = stubDeps("2.0.0");
|
|
171
|
+
await applyPlugins([item({ action: "update" })], PROJECT, deps);
|
|
172
|
+
expect(updated).toEqual([{ id: "dev@magus", scope: "project" }]);
|
|
173
|
+
expect(installed).toEqual([]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("an update with NO row for this project installs instead", async () => {
|
|
177
|
+
// `claude plugin update --scope project` resolves the project from the
|
|
178
|
+
// working directory, and with no row there it updates some OTHER
|
|
179
|
+
// project's row instead. Measured on 2.1.252 from a directory holding no
|
|
180
|
+
// madbench row, it moved /Users/jack/mag/tmux-mcp — a project nobody
|
|
181
|
+
// named. Live for git worktrees, which inherit the parent repo's version
|
|
182
|
+
// while owning no row of their own.
|
|
183
|
+
const { deps, installed, updated } = stubDeps(null);
|
|
184
|
+
await applyPlugins([item({ action: "update" })], PROJECT, deps);
|
|
185
|
+
expect(installed).toEqual([{ id: "dev@magus", scope: "project" }]);
|
|
186
|
+
expect(updated).toEqual([]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("the row check is per scope, not per plugin", async () => {
|
|
190
|
+
// user scope holds a row, project scope does not: one must update and
|
|
191
|
+
// the other install, in the same pass.
|
|
192
|
+
const { deps, installed, updated } = stubDeps((scope) =>
|
|
193
|
+
scope === "user" ? "1.5.0" : null,
|
|
194
|
+
);
|
|
195
|
+
await applyPlugins([item({ scopes: ["user", "project"] })], PROJECT, deps);
|
|
196
|
+
expect(updated).toEqual([{ id: "dev@magus", scope: "user" }]);
|
|
197
|
+
expect(installed).toEqual([{ id: "dev@magus", scope: "project" }]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("a fresh install runs `install`, never `update`", async () => {
|
|
201
|
+
const { deps, installed, updated } = stubDeps("2.0.0");
|
|
202
|
+
await applyPlugins(
|
|
203
|
+
[item({ action: "install", installed: null, scopes: [] })],
|
|
204
|
+
PROJECT,
|
|
205
|
+
deps,
|
|
206
|
+
);
|
|
207
|
+
expect(installed).toEqual([{ id: "dev@magus", scope: "project" }]);
|
|
208
|
+
expect(updated).toEqual([]);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("a repair runs neither — content drift needs uninstall+install", async () => {
|
|
212
|
+
const { deps, installed, updated, repaired } = stubDeps("1.0.0");
|
|
213
|
+
await applyPlugins(
|
|
214
|
+
[item({ action: "repair", target: "1.0.0", scopes: ["project"] })],
|
|
215
|
+
PROJECT,
|
|
216
|
+
deps,
|
|
217
|
+
);
|
|
218
|
+
expect(repaired).toEqual([{ id: "dev@magus", scope: "project" }]);
|
|
219
|
+
expect(installed).toEqual([]);
|
|
220
|
+
expect(updated).toEqual([]);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("every outdated scope is updated AND recorded separately", async () => {
|
|
224
|
+
const { deps, updated, saved } = stubDeps((scope) =>
|
|
225
|
+
scope === "user" ? "2.0.0" : "1.5.0",
|
|
226
|
+
);
|
|
227
|
+
await applyPlugins([item({ scopes: ["user", "project"] })], PROJECT, deps);
|
|
228
|
+
expect(updated).toEqual([
|
|
229
|
+
{ id: "dev@magus", scope: "user" },
|
|
230
|
+
{ id: "dev@magus", scope: "project" },
|
|
231
|
+
]);
|
|
232
|
+
// Each scope records ITS OWN read-back, never one shared value.
|
|
233
|
+
expect(saved).toEqual([
|
|
234
|
+
{ id: "dev@magus", version: "2.0.0", scope: "user" },
|
|
235
|
+
{ id: "dev@magus", version: "1.5.0", scope: "project" },
|
|
236
|
+
]);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("a repair uses repairPlugin, not a plain install", async () => {
|
|
240
|
+
// A content-stale plugin has no version bump, so `install` is a no-op that
|
|
241
|
+
// reports success — only uninstall+install refreshes the files.
|
|
242
|
+
const { deps, repaired, updated } = stubDeps("1.0.0");
|
|
243
|
+
await applyPlugins([item({ action: "repair" })], PROJECT, deps);
|
|
244
|
+
expect(repaired).toEqual([{ id: "dev@magus", scope: "project" }]);
|
|
245
|
+
expect(updated).toEqual([]);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe("what is skipped", () => {
|
|
250
|
+
test("'current' and 'unknown' items are never touched", async () => {
|
|
251
|
+
const { deps, updated, saved } = stubDeps("1.0.0");
|
|
252
|
+
await applyPlugins(
|
|
253
|
+
[
|
|
254
|
+
item({ action: "current", scopes: [] }),
|
|
255
|
+
item({ pluginId: "b@magus", action: "unknown", scopes: [] }),
|
|
256
|
+
],
|
|
257
|
+
PROJECT,
|
|
258
|
+
deps,
|
|
259
|
+
);
|
|
260
|
+
expect(updated).toEqual([]);
|
|
261
|
+
expect(saved).toEqual([]);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("an 'unknown' plugin is not installed on a guess", async () => {
|
|
265
|
+
// It is unknown because the catalog could not be reached. Acting on that
|
|
266
|
+
// would be picking a version at random.
|
|
267
|
+
const { deps, updated } = stubDeps("1.0.0");
|
|
268
|
+
await applyPlugins(
|
|
269
|
+
[item({ action: "unknown", target: null, scopes: ["project"] })],
|
|
270
|
+
PROJECT,
|
|
271
|
+
deps,
|
|
272
|
+
);
|
|
273
|
+
expect(updated).toEqual([]);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe("failure handling", () => {
|
|
278
|
+
test("a failing plugin is reported, and the rest still run", async () => {
|
|
279
|
+
const { deps, updated } = stubDeps("2.0.0");
|
|
280
|
+
deps.update = mock(async (id: string, scope: PluginScope) => {
|
|
281
|
+
if (id === "bad@magus") throw new Error("network down");
|
|
282
|
+
updated.push({ id, scope });
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const result = await applyPlugins(
|
|
286
|
+
[item({ pluginId: "bad@magus" }), item({ pluginId: "good@magus" })],
|
|
287
|
+
PROJECT,
|
|
288
|
+
deps,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
expect(result.failed).toEqual(["bad@magus"]);
|
|
292
|
+
expect(result.ok).toBe(1);
|
|
293
|
+
expect(updated).toEqual([{ id: "good@magus", scope: "project" }]);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("a scope that threw records nothing for that plugin", async () => {
|
|
297
|
+
// Half-applied state must not be recorded as if it succeeded.
|
|
298
|
+
const { deps, saved } = stubDeps("2.0.0");
|
|
299
|
+
deps.update = mock(async () => {
|
|
300
|
+
throw new Error("boom");
|
|
301
|
+
});
|
|
302
|
+
const result = await applyPlugins([item({})], PROJECT, deps);
|
|
303
|
+
expect(saved).toEqual([]);
|
|
304
|
+
expect(result.ok).toBe(0);
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
describe("installPluginInScope — a fresh install records what landed", () => {
|
|
309
|
+
// The TUI's install branches called the CLI directly and then wrote the
|
|
310
|
+
// version they had ASKED for. A marketplace serves whatever it publishes at
|
|
311
|
+
// the moment of the call, so that number was a guess — and `install --check`
|
|
312
|
+
// reads back the same field, so it compared the guess against a copy of
|
|
313
|
+
// itself and reported clean forever. Routing installs through the read-back
|
|
314
|
+
// the update branches already used is what closes it.
|
|
315
|
+
//
|
|
316
|
+
// The read-back semantics are pinned in cli-apply-seams.test.ts
|
|
317
|
+
// ("installPluginInScope — the scope-targeted path"). What these add is the
|
|
318
|
+
// property that makes the whole class of bug unrepresentable rather than
|
|
319
|
+
// merely absent right now.
|
|
320
|
+
|
|
321
|
+
test("takes no version parameter, so a caller cannot supply a wrong one", () => {
|
|
322
|
+
// Arity, not behaviour — and that is the point. Re-adding a `version`
|
|
323
|
+
// argument would reopen the defect without failing any behavioural test,
|
|
324
|
+
// because the new parameter would simply be believed.
|
|
325
|
+
// (pluginId, scope, projectPath, deps = REAL_PLUGIN_DEPS) — the defaulted
|
|
326
|
+
// parameter is not counted by Function.length.
|
|
327
|
+
expect(installPluginInScope.length).toBe(3);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("saves the version read back, not one the caller chose", async () => {
|
|
331
|
+
const { deps, saved } = stubDeps("9.9.9");
|
|
332
|
+
await installPluginInScope("dev@magus", "user", PROJECT, deps);
|
|
333
|
+
expect(saved).toEqual([
|
|
334
|
+
{ id: "dev@magus", version: "9.9.9", scope: "user" },
|
|
335
|
+
]);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("an unreadable version writes nothing at all", async () => {
|
|
339
|
+
// Never guess. A missing record is recoverable; a wrong one is not,
|
|
340
|
+
// because nothing downstream can tell it from a true one.
|
|
341
|
+
const { deps, saved } = stubDeps(null);
|
|
342
|
+
expect(
|
|
343
|
+
await installPluginInScope("dev@magus", "user", PROJECT, deps),
|
|
344
|
+
).toBeNull();
|
|
345
|
+
expect(saved).toEqual([]);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// The scope menu's update branch went through the install helper for as
|
|
349
|
+
// long as both issued `claude plugin install`. They no longer do, so the
|
|
350
|
+
// two helpers must reach different commands or the menu silently no-ops
|
|
351
|
+
// exactly the way "update all" did.
|
|
352
|
+
test("the install helper runs `install`, the update helper runs `update`", async () => {
|
|
353
|
+
const a = stubDeps("2.0.0");
|
|
354
|
+
await installPluginInScope("dev@magus", "user", PROJECT, a.deps);
|
|
355
|
+
expect(a.installed).toEqual([{ id: "dev@magus", scope: "user" }]);
|
|
356
|
+
expect(a.updated).toEqual([]);
|
|
357
|
+
|
|
358
|
+
const b = stubDeps("2.0.0");
|
|
359
|
+
await updatePluginInScope("dev@magus", "user", PROJECT, b.deps);
|
|
360
|
+
expect(b.updated).toEqual([{ id: "dev@magus", scope: "user" }]);
|
|
361
|
+
expect(b.installed).toEqual([]);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("the update helper takes no version parameter either", () => {
|
|
365
|
+
expect(updatePluginInScope.length).toBe(3);
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// ── R6: an installer that exits 0 and changes nothing is never "changed" ────
|
|
370
|
+
//
|
|
371
|
+
// Planning (R4) refuses the upgrades it cannot justify, but it cannot see the
|
|
372
|
+
// no-ops that only appear after an installer runs: a stale brew tap, a PATH
|
|
373
|
+
// that still resolves an older copy, `~/.bun/bin` missing from PATH, a
|
|
374
|
+
// marketplace clone lagging its catalog. Each exits 0 and is replanned
|
|
375
|
+
// identically next run. Read-back — re-probing the one tool, or re-reading the
|
|
376
|
+
// registry row — is what tells them apart. No installer output is parsed.
|
|
377
|
+
|
|
378
|
+
const bin = (over: Partial<BinUpdateItem> = {}): BinUpdateItem => ({
|
|
379
|
+
name: "tmux",
|
|
380
|
+
action: "upgrade",
|
|
381
|
+
reason: "behind",
|
|
382
|
+
installed: "1.2.2",
|
|
383
|
+
latest: "1.2.3",
|
|
384
|
+
target: "1.2.3",
|
|
385
|
+
sources: [],
|
|
386
|
+
...over,
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const readBack = (over: Partial<ToolFacts> = {}): ToolFacts => ({
|
|
390
|
+
name: "tmux",
|
|
391
|
+
present: true,
|
|
392
|
+
resolvedPath: "/opt/homebrew/Cellar/tmux/1.2.3/bin/tmux",
|
|
393
|
+
installed: "1.2.3",
|
|
394
|
+
latest: null,
|
|
395
|
+
behind: false,
|
|
396
|
+
managed: true,
|
|
397
|
+
sources: [],
|
|
398
|
+
...over,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
function binPlan(item: BinUpdateItem): UpdatePlan {
|
|
402
|
+
const spec: ResolvedBin = { name: item.name, via: "brew", sources: [] };
|
|
403
|
+
return {
|
|
404
|
+
profileId: "test",
|
|
405
|
+
plugins: [],
|
|
406
|
+
bins: [item],
|
|
407
|
+
skills: [],
|
|
408
|
+
binSpecs: new Map([[item.name, spec]]),
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function recordingReporter() {
|
|
413
|
+
const rows: Array<{ ok: boolean; name: string; detail: string }> = [];
|
|
414
|
+
const reporter: ApplyReporter = {
|
|
415
|
+
begin: () => {},
|
|
416
|
+
finish: (ok, name, detail) => rows.push({ ok, name, detail }),
|
|
417
|
+
detach: (work) => work(),
|
|
418
|
+
};
|
|
419
|
+
return { reporter, rows };
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const RUN_OK = async () => true;
|
|
423
|
+
|
|
424
|
+
describe("applyBins — read-back", () => {
|
|
425
|
+
test("an upgrade that reads back the same version is UNCHANGED (v-prefix tolerant)", async () => {
|
|
426
|
+
const { reporter, rows } = recordingReporter();
|
|
427
|
+
const result = await applyBins(
|
|
428
|
+
binPlan(bin({ installed: "v1.2.3", target: "1.2.4" })),
|
|
429
|
+
reporter,
|
|
430
|
+
RUN_OK,
|
|
431
|
+
async () => readBack({ installed: "1.2.3" }),
|
|
432
|
+
);
|
|
433
|
+
expect(result).toEqual({ ok: 0, failed: [], unchanged: ["tmux"] });
|
|
434
|
+
// A finished row, not a failure, carrying the reason.
|
|
435
|
+
expect(rows).toEqual([
|
|
436
|
+
{
|
|
437
|
+
ok: true,
|
|
438
|
+
name: "tmux",
|
|
439
|
+
detail: "no change — the installer exited 0 but tmux is still 1.2.3",
|
|
440
|
+
},
|
|
441
|
+
]);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test("a read-back where PATH still runs another copy is UNCHANGED", async () => {
|
|
445
|
+
const result = await applyBins(
|
|
446
|
+
binPlan(bin()),
|
|
447
|
+
undefined,
|
|
448
|
+
RUN_OK,
|
|
449
|
+
async () =>
|
|
450
|
+
readBack({
|
|
451
|
+
installed: "1.2.3",
|
|
452
|
+
managed: false,
|
|
453
|
+
resolvedPath: "/usr/local/bin/tmux",
|
|
454
|
+
}),
|
|
455
|
+
);
|
|
456
|
+
expect(result.unchanged).toEqual(["tmux"]);
|
|
457
|
+
expect(result.ok).toBe(0);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("an install that is still not on PATH is UNCHANGED", async () => {
|
|
461
|
+
const { reporter, rows } = recordingReporter();
|
|
462
|
+
const result = await applyBins(
|
|
463
|
+
binPlan(bin({ action: "install", installed: null, reason: "absent" })),
|
|
464
|
+
reporter,
|
|
465
|
+
RUN_OK,
|
|
466
|
+
async () => readBack({ present: false, installed: null, managed: null }),
|
|
467
|
+
);
|
|
468
|
+
expect(result.unchanged).toEqual(["tmux"]);
|
|
469
|
+
expect(rows[0]!.detail).toContain("still not on PATH");
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("a read-back that cannot read a version counts as CHANGED", async () => {
|
|
473
|
+
// Failing to read is not evidence nothing moved — otherwise every
|
|
474
|
+
// go-less machine's upgrade would turn into a warning.
|
|
475
|
+
const result = await applyBins(
|
|
476
|
+
binPlan(bin()),
|
|
477
|
+
undefined,
|
|
478
|
+
RUN_OK,
|
|
479
|
+
async () => readBack({ installed: null }),
|
|
480
|
+
);
|
|
481
|
+
expect(result).toEqual({ ok: 1, failed: [], unchanged: [] });
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("a read-back that throws counts as CHANGED, never as a failure", async () => {
|
|
485
|
+
const result = await applyBins(
|
|
486
|
+
binPlan(bin()),
|
|
487
|
+
undefined,
|
|
488
|
+
RUN_OK,
|
|
489
|
+
async () => {
|
|
490
|
+
throw new Error("probe blew up");
|
|
491
|
+
},
|
|
492
|
+
);
|
|
493
|
+
expect(result).toEqual({ ok: 1, failed: [], unchanged: [] });
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test("a real move is CHANGED", async () => {
|
|
497
|
+
const result = await applyBins(
|
|
498
|
+
binPlan(bin()),
|
|
499
|
+
undefined,
|
|
500
|
+
RUN_OK,
|
|
501
|
+
async () => readBack({ installed: "1.2.3" }),
|
|
502
|
+
);
|
|
503
|
+
expect(result).toEqual({ ok: 1, failed: [], unchanged: [] });
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test("an installer that fails is FAILED and is not read back", async () => {
|
|
507
|
+
const probe = mock(async () => readBack());
|
|
508
|
+
const result = await applyBins(
|
|
509
|
+
binPlan(bin()),
|
|
510
|
+
undefined,
|
|
511
|
+
async () => false,
|
|
512
|
+
probe,
|
|
513
|
+
);
|
|
514
|
+
expect(result).toEqual({ ok: 0, failed: ["tmux"], unchanged: [] });
|
|
515
|
+
expect(probe).not.toHaveBeenCalled();
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test("applyPlan: changed excludes the unchanged, which it lists", async () => {
|
|
519
|
+
const outcome = await applyPlan(
|
|
520
|
+
binPlan(bin({ installed: "1.2.2" })),
|
|
521
|
+
PROJECT,
|
|
522
|
+
{ run: RUN_OK, probe: async () => readBack({ installed: "1.2.2" }) },
|
|
523
|
+
);
|
|
524
|
+
expect(outcome.changed).toBe(0);
|
|
525
|
+
expect(outcome.unchanged).toEqual(["tmux"]);
|
|
526
|
+
expect(outcome.failed).toEqual([]);
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
describe("applyPlugins — read-back against beforeVersions", () => {
|
|
531
|
+
test("an update whose every scope reads back its before-version is UNCHANGED", async () => {
|
|
532
|
+
// `claude plugin update` answering "already at the latest version" while
|
|
533
|
+
// the marketplace clone lags the catalog: exit 0, nothing moved.
|
|
534
|
+
const { deps } = stubDeps("1.0.0");
|
|
535
|
+
const { reporter, rows } = recordingReporter();
|
|
536
|
+
const result = await applyPlugins(
|
|
537
|
+
[item({ beforeVersions: { project: "1.0.0" } })],
|
|
538
|
+
PROJECT,
|
|
539
|
+
deps,
|
|
540
|
+
reporter,
|
|
541
|
+
);
|
|
542
|
+
expect(result.ok).toBe(0);
|
|
543
|
+
expect(result.unchanged).toEqual(["dev@magus"]);
|
|
544
|
+
expect(result.failed).toEqual([]);
|
|
545
|
+
expect(rows[0]).toMatchObject({ ok: true });
|
|
546
|
+
expect(rows[0]!.detail).toContain("still 1.0.0");
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
test("an update where ONE scope moved is CHANGED", async () => {
|
|
550
|
+
const { deps } = stubDeps((scope) =>
|
|
551
|
+
scope === "user" ? "2.0.0" : "1.0.0",
|
|
552
|
+
);
|
|
553
|
+
const result = await applyPlugins(
|
|
554
|
+
[
|
|
555
|
+
item({
|
|
556
|
+
scopes: ["user", "project"],
|
|
557
|
+
beforeVersions: { user: "1.0.0", project: "1.0.0" },
|
|
558
|
+
}),
|
|
559
|
+
],
|
|
560
|
+
PROJECT,
|
|
561
|
+
deps,
|
|
562
|
+
);
|
|
563
|
+
expect(result.ok).toBe(1);
|
|
564
|
+
expect(result.unchanged).toEqual([]);
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
test("an install that reads back nothing in any scope is UNCHANGED", async () => {
|
|
568
|
+
const { deps } = stubDeps(null);
|
|
569
|
+
const result = await applyPlugins(
|
|
570
|
+
[
|
|
571
|
+
item({
|
|
572
|
+
action: "install",
|
|
573
|
+
installed: null,
|
|
574
|
+
scopes: [],
|
|
575
|
+
beforeVersions: {},
|
|
576
|
+
}),
|
|
577
|
+
],
|
|
578
|
+
PROJECT,
|
|
579
|
+
deps,
|
|
580
|
+
);
|
|
581
|
+
expect(result.unchanged).toEqual(["dev@magus"]);
|
|
582
|
+
expect(result.ok).toBe(0);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
test("an install that lands a version is CHANGED", async () => {
|
|
586
|
+
const { deps } = stubDeps("2.0.0");
|
|
587
|
+
const result = await applyPlugins(
|
|
588
|
+
[
|
|
589
|
+
item({
|
|
590
|
+
action: "install",
|
|
591
|
+
installed: null,
|
|
592
|
+
scopes: [],
|
|
593
|
+
beforeVersions: {},
|
|
594
|
+
}),
|
|
595
|
+
],
|
|
596
|
+
PROJECT,
|
|
597
|
+
deps,
|
|
598
|
+
);
|
|
599
|
+
expect(result.ok).toBe(1);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
test("a repair is always CHANGED — same version is its definition", async () => {
|
|
603
|
+
const { deps } = stubDeps("1.0.0");
|
|
604
|
+
const result = await applyPlugins(
|
|
605
|
+
[
|
|
606
|
+
item({
|
|
607
|
+
action: "repair",
|
|
608
|
+
target: "1.0.0",
|
|
609
|
+
beforeVersions: { project: "1.0.0" },
|
|
610
|
+
}),
|
|
611
|
+
],
|
|
612
|
+
PROJECT,
|
|
613
|
+
deps,
|
|
614
|
+
);
|
|
615
|
+
expect(result.ok).toBe(1);
|
|
616
|
+
expect(result.unchanged).toEqual([]);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
test("the TUI's collector names the same cause the CLI prints", async () => {
|
|
620
|
+
// The screen used to branch on `failed.length` alone: a no-op update
|
|
621
|
+
// closed with a success and a row that had not moved.
|
|
622
|
+
const { deps } = stubDeps("1.0.0");
|
|
623
|
+
const reasons = noMoveReasons();
|
|
624
|
+
const result = await applyPlugins(
|
|
625
|
+
[item({ beforeVersions: { project: "1.0.0" } })],
|
|
626
|
+
PROJECT,
|
|
627
|
+
deps,
|
|
628
|
+
reasons.reporter,
|
|
629
|
+
);
|
|
630
|
+
expect(result.unchanged).toEqual(["dev@magus"]);
|
|
631
|
+
expect(reasons.lines(result.unchanged)).toBe(
|
|
632
|
+
"dev@magus: the installer exited 0 but dev@magus is still 1.0.0",
|
|
633
|
+
);
|
|
634
|
+
// An item with no recorded cause still gets a line.
|
|
635
|
+
expect(reasons.lines(["other@magus"])).toBe("other@magus");
|
|
636
|
+
});
|
|
637
|
+
});
|