magus-cli 0.0.0-placeholder → 7.0.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 +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__/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 +445 -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 +174 -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 +335 -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__/moved-marketplace.test.ts +86 -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 +336 -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 +281 -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 +1373 -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 +388 -0
- package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
- package/src/cli/ansi.ts +520 -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 +207 -0
- package/src/cli/update-view.ts +526 -0
- package/src/cli/update.ts +613 -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 +1589 -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 +587 -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 +253 -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 +878 -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 +344 -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 +863 -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 +538 -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 +1464 -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 +352 -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 +144 -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,98 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
COMPONENT_BADGE,
|
|
4
|
+
CONTRAST_REFERENCE_KEYS,
|
|
5
|
+
componentBadge,
|
|
6
|
+
resetThemeMode,
|
|
7
|
+
setThemeMode,
|
|
8
|
+
} from "../ui/theme-mode.js";
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
resetThemeMode();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
/** WCAG relative luminance of a #rrggbb string. */
|
|
15
|
+
function luminance(hex: string): number {
|
|
16
|
+
const channel = (v: number) => {
|
|
17
|
+
const c = v / 255;
|
|
18
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
19
|
+
};
|
|
20
|
+
const n = Number.parseInt(hex.slice(1), 16);
|
|
21
|
+
return (
|
|
22
|
+
0.2126 * channel((n >> 16) & 0xff) +
|
|
23
|
+
0.7152 * channel((n >> 8) & 0xff) +
|
|
24
|
+
0.0722 * channel(n & 0xff)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function contrast(a: string, b: string): number {
|
|
29
|
+
const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
|
|
30
|
+
return (hi + 0.05) / (lo + 0.05);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const KINDS = ["skill", "command", "agent", "mcp"] as const;
|
|
34
|
+
|
|
35
|
+
describe("componentBadge", () => {
|
|
36
|
+
it("serves the dark pair before any resolution", () => {
|
|
37
|
+
// There is no unknown: dark is the stated default, so a chip is always a
|
|
38
|
+
// pair, never null, and the caller has no bare-glyph branch to keep.
|
|
39
|
+
for (const kind of KINDS) {
|
|
40
|
+
expect(componentBadge(kind)).toEqual(COMPONENT_BADGE[kind].dark);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("serves the light pair on a light terminal", () => {
|
|
45
|
+
setThemeMode("light");
|
|
46
|
+
|
|
47
|
+
expect(componentBadge("skill")).toEqual(COMPONENT_BADGE.skill.light);
|
|
48
|
+
expect(componentBadge("mcp")).toEqual(COMPONENT_BADGE.mcp.light);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("serves the dark pair on a dark terminal", () => {
|
|
52
|
+
setThemeMode("dark");
|
|
53
|
+
|
|
54
|
+
expect(componentBadge("skill")).toEqual(COMPONENT_BADGE.skill.dark);
|
|
55
|
+
expect(componentBadge("mcp")).toEqual(COMPONENT_BADGE.mcp.dark);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("keeps every glyph legible on its own chip", () => {
|
|
59
|
+
// 3:1 is the floor the rest of the palette is measured to. A chip whose
|
|
60
|
+
// glyph disappears into its wash is worse than no chip at all.
|
|
61
|
+
for (const kind of KINDS) {
|
|
62
|
+
for (const mode of ["light", "dark"] as const) {
|
|
63
|
+
const { bg, fg } = COMPONENT_BADGE[kind][mode];
|
|
64
|
+
expect(contrast(bg, fg)).toBeGreaterThanOrEqual(3);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("keeps every chip quiet against the page it is drawn on", () => {
|
|
70
|
+
// The wash must read as a tint of the page, not as a block on it. Above
|
|
71
|
+
// ~1.6 it stops being a chip and starts being a highlight bar.
|
|
72
|
+
for (const kind of KINDS) {
|
|
73
|
+
for (const mode of ["light", "dark"] as const) {
|
|
74
|
+
const { bg } = COMPONENT_BADGE[kind][mode];
|
|
75
|
+
const page = CONTRAST_REFERENCE_KEYS[mode];
|
|
76
|
+
expect(contrast(bg, page)).toBeLessThan(1.6);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("mirrors the reference pages theme.ts measures accents against", async () => {
|
|
82
|
+
// The duplicate exists so this module stays free of the renderer import.
|
|
83
|
+
// If theme.ts ever restates its backgrounds, the chips must move with it.
|
|
84
|
+
const { CONTRAST_REFERENCE } = await import("../ui/theme.js");
|
|
85
|
+
|
|
86
|
+
expect(CONTRAST_REFERENCE_KEYS.light).toBe(CONTRAST_REFERENCE.light);
|
|
87
|
+
expect(CONTRAST_REFERENCE_KEYS.dark).toBe(CONTRAST_REFERENCE.dark);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("gives each kind a distinguishable hue", () => {
|
|
91
|
+
// Single-character colour carries no readable hue *shift*, only a readable
|
|
92
|
+
// hue *difference* — the same reason the scope squares are far apart.
|
|
93
|
+
for (const mode of ["light", "dark"] as const) {
|
|
94
|
+
const fills = KINDS.map((k) => COMPONENT_BADGE[k][mode].bg);
|
|
95
|
+
expect(new Set(fills).size).toBe(KINDS.length);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { COMPONENT_ICON } from "../ui/renderers/pluginRenderers.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Every component badge is exactly one terminal cell wide.
|
|
6
|
+
*
|
|
7
|
+
* This is the constraint that rules out the icons the ecosystem actually uses:
|
|
8
|
+
* Cursor's skill badge defaults to a lightning bolt and the community VS Code
|
|
9
|
+
* Skills sidebar uses a sparkle, but `⚡` (U+26A1) and `✨` (U+2728) are both
|
|
10
|
+
* East-Asian-Width Wide — two cells — and a two-cell badge shifts every name on
|
|
11
|
+
* the row out of alignment with the plugin names above it.
|
|
12
|
+
*
|
|
13
|
+
* Unicode has no `east_asian_width` in the JS standard library, so the wide
|
|
14
|
+
* ranges are spelled out. They cover the emoji and CJK blocks a plausible icon
|
|
15
|
+
* would come from; anything outside them is single-width in practice.
|
|
16
|
+
*/
|
|
17
|
+
const WIDE_RANGES: ReadonlyArray<readonly [number, number]> = [
|
|
18
|
+
[0x1100, 0x115f], // Hangul Jamo
|
|
19
|
+
[0x2329, 0x232a], // angle brackets
|
|
20
|
+
[0x2e80, 0x303e], // CJK radicals, Kangxi
|
|
21
|
+
[0x3041, 0x33ff], // Hiragana through CJK compatibility
|
|
22
|
+
[0x3400, 0x4dbf], // CJK Extension A
|
|
23
|
+
[0x4e00, 0x9fff], // CJK Unified Ideographs
|
|
24
|
+
[0xa000, 0xa4cf], // Yi
|
|
25
|
+
[0xac00, 0xd7a3], // Hangul syllables
|
|
26
|
+
[0xf900, 0xfaff], // CJK compatibility ideographs
|
|
27
|
+
[0xfe30, 0xfe6f], // CJK compatibility forms
|
|
28
|
+
[0xff00, 0xff60], // fullwidth forms
|
|
29
|
+
[0xffe0, 0xffe6], // fullwidth signs
|
|
30
|
+
[0x1f300, 0x1f64f], // misc symbols and pictographs, emoticons
|
|
31
|
+
[0x1f900, 0x1f9ff], // supplemental symbols and pictographs
|
|
32
|
+
[0x20000, 0x3fffd], // CJK Extension B and beyond
|
|
33
|
+
// Individually Wide despite sitting among narrow neighbours. These are the
|
|
34
|
+
// ones that actually bite: ⚡ and ✨ are exactly the icons a future editor
|
|
35
|
+
// would reach for.
|
|
36
|
+
[0x231a, 0x231b], // watch, hourglass
|
|
37
|
+
[0x23e9, 0x23ec],
|
|
38
|
+
[0x25fd, 0x25fe],
|
|
39
|
+
[0x2614, 0x2615],
|
|
40
|
+
[0x26a1, 0x26a1], // ⚡ HIGH VOLTAGE SIGN
|
|
41
|
+
[0x2728, 0x2728], // ✨ SPARKLES
|
|
42
|
+
[0x274c, 0x274c],
|
|
43
|
+
[0x2b1b, 0x2b1c],
|
|
44
|
+
[0x2b50, 0x2b50],
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
function isWide(ch: string): boolean {
|
|
48
|
+
const cp = ch.codePointAt(0) ?? 0;
|
|
49
|
+
return WIDE_RANGES.some(([lo, hi]) => cp >= lo && cp <= hi);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("component icons", () => {
|
|
53
|
+
it("covers every component kind", () => {
|
|
54
|
+
expect(Object.keys(COMPONENT_ICON).sort()).toEqual([
|
|
55
|
+
"agent",
|
|
56
|
+
"command",
|
|
57
|
+
"mcp",
|
|
58
|
+
"skill",
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("uses exactly one code point per icon", () => {
|
|
63
|
+
for (const [kind, icon] of Object.entries(COMPONENT_ICON)) {
|
|
64
|
+
expect([...icon], `${kind} must be a single code point`).toHaveLength(1);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("never uses a double-width glyph", () => {
|
|
69
|
+
for (const [kind, icon] of Object.entries(COMPONENT_ICON)) {
|
|
70
|
+
expect(isWide(icon), `${kind} icon ${icon} is double-width`).toBe(false);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("rejects the two emoji a future editor would reach for", () => {
|
|
75
|
+
// Negative control: the guard above only means something if it fires.
|
|
76
|
+
expect(isWide("⚡")).toBe(true);
|
|
77
|
+
expect(isWide("✨")).toBe(true);
|
|
78
|
+
expect(isWide("📁")).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("gives each kind a distinct glyph", () => {
|
|
82
|
+
const icons = Object.values(COMPONENT_ICON);
|
|
83
|
+
expect(new Set(icons).size).toBe(icons.length);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("does not reuse a glyph this screen already means something by", () => {
|
|
87
|
+
// ★ is "★ Official" on a marketplace row, ● is "● Installed", ■ is a scope
|
|
88
|
+
// segment, and ▼ / ▶ are the expand arrows. Reusing one would make a badge
|
|
89
|
+
// read as a status.
|
|
90
|
+
for (const taken of ["★", "●", "■", "▼", "▶", "○", "✓", "⚠"]) {
|
|
91
|
+
expect(Object.values(COMPONENT_ICON)).not.toContain(taken);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content drift: a plugin whose files changed without its version changing.
|
|
3
|
+
*
|
|
4
|
+
* Hermetic — builds a real git marketplace under a temp CLAUDE_CONFIG_DIR.
|
|
5
|
+
* Real git is used deliberately: the detection IS a git question, and mocking
|
|
6
|
+
* it would only test the mock.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { promises as fs } from "node:fs";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import {
|
|
15
|
+
clearContentDriftCache,
|
|
16
|
+
hasContentDrift,
|
|
17
|
+
repoHeadSha,
|
|
18
|
+
} from "../services/content-drift.js";
|
|
19
|
+
import {
|
|
20
|
+
type PluginInfo,
|
|
21
|
+
contentDriftCandidate,
|
|
22
|
+
} from "../services/plugin-manager.js";
|
|
23
|
+
|
|
24
|
+
let configDir: string;
|
|
25
|
+
let prevConfigDir: string | undefined;
|
|
26
|
+
let repo: string;
|
|
27
|
+
|
|
28
|
+
// commit.gpgsign=false isolates the developer's git config. With signing on
|
|
29
|
+
// (1Password's SSH signer, for instance) every commit here calls that signer,
|
|
30
|
+
// and because this helper ignores the exit status a failure is silent: no
|
|
31
|
+
// commit is created, so drift is never detected and the assertions fail with a
|
|
32
|
+
// bare `false` that points nowhere near the real cause. CI has no signing
|
|
33
|
+
// config, so this only ever broke locally.
|
|
34
|
+
const git = (args: string[]) =>
|
|
35
|
+
spawnSync("git", ["-c", "commit.gpgsign=false", ...args], {
|
|
36
|
+
cwd: repo,
|
|
37
|
+
encoding: "utf-8",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const head = () => git(["rev-parse", "HEAD"]).stdout.trim();
|
|
41
|
+
|
|
42
|
+
/** Rewrite a plugin's manifest version, mimicking a release. */
|
|
43
|
+
const setVersion = (plugin: string, version: string) =>
|
|
44
|
+
fs.writeFile(
|
|
45
|
+
path.join(repo, "plugins", plugin, "plugin.json"),
|
|
46
|
+
JSON.stringify({ name: plugin, version }, null, 2),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
beforeEach(async () => {
|
|
50
|
+
prevConfigDir = process.env.CLAUDE_CONFIG_DIR;
|
|
51
|
+
configDir = await fs.mkdtemp(path.join(os.tmpdir(), "drift-"));
|
|
52
|
+
process.env.CLAUDE_CONFIG_DIR = configDir;
|
|
53
|
+
clearContentDriftCache();
|
|
54
|
+
|
|
55
|
+
repo = path.join(configDir, "plugins", "marketplaces", "probemp");
|
|
56
|
+
await fs.mkdir(path.join(repo, "plugins", "alpha", "skills"), {
|
|
57
|
+
recursive: true,
|
|
58
|
+
});
|
|
59
|
+
await fs.mkdir(path.join(repo, "plugins", "beta"), { recursive: true });
|
|
60
|
+
await fs.writeFile(
|
|
61
|
+
path.join(repo, "plugins", "alpha", "skills", "s.md"),
|
|
62
|
+
"v1",
|
|
63
|
+
);
|
|
64
|
+
await fs.writeFile(path.join(repo, "plugins", "beta", "readme.md"), "v1");
|
|
65
|
+
await setVersion("alpha", "1.0.0");
|
|
66
|
+
await setVersion("beta", "1.0.0");
|
|
67
|
+
|
|
68
|
+
git(["init", "-q", "-b", "main"]);
|
|
69
|
+
git(["config", "user.email", "probe@example.invalid"]);
|
|
70
|
+
git(["config", "user.name", "probe"]);
|
|
71
|
+
git(["add", "-A"]);
|
|
72
|
+
git(["commit", "-q", "-m", "release 1.0.0"]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterEach(async () => {
|
|
76
|
+
// biome-ignore lint/performance/noDelete: assigning undefined to process.env stores the string "undefined"; only delete unsets the variable
|
|
77
|
+
if (prevConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR;
|
|
78
|
+
else process.env.CLAUDE_CONFIG_DIR = prevConfigDir;
|
|
79
|
+
await fs.rm(configDir, { recursive: true, force: true });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("hasContentDrift", () => {
|
|
83
|
+
test("no drift when the plugin's files are untouched", async () => {
|
|
84
|
+
const sha = head();
|
|
85
|
+
expect(
|
|
86
|
+
await hasContentDrift({
|
|
87
|
+
marketplace: "probemp",
|
|
88
|
+
pluginName: "alpha",
|
|
89
|
+
installedSha: sha,
|
|
90
|
+
installedVersion: "1.0.0",
|
|
91
|
+
}),
|
|
92
|
+
).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("drift when the plugin's files change under an unchanged version", async () => {
|
|
96
|
+
const sha = head();
|
|
97
|
+
// Republish: same version, different content — what publish-dist.sh does.
|
|
98
|
+
await fs.writeFile(
|
|
99
|
+
path.join(repo, "plugins", "alpha", "skills", "s.md"),
|
|
100
|
+
"v2 — different content, same version",
|
|
101
|
+
);
|
|
102
|
+
git(["add", "-A"]);
|
|
103
|
+
git(["commit", "-q", "-m", "republish 1.0.0"]);
|
|
104
|
+
|
|
105
|
+
expect(
|
|
106
|
+
await hasContentDrift({
|
|
107
|
+
marketplace: "probemp",
|
|
108
|
+
pluginName: "alpha",
|
|
109
|
+
installedSha: sha,
|
|
110
|
+
installedVersion: "1.0.0",
|
|
111
|
+
}),
|
|
112
|
+
).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("a change to ANOTHER plugin does not flag this one", async () => {
|
|
116
|
+
// The reason this compares the plugin subtree and not the marketplace
|
|
117
|
+
// HEAD: any commit to any plugin would otherwise flag every plugin.
|
|
118
|
+
const sha = head();
|
|
119
|
+
await fs.writeFile(path.join(repo, "plugins", "beta", "readme.md"), "v2");
|
|
120
|
+
git(["add", "-A"]);
|
|
121
|
+
git(["commit", "-q", "-m", "unrelated change"]);
|
|
122
|
+
|
|
123
|
+
expect(
|
|
124
|
+
await hasContentDrift({
|
|
125
|
+
marketplace: "probemp",
|
|
126
|
+
pluginName: "alpha",
|
|
127
|
+
installedSha: sha,
|
|
128
|
+
installedVersion: "1.0.0",
|
|
129
|
+
}),
|
|
130
|
+
).toBe(false);
|
|
131
|
+
expect(
|
|
132
|
+
await hasContentDrift({
|
|
133
|
+
marketplace: "probemp",
|
|
134
|
+
pluginName: "beta",
|
|
135
|
+
installedSha: sha,
|
|
136
|
+
installedVersion: "1.0.0",
|
|
137
|
+
}),
|
|
138
|
+
).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("deleting a skill counts as drift", async () => {
|
|
142
|
+
// The live case: dev@magus 3.0.1 lost tui-progress/SKILL.md while still
|
|
143
|
+
// declaring 3.0.1, so the version compare reported "up to date".
|
|
144
|
+
const sha = head();
|
|
145
|
+
await fs.rm(path.join(repo, "plugins", "alpha", "skills", "s.md"));
|
|
146
|
+
git(["add", "-A"]);
|
|
147
|
+
git(["commit", "-q", "-m", "drop a skill"]);
|
|
148
|
+
|
|
149
|
+
expect(
|
|
150
|
+
await hasContentDrift({
|
|
151
|
+
marketplace: "probemp",
|
|
152
|
+
pluginName: "alpha",
|
|
153
|
+
installedSha: sha,
|
|
154
|
+
installedVersion: "1.0.0",
|
|
155
|
+
}),
|
|
156
|
+
).toBe(true);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("stays silent when it cannot answer honestly", async () => {
|
|
160
|
+
// No recorded sha, unknown sha (force-push/shallow clone), and unknown
|
|
161
|
+
// marketplace must all report false rather than nag for a reinstall.
|
|
162
|
+
expect(
|
|
163
|
+
await hasContentDrift({
|
|
164
|
+
marketplace: "probemp",
|
|
165
|
+
pluginName: "alpha",
|
|
166
|
+
installedSha: undefined,
|
|
167
|
+
installedVersion: "1.0.0",
|
|
168
|
+
}),
|
|
169
|
+
).toBe(false);
|
|
170
|
+
expect(
|
|
171
|
+
await hasContentDrift({
|
|
172
|
+
marketplace: "probemp",
|
|
173
|
+
pluginName: "alpha",
|
|
174
|
+
installedSha: "0".repeat(40),
|
|
175
|
+
installedVersion: "1.0.0",
|
|
176
|
+
}),
|
|
177
|
+
).toBe(false);
|
|
178
|
+
expect(
|
|
179
|
+
await hasContentDrift({
|
|
180
|
+
marketplace: "does-not-exist",
|
|
181
|
+
pluginName: "alpha",
|
|
182
|
+
installedSha: head(),
|
|
183
|
+
installedVersion: "1.0.0",
|
|
184
|
+
}),
|
|
185
|
+
).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// REGRESSION: every installed plugin rendered "stale — reinstall" because
|
|
189
|
+
// updateInstalledPluginsRegistry bumped `version` while carrying the
|
|
190
|
+
// pre-update `gitCommitSha` forward. The sha then named a commit several
|
|
191
|
+
// releases old, so the subtree diff was non-empty for essentially every
|
|
192
|
+
// plugin. Fixed in /dev:fix session dev-fix-20260822-125510-03db093b.
|
|
193
|
+
test("a sha recorded against a DIFFERENT version cannot answer the question", async () => {
|
|
194
|
+
const oldSha = head();
|
|
195
|
+
|
|
196
|
+
// Ship 2.0.0 — the plugin genuinely changed, as a release does.
|
|
197
|
+
await fs.writeFile(
|
|
198
|
+
path.join(repo, "plugins", "alpha", "skills", "s.md"),
|
|
199
|
+
"v2 content",
|
|
200
|
+
);
|
|
201
|
+
await setVersion("alpha", "2.0.0");
|
|
202
|
+
git(["add", "-A"]);
|
|
203
|
+
git(["commit", "-q", "-m", "release 2.0.0"]);
|
|
204
|
+
|
|
205
|
+
// 2.0.0 is installed, but the registry still carries the 1.0.0 sha.
|
|
206
|
+
// The subtree diff between them is non-empty, and says nothing about
|
|
207
|
+
// whether the installed 2.0.0 files match the marketplace's 2.0.0.
|
|
208
|
+
expect(
|
|
209
|
+
await hasContentDrift({
|
|
210
|
+
marketplace: "probemp",
|
|
211
|
+
pluginName: "alpha",
|
|
212
|
+
installedSha: oldSha,
|
|
213
|
+
installedVersion: "2.0.0",
|
|
214
|
+
}),
|
|
215
|
+
).toBe(false);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("a manifest that cannot be read at that sha reports no drift", async () => {
|
|
219
|
+
// Provenance unverifiable — the module's policy is silence, not a nag.
|
|
220
|
+
await fs.rm(path.join(repo, "plugins", "beta", "plugin.json"));
|
|
221
|
+
git(["add", "-A"]);
|
|
222
|
+
git(["commit", "-q", "-m", "beta loses its manifest"]);
|
|
223
|
+
const sha = head();
|
|
224
|
+
|
|
225
|
+
await fs.writeFile(path.join(repo, "plugins", "beta", "readme.md"), "v2");
|
|
226
|
+
git(["add", "-A"]);
|
|
227
|
+
git(["commit", "-q", "-m", "beta content changes"]);
|
|
228
|
+
|
|
229
|
+
expect(
|
|
230
|
+
await hasContentDrift({
|
|
231
|
+
marketplace: "probemp",
|
|
232
|
+
pluginName: "beta",
|
|
233
|
+
installedSha: sha,
|
|
234
|
+
installedVersion: "1.0.0",
|
|
235
|
+
}),
|
|
236
|
+
).toBe(false);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("reads a manifest at .claude-plugin/plugin.json too", async () => {
|
|
240
|
+
// magus lays plugins out as plugins/<name>/plugin.json, but Claude Code's
|
|
241
|
+
// documented location is .claude-plugin/plugin.json. Both must resolve or
|
|
242
|
+
// the guard silences drift detection for half the ecosystem.
|
|
243
|
+
await fs.mkdir(path.join(repo, "plugins", "beta", ".claude-plugin"), {
|
|
244
|
+
recursive: true,
|
|
245
|
+
});
|
|
246
|
+
await fs.rm(path.join(repo, "plugins", "beta", "plugin.json"));
|
|
247
|
+
await fs.writeFile(
|
|
248
|
+
path.join(repo, "plugins", "beta", ".claude-plugin", "plugin.json"),
|
|
249
|
+
JSON.stringify({ name: "beta", version: "1.0.0" }),
|
|
250
|
+
);
|
|
251
|
+
git(["add", "-A"]);
|
|
252
|
+
git(["commit", "-q", "-m", "beta uses .claude-plugin"]);
|
|
253
|
+
const sha = head();
|
|
254
|
+
|
|
255
|
+
await fs.writeFile(path.join(repo, "plugins", "beta", "readme.md"), "v2");
|
|
256
|
+
git(["add", "-A"]);
|
|
257
|
+
git(["commit", "-q", "-m", "republish 1.0.0"]);
|
|
258
|
+
|
|
259
|
+
expect(
|
|
260
|
+
await hasContentDrift({
|
|
261
|
+
marketplace: "probemp",
|
|
262
|
+
pluginName: "beta",
|
|
263
|
+
installedSha: sha,
|
|
264
|
+
installedVersion: "1.0.0",
|
|
265
|
+
}),
|
|
266
|
+
).toBe(true);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe("repoHeadSha", () => {
|
|
271
|
+
// The other half of the same bug: nothing in magus ever recorded a fresh
|
|
272
|
+
// sha, so the registry could only ever go stale. This is what
|
|
273
|
+
// updateInstalledPluginsRegistry now calls to stamp real provenance onto the
|
|
274
|
+
// files it just copied out of the marketplace clone.
|
|
275
|
+
test("resolves HEAD from a path inside the repo", async () => {
|
|
276
|
+
const sha = head();
|
|
277
|
+
expect(await repoHeadSha(path.join(repo, "plugins", "alpha"))).toBe(sha);
|
|
278
|
+
expect(await repoHeadSha(repo)).toBe(sha);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("returns undefined for a directory that is not in a git repo", async () => {
|
|
282
|
+
// Directory-type marketplaces have no commit to name. Recording a sha
|
|
283
|
+
// from some unrelated clone would assert a provenance that is false.
|
|
284
|
+
const plain = await fs.mkdtemp(path.join(os.tmpdir(), "nogit-"));
|
|
285
|
+
try {
|
|
286
|
+
expect(await repoHeadSha(plain)).toBeUndefined();
|
|
287
|
+
} finally {
|
|
288
|
+
await fs.rm(plain, { recursive: true, force: true });
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("returns undefined for a path that does not exist", async () => {
|
|
293
|
+
expect(
|
|
294
|
+
await repoHeadSha(path.join(configDir, "no", "such", "dir")),
|
|
295
|
+
).toBeUndefined();
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe("contentDriftCandidate — which plugins are even asked", () => {
|
|
300
|
+
const info = (over: Partial<PluginInfo> = {}): PluginInfo => ({
|
|
301
|
+
id: "dev@magus",
|
|
302
|
+
name: "dev",
|
|
303
|
+
version: "3.0.0",
|
|
304
|
+
description: "",
|
|
305
|
+
marketplace: "magus",
|
|
306
|
+
marketplaceDisplay: "magus",
|
|
307
|
+
enabled: true,
|
|
308
|
+
installedVersion: "3.0.0",
|
|
309
|
+
...over,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test("same version, provably: asked", () => {
|
|
313
|
+
expect(contentDriftCandidate(info())).toBe(true);
|
|
314
|
+
// The comparator, not string equality: "3.0" IS 3.0.0.
|
|
315
|
+
expect(contentDriftCandidate(info({ installedVersion: "3.0" }))).toBe(true);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("the pre-classifier `hasUpdate` no longer decides", () => {
|
|
319
|
+
// It was the gate's first clause. It could only ever be true for a pair
|
|
320
|
+
// the version compare already rejects, and reading it here made the
|
|
321
|
+
// input the shared classifier consumes depend on the rule it replaced.
|
|
322
|
+
// A stale flag must not suppress the question.
|
|
323
|
+
expect(contentDriftCandidate(info({ hasUpdate: true }))).toBe(true);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("behind the catalog: NOT asked — that is an update, not drift", () => {
|
|
327
|
+
// hasContentDrift diffs the installed commit against the clone HEAD, so
|
|
328
|
+
// a plugin that is merely behind always "differs". Calling that drift
|
|
329
|
+
// would make it a repair, and a repair reinstalls whatever the clone
|
|
330
|
+
// holds — past a pin.
|
|
331
|
+
expect(contentDriftCandidate(info({ installedVersion: "2.9.0" }))).toBe(
|
|
332
|
+
false,
|
|
333
|
+
);
|
|
334
|
+
expect(
|
|
335
|
+
contentDriftCandidate(
|
|
336
|
+
info({ hasUpdate: false, installedVersion: "2.9.0" }),
|
|
337
|
+
),
|
|
338
|
+
).toBe(false);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("unorderable, not installed, no catalog version, orphaned: not asked", () => {
|
|
342
|
+
expect(
|
|
343
|
+
contentDriftCandidate(
|
|
344
|
+
info({ version: "517b2fcd1b60", installedVersion: "b819188d2eea" }),
|
|
345
|
+
),
|
|
346
|
+
).toBe(false);
|
|
347
|
+
expect(contentDriftCandidate(info({ installedVersion: undefined }))).toBe(
|
|
348
|
+
false,
|
|
349
|
+
);
|
|
350
|
+
expect(contentDriftCandidate(info({ version: null }))).toBe(false);
|
|
351
|
+
expect(contentDriftCandidate(info({ isOrphaned: true }))).toBe(false);
|
|
352
|
+
});
|
|
353
|
+
});
|