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,403 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
buildScopeStatus,
|
|
4
|
+
compareVersions,
|
|
5
|
+
isBehind,
|
|
6
|
+
isInstalledInScope,
|
|
7
|
+
resolveScopeAction,
|
|
8
|
+
} from "../services/plugin-manager.js";
|
|
9
|
+
import type { PluginInfo } from "../services/plugin-manager.js";
|
|
10
|
+
import { planPluginUpdates } from "../services/update-plan.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* REGRESSION: magus renders "I cannot tell" as "nothing there" in three
|
|
14
|
+
* separate places — Fixed in /dev:fix session dev-fix-20260910-magus.
|
|
15
|
+
*
|
|
16
|
+
* Measured before the fix, against the real function and real registry values:
|
|
17
|
+
*
|
|
18
|
+
* compareVersions("517b2fcd1b60", "b819188d2eea") = 0 hasUpdate=false
|
|
19
|
+
* compareVersions("7.2.0", "517b2fcd1b60") = 1 hasUpdate=true
|
|
20
|
+
*
|
|
21
|
+
* Two different git commits compared EQUAL, so a sha-versioned plugin could
|
|
22
|
+
* never report an update; and a sha compared against a semver reported a
|
|
23
|
+
* spurious one. `feature-dev@claude-plugins-official` is 517b2fcd1b60 in one
|
|
24
|
+
* project and b819188d2eea in another on the reporter's machine.
|
|
25
|
+
*
|
|
26
|
+
* The rule this file pins: an unorderable pair is `null` (unknown), never 0
|
|
27
|
+
* (equal) and never a direction.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const SHA_A = "517b2fcd1b60";
|
|
31
|
+
const SHA_B = "b819188d2eea";
|
|
32
|
+
|
|
33
|
+
describe("compareVersions — orderable pairs", () => {
|
|
34
|
+
test("semver ordering is unchanged", () => {
|
|
35
|
+
expect(compareVersions("7.2.0", "7.1.0")).toBe(1);
|
|
36
|
+
expect(compareVersions("7.1.0", "7.2.0")).toBe(-1);
|
|
37
|
+
expect(compareVersions("7.1.0", "7.1.0")).toBe(0);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("a leading v is still stripped", () => {
|
|
41
|
+
expect(compareVersions("v2.0.0", "1.9.9")).toBe(1);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("uneven part counts still compare", () => {
|
|
45
|
+
expect(compareVersions("1.2", "1.2.0")).toBe(0);
|
|
46
|
+
expect(compareVersions("1.2.1", "1.2")).toBe(1);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("compareVersions — unorderable pairs are null, not 0", () => {
|
|
51
|
+
test("two DIFFERENT commit shas are not equal", () => {
|
|
52
|
+
// The bug: this returned 0, so nothing downstream could see a difference.
|
|
53
|
+
expect(compareVersions(SHA_A, SHA_B)).toBeNull();
|
|
54
|
+
expect(compareVersions(SHA_B, SHA_A)).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("the same commit sha is genuinely equal", () => {
|
|
58
|
+
expect(compareVersions(SHA_A, SHA_A)).toBe(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("a sha against a semver is not orderable in either direction", () => {
|
|
62
|
+
// The bug: these returned 1 and -1, a fabricated ordering.
|
|
63
|
+
expect(compareVersions("7.2.0", SHA_A)).toBeNull();
|
|
64
|
+
expect(compareVersions(SHA_A, "7.2.0")).toBeNull();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("a missing side is unknown, not equal", () => {
|
|
68
|
+
// The bug: `if (!a || !b) return 0` claimed two absent versions matched.
|
|
69
|
+
expect(compareVersions(undefined, "1.0.0")).toBeNull();
|
|
70
|
+
expect(compareVersions("1.0.0", null)).toBeNull();
|
|
71
|
+
expect(compareVersions(undefined, undefined)).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("the 0.0.0 sentinel is comparable — official plugins record it", () => {
|
|
75
|
+
expect(compareVersions("0.0.0", "0.0.0")).toBe(0);
|
|
76
|
+
expect(compareVersions("1.0.0", "0.0.0")).toBe(1);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("isBehind — the question callers actually ask", () => {
|
|
81
|
+
test("semver behind, ahead, level", () => {
|
|
82
|
+
expect(isBehind("7.2.0", "7.1.0")).toBe(true);
|
|
83
|
+
expect(isBehind("7.1.0", "7.2.0")).toBe(false);
|
|
84
|
+
expect(isBehind("7.1.0", "7.1.0")).toBe(false);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("a different published sha means the install is not current", () => {
|
|
88
|
+
// The catalog is authoritative for what is published NOW. An installed
|
|
89
|
+
// commit that differs from it is not current, even though the two shas
|
|
90
|
+
// carry no ordering between them.
|
|
91
|
+
expect(isBehind(SHA_B, SHA_A)).toBe(true);
|
|
92
|
+
expect(isBehind(SHA_A, SHA_A)).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("a sha against a semver is unknown, never a silent false", () => {
|
|
96
|
+
expect(isBehind("7.2.0", SHA_A)).toBeNull();
|
|
97
|
+
expect(isBehind(SHA_A, "7.2.0")).toBeNull();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("not installed is unknown, not up to date", () => {
|
|
101
|
+
// The bug: `installedVersion && compare(...)` yielded false, so a plugin
|
|
102
|
+
// that was not installed at all was excluded from the update count and
|
|
103
|
+
// read as up to date.
|
|
104
|
+
expect(isBehind("7.2.0", undefined)).toBeNull();
|
|
105
|
+
expect(isBehind("7.2.0", null)).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("buildScopeStatus — installation is a registry fact, not a settings key", () => {
|
|
110
|
+
test("an installed but NOT enabled plugin still reports its scope", () => {
|
|
111
|
+
// The bug: scope was `enabledPlugins[id] !== undefined ? {...} : undefined`,
|
|
112
|
+
// so a plugin with a real registry row but no settings key rendered
|
|
113
|
+
// identically to one that was never installed.
|
|
114
|
+
const scope = buildScopeStatus({}, { "dev@magus": "7.2.0" }, "dev@magus");
|
|
115
|
+
expect(scope).toBeDefined();
|
|
116
|
+
expect(scope?.version).toBe("7.2.0");
|
|
117
|
+
expect(scope?.enabled).toBe(false);
|
|
118
|
+
expect(isInstalledInScope(scope)).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("enabled with no install is still reported, as a broken state", () => {
|
|
122
|
+
const scope = buildScopeStatus({ "dev@magus": true }, {}, "dev@magus");
|
|
123
|
+
expect(scope).toBeDefined();
|
|
124
|
+
expect(scope?.enabled).toBe(true);
|
|
125
|
+
expect(scope?.version).toBeUndefined();
|
|
126
|
+
expect(isInstalledInScope(scope)).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("neither enabled nor installed has no scope at all", () => {
|
|
130
|
+
expect(buildScopeStatus({}, {}, "dev@magus")).toBeUndefined();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("isInstalledInScope — presence of files, not permission to load", () => {
|
|
135
|
+
test("installed and disabled is still installed", () => {
|
|
136
|
+
// The old rule was `!!scope?.enabled && !!scope.version`, which made
|
|
137
|
+
// disabling a plugin look like uninstalling it.
|
|
138
|
+
expect(isInstalledInScope({ enabled: false, version: "7.2.0" })).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("the 0.0.0 sentinel counts as installed", () => {
|
|
142
|
+
expect(isInstalledInScope({ enabled: true, version: "0.0.0" })).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("enabled with no version is not installed", () => {
|
|
146
|
+
expect(isInstalledInScope({ enabled: true, version: undefined })).toBe(
|
|
147
|
+
false,
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const info = (over: Partial<PluginInfo>): PluginInfo =>
|
|
153
|
+
({
|
|
154
|
+
id: "x@magus",
|
|
155
|
+
name: "x",
|
|
156
|
+
version: "1.0.0",
|
|
157
|
+
description: "",
|
|
158
|
+
marketplace: "magus",
|
|
159
|
+
marketplaceDisplay: "Magus",
|
|
160
|
+
enabled: true,
|
|
161
|
+
...over,
|
|
162
|
+
}) as PluginInfo;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* A catalog that HAS been read for @magus — it lists other magus plugins. That
|
|
166
|
+
* distinction is load-bearing: an empty catalog means "this marketplace was
|
|
167
|
+
* never read", where absence proves nothing, and installing is what registers
|
|
168
|
+
* it. Only a catalog we demonstrably hold entries from can turn absence into
|
|
169
|
+
* the answer "that plugin is not published".
|
|
170
|
+
*/
|
|
171
|
+
const magusCatalog = (...extra: PluginInfo[]) =>
|
|
172
|
+
new Map(
|
|
173
|
+
[
|
|
174
|
+
info({ id: "dev@magus", version: "7.2.0" }),
|
|
175
|
+
info({ id: "terminal@magus", version: "5.0.1" }),
|
|
176
|
+
...extra,
|
|
177
|
+
].map((p) => [p.id, p]),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
describe("planPluginUpdates — a pin its own marketplace does not offer", () => {
|
|
181
|
+
test("declared, not installed, marketplace read, plugin absent → unknown", () => {
|
|
182
|
+
// The bug: the !installed branch ran before any catalog guard, so
|
|
183
|
+
// `agentdev@magus` (retired) and `seo@magus` (moved to magus-marketing)
|
|
184
|
+
// planned as `install` with the literal pin as target on every run,
|
|
185
|
+
// forever, and every apply failed. The installed path already answered
|
|
186
|
+
// this correctly further down; the two are now consistent.
|
|
187
|
+
const items = planPluginUpdates(
|
|
188
|
+
{ "agentdev@magus": "latest" },
|
|
189
|
+
magusCatalog(),
|
|
190
|
+
);
|
|
191
|
+
expect(items).toHaveLength(1);
|
|
192
|
+
expect(items[0]?.action).toBe("unknown");
|
|
193
|
+
expect(items[0]?.target).toBeNull();
|
|
194
|
+
expect(items[0]?.note).toContain(
|
|
195
|
+
"not offered by any configured marketplace",
|
|
196
|
+
);
|
|
197
|
+
expect(items[0]?.reason).toBe("not-offered");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("an exact pin its marketplace does not offer is also unknown", () => {
|
|
201
|
+
const items = planPluginUpdates({ "gone@magus": "3.0.0" }, magusCatalog());
|
|
202
|
+
expect(items[0]?.action).toBe("unknown");
|
|
203
|
+
expect(items[0]?.target).toBeNull();
|
|
204
|
+
expect(items[0]?.reason).toBe("not-offered");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("declared, not installed, and the catalog does offer it → install", () => {
|
|
208
|
+
const items = planPluginUpdates(
|
|
209
|
+
{ "kanban@magus": "latest" },
|
|
210
|
+
magusCatalog(info({ id: "kanban@magus", version: "1.6.2" })),
|
|
211
|
+
);
|
|
212
|
+
expect(items[0]?.action).toBe("install");
|
|
213
|
+
expect(items[0]?.target).toBe("1.6.2");
|
|
214
|
+
expect(items[0]?.reason).toBe("absent");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("a marketplace we have NOT read still plans an install", () => {
|
|
218
|
+
// Not the dead-pin case: with no entries from @newmp we never read its
|
|
219
|
+
// catalog, so absence is not evidence — and installing is what registers
|
|
220
|
+
// the marketplace in the first place. Guarding on `available === null`
|
|
221
|
+
// alone broke exactly this, which the existing suite caught.
|
|
222
|
+
const items = planPluginUpdates(
|
|
223
|
+
{ "thing@newmp": "latest" },
|
|
224
|
+
magusCatalog(),
|
|
225
|
+
);
|
|
226
|
+
expect(items[0]?.action).toBe("install");
|
|
227
|
+
expect(items[0]?.reason).toBe("absent");
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe("REVIEW-B regressions — found by the patch-quality panel", () => {
|
|
232
|
+
test("a catalog entry with NO version is offered, not unknown", () => {
|
|
233
|
+
// Measured: 278 of the 292 plugins in claude-plugins-official declare no
|
|
234
|
+
// version at all, `feature-dev` among them. Keying "not offered" on
|
|
235
|
+
// `available === null` planned `unknown` for every one of them and never
|
|
236
|
+
// installed. Membership in the catalog is the real test.
|
|
237
|
+
const items = planPluginUpdates(
|
|
238
|
+
{ "feature-dev@official": "latest" },
|
|
239
|
+
new Map(
|
|
240
|
+
[
|
|
241
|
+
info({ id: "feature-dev@official", version: null }),
|
|
242
|
+
info({ id: "github@official", version: null }),
|
|
243
|
+
].map((p) => [p.id, p]),
|
|
244
|
+
),
|
|
245
|
+
);
|
|
246
|
+
expect(items[0]?.action).toBe("install");
|
|
247
|
+
expect(items[0]?.reason).toBe("absent");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// REVIEW-B's finding is accepted and its remedy REVERSED. 21 registry rows
|
|
251
|
+
// carry the literal string "unknown" as their version; against a semver
|
|
252
|
+
// catalog they are unorderable, so nothing is provably behind. REVIEW-B
|
|
253
|
+
// concluded that made `current` a false claim.
|
|
254
|
+
//
|
|
255
|
+
// The claim is now explicitly "nothing is KNOWN to be behind", and that is
|
|
256
|
+
// what `current` means here. Raising a warning instead put a permanent amber
|
|
257
|
+
// row in front of the user with no action that could ever clear it, which is
|
|
258
|
+
// a worse falsehood: it says "do something" when there is nothing to do.
|
|
259
|
+
test("an installed version that cannot be compared is assumed current", () => {
|
|
260
|
+
const items = planPluginUpdates(
|
|
261
|
+
{ "playground@official": "latest" },
|
|
262
|
+
new Map([
|
|
263
|
+
[
|
|
264
|
+
"playground@official",
|
|
265
|
+
info({
|
|
266
|
+
id: "playground@official",
|
|
267
|
+
version: "7.3.0",
|
|
268
|
+
projectScope: { enabled: true, version: "unknown" },
|
|
269
|
+
}),
|
|
270
|
+
],
|
|
271
|
+
]),
|
|
272
|
+
);
|
|
273
|
+
expect(items[0]?.action).toBe("current");
|
|
274
|
+
expect(items[0]?.reason).not.toBe("unorderable");
|
|
275
|
+
// The row stays silent: no note, and nothing planned against it.
|
|
276
|
+
expect(items[0]?.note).toBeUndefined();
|
|
277
|
+
expect(items[0]?.scopes).toEqual([]);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("a genuinely current install is still current", () => {
|
|
281
|
+
const items = planPluginUpdates(
|
|
282
|
+
{ "dev@magus": "latest" },
|
|
283
|
+
new Map([
|
|
284
|
+
[
|
|
285
|
+
"dev@magus",
|
|
286
|
+
info({
|
|
287
|
+
id: "dev@magus",
|
|
288
|
+
version: "7.3.0",
|
|
289
|
+
projectScope: { enabled: true, version: "7.3.0" },
|
|
290
|
+
}),
|
|
291
|
+
],
|
|
292
|
+
]),
|
|
293
|
+
);
|
|
294
|
+
expect(items[0]?.action).toBe("current");
|
|
295
|
+
expect(items[0]?.reason).toBe("up-to-date");
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe("resolveScopeAction — the scope key must never silently uninstall", () => {
|
|
300
|
+
test("installed but DISABLED resolves to install, not uninstall", () => {
|
|
301
|
+
// The hazard: the scope key runs cliUninstallPlugin with no confirmation.
|
|
302
|
+
// Relaxing isInstalledInScope made this combination fall through to
|
|
303
|
+
// "uninstall", so pressing the key on a plugin the user had merely
|
|
304
|
+
// switched off would delete it.
|
|
305
|
+
expect(
|
|
306
|
+
resolveScopeAction({ enabled: false, version: "3.0.2" }, false),
|
|
307
|
+
).toBe("install");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("installed AND enabled at the latest version still offers uninstall", () => {
|
|
311
|
+
expect(resolveScopeAction({ enabled: true, version: "3.0.2" }, false)).toBe(
|
|
312
|
+
"uninstall",
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("installed, enabled, and behind still offers update", () => {
|
|
317
|
+
expect(resolveScopeAction({ enabled: true, version: "3.0.1" }, true)).toBe(
|
|
318
|
+
"update",
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("not installed at all resolves to install", () => {
|
|
323
|
+
expect(resolveScopeAction(undefined, false)).toBe("install");
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
describe("installed with no comparable catalog version — two different causes", () => {
|
|
328
|
+
test("in the catalog but declaring no version is NOT 'withdrawn'", () => {
|
|
329
|
+
// feature-dev@claude-plugins-official, measured live: installed, listed by
|
|
330
|
+
// its marketplace, and declaring no version. Reporting it as "not offered
|
|
331
|
+
// by any configured marketplace" told the user a working, currently
|
|
332
|
+
// published plugin had been withdrawn.
|
|
333
|
+
const items = planPluginUpdates(
|
|
334
|
+
{ "feature-dev@official": "latest" },
|
|
335
|
+
new Map([
|
|
336
|
+
[
|
|
337
|
+
"feature-dev@official",
|
|
338
|
+
info({
|
|
339
|
+
id: "feature-dev@official",
|
|
340
|
+
version: null,
|
|
341
|
+
projectScope: { enabled: true, version: "3ea32df27be7" },
|
|
342
|
+
}),
|
|
343
|
+
],
|
|
344
|
+
]),
|
|
345
|
+
);
|
|
346
|
+
// The distinction this test exists for is between the two REASONS, and it
|
|
347
|
+
// still holds. What changed is the verdict: declaring no version is not a
|
|
348
|
+
// problem, so the row is `current` and says nothing. Being withdrawn IS a
|
|
349
|
+
// problem, so the next test's row is `unknown` and says so.
|
|
350
|
+
expect(items[0]?.reason).toBe("no-published-version");
|
|
351
|
+
expect(items[0]?.reason).not.toBe("not-offered");
|
|
352
|
+
expect(items[0]?.action).toBe("current");
|
|
353
|
+
expect(items[0]?.note).toBeUndefined();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("absent from the catalog entirely IS 'not offered'", () => {
|
|
357
|
+
const items = planPluginUpdates(
|
|
358
|
+
{ "gone@magus": "latest" },
|
|
359
|
+
new Map([
|
|
360
|
+
[
|
|
361
|
+
"other@magus",
|
|
362
|
+
info({
|
|
363
|
+
id: "other@magus",
|
|
364
|
+
version: "1.0.0",
|
|
365
|
+
projectScope: { enabled: true, version: "1.0.0" },
|
|
366
|
+
}),
|
|
367
|
+
],
|
|
368
|
+
]),
|
|
369
|
+
);
|
|
370
|
+
expect(items[0]?.action).toBe("unknown");
|
|
371
|
+
expect(items[0]?.note).toContain(
|
|
372
|
+
"not offered by any configured marketplace",
|
|
373
|
+
);
|
|
374
|
+
expect(items[0]?.reason).toBe("not-offered");
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
describe('the "0.0.0" registry value keeps its reason', () => {
|
|
379
|
+
test("installed at 0.0.0 with no published version is no-published-version", () => {
|
|
380
|
+
// Real: explanatory-output-style@claude-plugins-official is recorded as
|
|
381
|
+
// "0.0.0" in the registry for many project paths, and its marketplace
|
|
382
|
+
// publishes no version.
|
|
383
|
+
//
|
|
384
|
+
// "0.0.0" must never silently become a DIFFERENT reason — that is what
|
|
385
|
+
// this pins. The verdict is `current`: there is no published version to
|
|
386
|
+
// be behind, so there is nothing to report and nothing to do.
|
|
387
|
+
const items = planPluginUpdates(
|
|
388
|
+
{ "explanatory-output-style@official": "latest" },
|
|
389
|
+
new Map([
|
|
390
|
+
[
|
|
391
|
+
"explanatory-output-style@official",
|
|
392
|
+
info({
|
|
393
|
+
id: "explanatory-output-style@official",
|
|
394
|
+
version: null,
|
|
395
|
+
projectScope: { enabled: true, version: "0.0.0" },
|
|
396
|
+
}),
|
|
397
|
+
],
|
|
398
|
+
]),
|
|
399
|
+
);
|
|
400
|
+
expect(items[0]?.reason).toBe("no-published-version");
|
|
401
|
+
expect(items[0]?.action).toBe("current");
|
|
402
|
+
});
|
|
403
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { pickRegistryEntry } from "../services/claude-settings.js";
|
|
3
|
+
import type { InstalledPluginEntry } from "../types/index.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Regression cover for "a fresh git worktree reports every plugin as not installed".
|
|
7
|
+
*
|
|
8
|
+
* `installed_plugins.json` keys one entry per `projectPath`, and a new worktree is
|
|
9
|
+
* a project path nobody has ever installed into — measured on a real machine:
|
|
10
|
+
* 750 project-scope rows, zero of them for the worktree, and zero user-scope rows
|
|
11
|
+
* for any magus plugin. So `pickRegistryEntry` found nothing, `overlayRegistryVersions`
|
|
12
|
+
* deleted the version as an unbacked claim, and `isInstalledInScope` reported the
|
|
13
|
+
* plugin missing.
|
|
14
|
+
*
|
|
15
|
+
* Claude Code disagreed: it loaded all of them. Its loader is not gated on the
|
|
16
|
+
* current project — a worktree with no rows resolves `installPath` from another
|
|
17
|
+
* project's row and loads from cache (measured, `ai-docs/plugin-system-state-model.md`).
|
|
18
|
+
* So magus was offering to install plugins that were already working.
|
|
19
|
+
*
|
|
20
|
+
* A linked worktree is the same project as its main working tree, so the main
|
|
21
|
+
* tree's row is the honest answer for it.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const entry = (
|
|
25
|
+
scope: InstalledPluginEntry["scope"],
|
|
26
|
+
version: string,
|
|
27
|
+
projectPath?: string,
|
|
28
|
+
): InstalledPluginEntry => ({
|
|
29
|
+
scope,
|
|
30
|
+
version,
|
|
31
|
+
projectPath,
|
|
32
|
+
installPath: `/cache/${version}`,
|
|
33
|
+
installedAt: "2026-01-01T00:00:00.000Z",
|
|
34
|
+
lastUpdated: "2026-01-01T00:00:00.000Z",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const MAIN = "/repo";
|
|
38
|
+
const TREE = "/repo/.claude/worktrees/feature";
|
|
39
|
+
|
|
40
|
+
describe("pickRegistryEntry — worktree fallback", () => {
|
|
41
|
+
test("a worktree with no rows of its own resolves to the main working tree's row", () => {
|
|
42
|
+
const entries = [entry("project", "3.3.0", MAIN)];
|
|
43
|
+
expect(pickRegistryEntry(entries, "project", TREE, [MAIN])?.version).toBe(
|
|
44
|
+
"3.3.0",
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("the worktree's own row still wins over the main tree's row", () => {
|
|
49
|
+
const entries = [
|
|
50
|
+
entry("project", "3.3.0", MAIN),
|
|
51
|
+
entry("project", "4.0.0", TREE),
|
|
52
|
+
];
|
|
53
|
+
expect(pickRegistryEntry(entries, "project", TREE, [MAIN])?.version).toBe(
|
|
54
|
+
"4.0.0",
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("a local-scope row on the main tree satisfies a worktree project lookup", () => {
|
|
59
|
+
const entries = [entry("local", "5.0.0", MAIN)];
|
|
60
|
+
expect(pickRegistryEntry(entries, "project", TREE, [MAIN])?.version).toBe(
|
|
61
|
+
"5.0.0",
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("the main-tree row outranks a user-scope entry", () => {
|
|
66
|
+
// Same project beats "installed globally somewhere". The user entry is the
|
|
67
|
+
// looser fallback and stays available when the main tree has no row.
|
|
68
|
+
const entries = [entry("user", "1.0.0"), entry("project", "3.3.0", MAIN)];
|
|
69
|
+
expect(pickRegistryEntry(entries, "project", TREE, [MAIN])?.version).toBe(
|
|
70
|
+
"3.3.0",
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("the user-scope entry still applies when the main tree has no row either", () => {
|
|
75
|
+
const entries = [
|
|
76
|
+
entry("user", "2.1.2"),
|
|
77
|
+
entry("project", "9.9.9", "/elsewhere"),
|
|
78
|
+
];
|
|
79
|
+
expect(pickRegistryEntry(entries, "project", TREE, [MAIN])?.version).toBe(
|
|
80
|
+
"2.1.2",
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("an unrelated project's row is never inherited", () => {
|
|
85
|
+
// The loader may well resolve one of these, but magus must not claim a
|
|
86
|
+
// version it cannot attribute to this project.
|
|
87
|
+
const entries = [entry("project", "9.9.9", "/some/other/project")];
|
|
88
|
+
expect(pickRegistryEntry(entries, "project", TREE, [MAIN])).toBeUndefined();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("fallback paths are compared resolved, not as raw strings", () => {
|
|
92
|
+
const entries = [entry("project", "4.2.0", "/repo/./sub/..")];
|
|
93
|
+
expect(pickRegistryEntry(entries, "project", TREE, [MAIN])?.version).toBe(
|
|
94
|
+
"4.2.0",
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("fallback paths are ignored for a user-scope lookup", () => {
|
|
99
|
+
const entries = [entry("project", "3.3.0", MAIN)];
|
|
100
|
+
expect(pickRegistryEntry(entries, "user", TREE, [MAIN])).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("omitting fallback paths preserves the previous behaviour", () => {
|
|
104
|
+
const entries = [entry("project", "3.3.0", MAIN)];
|
|
105
|
+
expect(pickRegistryEntry(entries, "project", TREE)).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
});
|