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,545 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BLACK-BOX: R1 — one plugin classifier, and the TUI text contract (§8.1, §8.7).
|
|
3
|
+
* Written from requirements.md and architecture §8 only.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import type { PluginInfo } from "../services/plugin-manager.js";
|
|
7
|
+
import {
|
|
8
|
+
type PluginUpdateItem,
|
|
9
|
+
type UpdatePlan,
|
|
10
|
+
classifyPlugin,
|
|
11
|
+
classifyPluginRows,
|
|
12
|
+
countPluginStates,
|
|
13
|
+
indexCatalog,
|
|
14
|
+
pinForRow,
|
|
15
|
+
planHasWork,
|
|
16
|
+
planIsBehind,
|
|
17
|
+
planPluginUpdates,
|
|
18
|
+
scopeIsBehind,
|
|
19
|
+
} from "../services/update-plan.js";
|
|
20
|
+
import type { PluginPluginItem } from "../ui/adapters/pluginsAdapter.js";
|
|
21
|
+
import { pluginRowText } from "../ui/renderers/pluginRowText.js";
|
|
22
|
+
import { pluginStatusText } from "../ui/renderers/pluginStatusText.js";
|
|
23
|
+
|
|
24
|
+
function info(
|
|
25
|
+
id: string,
|
|
26
|
+
catalogVersion: string | null,
|
|
27
|
+
installed: string | null,
|
|
28
|
+
extra: Partial<PluginInfo> = {},
|
|
29
|
+
): PluginInfo {
|
|
30
|
+
const at = id.indexOf("@");
|
|
31
|
+
return {
|
|
32
|
+
id,
|
|
33
|
+
name: at >= 0 ? id.slice(0, at) : id,
|
|
34
|
+
version: catalogVersion,
|
|
35
|
+
description: "",
|
|
36
|
+
marketplace: at >= 0 ? id.slice(at + 1) : "",
|
|
37
|
+
marketplaceDisplay: at >= 0 ? id.slice(at + 1) : "",
|
|
38
|
+
enabled: installed !== null,
|
|
39
|
+
...(installed !== null
|
|
40
|
+
? {
|
|
41
|
+
installedVersion: installed,
|
|
42
|
+
projectScope: { enabled: true, version: installed },
|
|
43
|
+
}
|
|
44
|
+
: {}),
|
|
45
|
+
...extra,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function item(over: Partial<PluginUpdateItem>): PluginUpdateItem {
|
|
50
|
+
return {
|
|
51
|
+
pluginId: "x@magus",
|
|
52
|
+
pinned: "latest",
|
|
53
|
+
installed: "1.0.0",
|
|
54
|
+
available: "1.0.0",
|
|
55
|
+
target: null,
|
|
56
|
+
action: "current",
|
|
57
|
+
reason: "up-to-date",
|
|
58
|
+
scopes: [],
|
|
59
|
+
beforeVersions: {},
|
|
60
|
+
...over,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe("indexCatalog", () => {
|
|
65
|
+
const idx = indexCatalog([
|
|
66
|
+
info("dev@magus", "1.0.0", "1.0.0"),
|
|
67
|
+
info("old@magus", null, "0.1.0", { isOrphaned: true }),
|
|
68
|
+
info("solo@lonely", null, "0.1.0", { isOrphaned: true }),
|
|
69
|
+
]);
|
|
70
|
+
|
|
71
|
+
test("isListed is true only for a catalog row that is not an orphan", () => {
|
|
72
|
+
expect(idx.isListed("dev@magus")).toBe(true);
|
|
73
|
+
expect(idx.isListed("old@magus")).toBe(false);
|
|
74
|
+
expect(idx.isListed("missing@magus")).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("marketplaceIsKnown is true when another listed id shares the marketplace suffix", () => {
|
|
78
|
+
expect(idx.marketplaceIsKnown("missing@magus")).toBe(true);
|
|
79
|
+
expect(idx.marketplaceIsKnown("solo@lonely")).toBe(false);
|
|
80
|
+
expect(idx.marketplaceIsKnown("no-at-sign")).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("accepts a Map as well as an array", () => {
|
|
84
|
+
const fromMap = indexCatalog(
|
|
85
|
+
new Map([["dev@magus", info("dev@magus", "1.0.0", null)]]),
|
|
86
|
+
);
|
|
87
|
+
expect(fromMap.get("dev@magus")?.version).toBe("1.0.0");
|
|
88
|
+
expect(fromMap.isListed("dev@magus")).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe("classifyPlugin — one state plus a human reason", () => {
|
|
93
|
+
test("declared, absent from the catalog, marketplace read: unknown / not-offered, no target", () => {
|
|
94
|
+
const idx = indexCatalog([info("dev@magus", "1.0.0", "1.0.0")]);
|
|
95
|
+
|
|
96
|
+
const got = classifyPlugin("gone@magus", "latest", idx);
|
|
97
|
+
|
|
98
|
+
expect({
|
|
99
|
+
action: got.action,
|
|
100
|
+
reason: got.reason,
|
|
101
|
+
target: got.target,
|
|
102
|
+
}).toEqual({
|
|
103
|
+
action: "unknown",
|
|
104
|
+
reason: "not-offered",
|
|
105
|
+
target: null,
|
|
106
|
+
});
|
|
107
|
+
expect(got.note).toBeTruthy();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("installed orphan whose marketplace contributes no listed id: unknown / marketplace-unread", () => {
|
|
111
|
+
const idx = indexCatalog([
|
|
112
|
+
info("dev@magus", "1.0.0", "1.0.0"),
|
|
113
|
+
info("ghost@unread-mp", null, "0.3.0", { isOrphaned: true }),
|
|
114
|
+
]);
|
|
115
|
+
|
|
116
|
+
const got = classifyPlugin("ghost@unread-mp", "latest", idx);
|
|
117
|
+
|
|
118
|
+
expect({ action: got.action, reason: got.reason }).toEqual({
|
|
119
|
+
action: "unknown",
|
|
120
|
+
reason: "marketplace-unread",
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("declared latest, installed nowhere: install / absent, aiming at the catalog version", () => {
|
|
125
|
+
const idx = indexCatalog([info("bunjs@magus", "0.4.4", null)]);
|
|
126
|
+
|
|
127
|
+
const got = classifyPlugin("bunjs@magus", "latest", idx);
|
|
128
|
+
|
|
129
|
+
expect({
|
|
130
|
+
action: got.action,
|
|
131
|
+
reason: got.reason,
|
|
132
|
+
target: got.target,
|
|
133
|
+
}).toEqual({
|
|
134
|
+
action: "install",
|
|
135
|
+
reason: "absent",
|
|
136
|
+
target: "0.4.4",
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("installed behind the catalog: update / behind with the scope and its before-version", () => {
|
|
141
|
+
const idx = indexCatalog([info("designer@magus", "2.0.0", "1.5.0")]);
|
|
142
|
+
|
|
143
|
+
const got = classifyPlugin("designer@magus", "latest", idx);
|
|
144
|
+
|
|
145
|
+
expect({
|
|
146
|
+
action: got.action,
|
|
147
|
+
reason: got.reason,
|
|
148
|
+
target: got.target,
|
|
149
|
+
installed: got.installed,
|
|
150
|
+
available: got.available,
|
|
151
|
+
}).toEqual({
|
|
152
|
+
action: "update",
|
|
153
|
+
reason: "behind",
|
|
154
|
+
target: "2.0.0",
|
|
155
|
+
installed: "1.5.0",
|
|
156
|
+
available: "2.0.0",
|
|
157
|
+
});
|
|
158
|
+
expect(got.scopes).toContain("project");
|
|
159
|
+
expect(got.beforeVersions.project).toBe("1.5.0");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("installed equal to the catalog: current / up-to-date", () => {
|
|
163
|
+
const idx = indexCatalog([info("dev@magus", "7.4.0", "7.4.0")]);
|
|
164
|
+
|
|
165
|
+
const got = classifyPlugin("dev@magus", "latest", idx);
|
|
166
|
+
|
|
167
|
+
expect({ action: got.action, reason: got.reason }).toEqual({
|
|
168
|
+
action: "current",
|
|
169
|
+
reason: "up-to-date",
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Report #1 asked for the opposite of this, and that decision was REVERSED:
|
|
174
|
+
// a catalog that publishes no version leaves the install `current`, because
|
|
175
|
+
// there is no newer version to be behind. The reason code still records WHY
|
|
176
|
+
// the verdict was reached, so the state is not lost — it just stops being
|
|
177
|
+
// reported as a problem the reader must act on.
|
|
178
|
+
test("unversioned catalog with a commit-sha install is current, with the reason kept", () => {
|
|
179
|
+
const idx = indexCatalog([
|
|
180
|
+
info("code-review@claude-plugins-official", null, "3ea32df27be7"),
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
const got = classifyPlugin(
|
|
184
|
+
"code-review@claude-plugins-official",
|
|
185
|
+
"latest",
|
|
186
|
+
idx,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
expect({ action: got.action, reason: got.reason }).toEqual({
|
|
190
|
+
action: "current",
|
|
191
|
+
reason: "no-published-version",
|
|
192
|
+
});
|
|
193
|
+
// No note: nothing here is actionable, so there is nothing to tell anyone.
|
|
194
|
+
expect(got.note).toBeUndefined();
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("a failed catalog check on a latest pin is unknown / catalog-unverified, never current", () => {
|
|
198
|
+
const idx = indexCatalog([
|
|
199
|
+
info("claudish@magus", "2.0.0", "2.0.0", { updateCheckFailed: true }),
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
const got = classifyPlugin("claudish@magus", "latest", idx);
|
|
203
|
+
|
|
204
|
+
expect({ action: got.action, reason: got.reason }).toEqual({
|
|
205
|
+
action: "unknown",
|
|
206
|
+
reason: "catalog-unverified",
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("every unknown item carries a note and no target", () => {
|
|
211
|
+
const idx = indexCatalog([
|
|
212
|
+
info("a@magus", null, "abc123def456"),
|
|
213
|
+
info("b@magus", null, "1.0.0", { isOrphaned: true }),
|
|
214
|
+
info("c@magus", "1.0.0", "1.0.0", { updateCheckFailed: true }),
|
|
215
|
+
info("e@nowhere", null, "0.3.0", { isOrphaned: true }),
|
|
216
|
+
]);
|
|
217
|
+
|
|
218
|
+
// `a@magus` is deliberately NOT in this list: a catalog that publishes no
|
|
219
|
+
// version is `current` now, not unknown. It is the negative control
|
|
220
|
+
// below — without it this loop would pass even if every state collapsed
|
|
221
|
+
// into one.
|
|
222
|
+
for (const id of ["b@magus", "c@magus", "d@magus", "e@nowhere"]) {
|
|
223
|
+
const got = classifyPlugin(id, "latest", idx);
|
|
224
|
+
expect({ id, action: got.action }).toEqual({ id, action: "unknown" });
|
|
225
|
+
expect({ id, target: got.target, hasNote: Boolean(got.note) }).toEqual({
|
|
226
|
+
id,
|
|
227
|
+
target: null,
|
|
228
|
+
hasNote: true,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// The control: same catalog, same call, the opposite verdict — and
|
|
233
|
+
// crucially no note, because nothing about it is actionable.
|
|
234
|
+
const a = classifyPlugin("a@magus", "latest", idx);
|
|
235
|
+
expect(a.action).toBe("current");
|
|
236
|
+
expect(a.note).toBeUndefined();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("planPluginUpdates is classifyPlugin over every pin, sorted", () => {
|
|
241
|
+
test("output is sorted by pluginId and equals classifyPlugin item for item", () => {
|
|
242
|
+
const catalog = [
|
|
243
|
+
info("zeta@magus", "2.0.0", "1.0.0"),
|
|
244
|
+
info("alpha@magus", "1.0.0", "1.0.0"),
|
|
245
|
+
info("mid@magus", null, "abc123def456"),
|
|
246
|
+
];
|
|
247
|
+
const pins = {
|
|
248
|
+
"zeta@magus": "latest",
|
|
249
|
+
"alpha@magus": "latest",
|
|
250
|
+
"mid@magus": "latest",
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const plan = planPluginUpdates(
|
|
254
|
+
pins,
|
|
255
|
+
new Map(catalog.map((c) => [c.id, c])),
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
expect(plan.map((p) => p.pluginId)).toEqual([
|
|
259
|
+
"alpha@magus",
|
|
260
|
+
"mid@magus",
|
|
261
|
+
"zeta@magus",
|
|
262
|
+
]);
|
|
263
|
+
const idx = indexCatalog(catalog);
|
|
264
|
+
for (const p of plan) {
|
|
265
|
+
expect(p).toEqual(
|
|
266
|
+
classifyPlugin(p.pluginId, pins[p.pluginId as keyof typeof pins], idx),
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("pinForRow / classifyPluginRows", () => {
|
|
273
|
+
const declared = info("dev@magus", "1.0.0", "1.0.0");
|
|
274
|
+
const installedOnly = info("extra@magus", "1.0.0", "1.0.0");
|
|
275
|
+
const enabledOnly = info("en@magus", "1.0.0", null, {
|
|
276
|
+
enabled: true,
|
|
277
|
+
userScope: { enabled: true },
|
|
278
|
+
});
|
|
279
|
+
const browseOnly = info("browse@magus", "1.0.0", null);
|
|
280
|
+
const pins = { "dev@magus": "1.0.0" };
|
|
281
|
+
|
|
282
|
+
test("closure pin wins; installed or enabled rows are judged against latest; browse-only has no pin", () => {
|
|
283
|
+
expect(pinForRow(declared, pins)).toBe("1.0.0");
|
|
284
|
+
expect(pinForRow(installedOnly, pins)).toBe("latest");
|
|
285
|
+
expect(pinForRow(enabledOnly, pins)).toBe("latest");
|
|
286
|
+
expect(pinForRow(browseOnly, pins)).toBeNull();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("classifyPluginRows has one status per pinned row and none for browse-only rows", () => {
|
|
290
|
+
const rows = classifyPluginRows(
|
|
291
|
+
[declared, installedOnly, enabledOnly, browseOnly],
|
|
292
|
+
pins,
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
expect([...rows.keys()].sort()).toEqual([
|
|
296
|
+
"dev@magus",
|
|
297
|
+
"en@magus",
|
|
298
|
+
"extra@magus",
|
|
299
|
+
]);
|
|
300
|
+
expect(rows.get("en@magus")?.action).toBe("install");
|
|
301
|
+
expect(rows.get("en@magus")?.reason).toBe("enabled-not-installed");
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
describe("countPluginStates", () => {
|
|
306
|
+
test("tallies each state exactly", () => {
|
|
307
|
+
const counts = countPluginStates([
|
|
308
|
+
item({ action: "install", reason: "absent" }),
|
|
309
|
+
item({ action: "update", reason: "behind" }),
|
|
310
|
+
item({ action: "update", reason: "behind" }),
|
|
311
|
+
item({ action: "repair", reason: "content-stale" }),
|
|
312
|
+
item({ action: "current" }),
|
|
313
|
+
item({ action: "unknown", reason: "no-published-version" }),
|
|
314
|
+
]);
|
|
315
|
+
|
|
316
|
+
expect(counts).toEqual({
|
|
317
|
+
install: 1,
|
|
318
|
+
update: 2,
|
|
319
|
+
repair: 1,
|
|
320
|
+
current: 1,
|
|
321
|
+
unknown: 1,
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("an empty input counts zero everywhere", () => {
|
|
326
|
+
expect(countPluginStates([])).toEqual({
|
|
327
|
+
install: 0,
|
|
328
|
+
update: 0,
|
|
329
|
+
repair: 0,
|
|
330
|
+
current: 0,
|
|
331
|
+
unknown: 0,
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe("planIsBehind — plugin actions at the --check gate", () => {
|
|
337
|
+
function planOf(plugins: PluginUpdateItem[]): UpdatePlan {
|
|
338
|
+
return {
|
|
339
|
+
profileId: "default",
|
|
340
|
+
plugins,
|
|
341
|
+
bins: [],
|
|
342
|
+
skills: [],
|
|
343
|
+
binSpecs: new Map(),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
test("install, update and repair fail the gate; current and unknown do not", () => {
|
|
348
|
+
// The unknown row is the point: "could not be checked" is not evidence of
|
|
349
|
+
// drift, and for a marketplace that publishes no version it never will be,
|
|
350
|
+
// so the same rule as an unknown tool applies. It is still not APPLIED,
|
|
351
|
+
// which is planHasWork's business, not this gate's.
|
|
352
|
+
const table: Array<[PluginUpdateItem["action"], boolean]> = [
|
|
353
|
+
["install", true],
|
|
354
|
+
["update", true],
|
|
355
|
+
["repair", true],
|
|
356
|
+
["current", false],
|
|
357
|
+
["unknown", false],
|
|
358
|
+
];
|
|
359
|
+
for (const [action, behind] of table) {
|
|
360
|
+
const plan = planOf([item({ action })]);
|
|
361
|
+
expect({ action, behind: planIsBehind(plan) }).toEqual({
|
|
362
|
+
action,
|
|
363
|
+
behind,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("one unknown among current rows does not fail the gate; one update does", () => {
|
|
369
|
+
const unknowns = planOf([
|
|
370
|
+
item({ action: "current" }),
|
|
371
|
+
item({ action: "unknown", reason: "no-published-version" }),
|
|
372
|
+
]);
|
|
373
|
+
expect(planIsBehind(unknowns)).toBe(false);
|
|
374
|
+
expect(planHasWork(unknowns)).toBe(true);
|
|
375
|
+
|
|
376
|
+
const withUpdate = planOf([
|
|
377
|
+
item({ action: "unknown", reason: "no-published-version" }),
|
|
378
|
+
item({ action: "update", reason: "behind" }),
|
|
379
|
+
]);
|
|
380
|
+
expect(planIsBehind(withUpdate)).toBe(true);
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
describe("scopeIsBehind", () => {
|
|
385
|
+
test("true only for an update whose scopes include that scope", () => {
|
|
386
|
+
const upd = item({
|
|
387
|
+
action: "update",
|
|
388
|
+
reason: "behind",
|
|
389
|
+
scopes: ["project"],
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
expect(scopeIsBehind(upd, "project")).toBe(true);
|
|
393
|
+
expect(scopeIsBehind(upd, "user")).toBe(false);
|
|
394
|
+
expect(
|
|
395
|
+
scopeIsBehind(
|
|
396
|
+
item({ action: "unknown", scopes: ["project"] }),
|
|
397
|
+
"project",
|
|
398
|
+
),
|
|
399
|
+
).toBe(false);
|
|
400
|
+
expect(scopeIsBehind(null, "project")).toBe(false);
|
|
401
|
+
expect(scopeIsBehind(undefined, "user")).toBe(false);
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
describe("pluginStatusText — the TUI suffix contract (§8.7)", () => {
|
|
406
|
+
test("null status: empty, muted", () => {
|
|
407
|
+
expect(pluginStatusText(null)).toEqual({ suffix: "", tone: "muted" });
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("unknown: ' ? <note>' with the note verbatim", () => {
|
|
411
|
+
const note =
|
|
412
|
+
"its marketplace publishes no version for this plugin, so there is nothing to compare";
|
|
413
|
+
expect(
|
|
414
|
+
pluginStatusText(
|
|
415
|
+
item({ action: "unknown", reason: "no-published-version", note }),
|
|
416
|
+
).suffix,
|
|
417
|
+
).toBe(` ? ${note}`);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("update: ' → v<target> available', plus ' (<note>)' when a note is set", () => {
|
|
421
|
+
expect(
|
|
422
|
+
pluginStatusText(
|
|
423
|
+
item({ action: "update", reason: "behind", target: "2.0.0" }),
|
|
424
|
+
).suffix,
|
|
425
|
+
).toBe(" → v2.0.0 available");
|
|
426
|
+
expect(
|
|
427
|
+
pluginStatusText(
|
|
428
|
+
item({
|
|
429
|
+
action: "update",
|
|
430
|
+
reason: "pin-undeliverable",
|
|
431
|
+
target: "1.0.0",
|
|
432
|
+
note: "pin n",
|
|
433
|
+
}),
|
|
434
|
+
).suffix,
|
|
435
|
+
).toBe(" → v1.0.0 available (pin n)");
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test("repair: ' stale — reinstall (<note>)'", () => {
|
|
439
|
+
expect(
|
|
440
|
+
pluginStatusText(
|
|
441
|
+
item({
|
|
442
|
+
action: "repair",
|
|
443
|
+
reason: "content-stale",
|
|
444
|
+
note: "files moved",
|
|
445
|
+
}),
|
|
446
|
+
).suffix,
|
|
447
|
+
).toBe(" stale — reinstall (files moved)");
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test("install: absent claims the profile declares it; enabled-not-installed shows its note instead", () => {
|
|
451
|
+
expect(
|
|
452
|
+
pluginStatusText(
|
|
453
|
+
item({ action: "install", reason: "absent", installed: null }),
|
|
454
|
+
).suffix,
|
|
455
|
+
).toBe(" not installed — profile declares it");
|
|
456
|
+
const enabled = pluginStatusText(
|
|
457
|
+
item({
|
|
458
|
+
action: "install",
|
|
459
|
+
reason: "enabled-not-installed",
|
|
460
|
+
installed: null,
|
|
461
|
+
note: "enabled in user settings",
|
|
462
|
+
}),
|
|
463
|
+
).suffix;
|
|
464
|
+
expect(enabled).toBe(" not installed (enabled in user settings)");
|
|
465
|
+
expect(enabled).not.toContain("profile declares");
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("current: empty, or ' (<note>)' for pin-undeliverable", () => {
|
|
469
|
+
expect(pluginStatusText(item({ action: "current" })).suffix).toBe("");
|
|
470
|
+
expect(
|
|
471
|
+
pluginStatusText(
|
|
472
|
+
item({ action: "current", reason: "pin-undeliverable", note: "pin n" }),
|
|
473
|
+
).suffix,
|
|
474
|
+
).toBe(" (pin n)");
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
describe("pluginRowText", () => {
|
|
479
|
+
function rowItem(
|
|
480
|
+
plugin: PluginInfo,
|
|
481
|
+
status: PluginUpdateItem | null,
|
|
482
|
+
): PluginPluginItem {
|
|
483
|
+
return {
|
|
484
|
+
id: `plugin:${plugin.id}`,
|
|
485
|
+
kind: "plugin",
|
|
486
|
+
label: plugin.name,
|
|
487
|
+
plugin,
|
|
488
|
+
isExpandable: false,
|
|
489
|
+
isExpanded: false,
|
|
490
|
+
status,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// The gate this guards: every status clause used to be conditional on the
|
|
495
|
+
// installed version being displayable, so a plugin recorded as "0.0.0"
|
|
496
|
+
// printed nothing after its name and read as healthy.
|
|
497
|
+
//
|
|
498
|
+
// The fixture is an ORPHAN at 0.0.0, not a plain 0.0.0 install. A plain one
|
|
499
|
+
// is `current` with no note now, which would leave nothing for this test to
|
|
500
|
+
// find and let the gate come back unnoticed.
|
|
501
|
+
test("a note renders on a 0.0.0 install — no version gate", () => {
|
|
502
|
+
const plugin = info("zero-version@magus", null, "0.0.0", {
|
|
503
|
+
isOrphaned: true,
|
|
504
|
+
});
|
|
505
|
+
const sibling = info("dev@magus", "1.0.0", "1.0.0");
|
|
506
|
+
const status = classifyPlugin(
|
|
507
|
+
plugin.id,
|
|
508
|
+
"latest",
|
|
509
|
+
indexCatalog([plugin, sibling]),
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
const out = pluginRowText(rowItem(plugin, status));
|
|
513
|
+
|
|
514
|
+
expect(status.action).toBe("unknown");
|
|
515
|
+
expect(status.note).toBeTruthy();
|
|
516
|
+
expect(out.text).toContain(status.note ?? "<missing note>");
|
|
517
|
+
// The undisplayable version itself is still never printed.
|
|
518
|
+
expect(out.text).not.toContain("0.0.0");
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("text is exactly the segments joined, and contains the status suffix", () => {
|
|
522
|
+
const plugin = info("designer@magus", "2.0.0", "1.5.0");
|
|
523
|
+
const status = classifyPlugin(plugin.id, "latest", indexCatalog([plugin]));
|
|
524
|
+
|
|
525
|
+
const out = pluginRowText(rowItem(plugin, status));
|
|
526
|
+
|
|
527
|
+
expect(out.text).toBe(out.segments.map((s) => s.text).join(""));
|
|
528
|
+
expect(out.text).toContain(pluginStatusText(status).suffix);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
test("an orphan row renders its note", () => {
|
|
532
|
+
const plugin = info("legacy@magus", null, "0.5.0", { isOrphaned: true });
|
|
533
|
+
const sibling = info("dev@magus", "1.0.0", "1.0.0");
|
|
534
|
+
const status = classifyPlugin(
|
|
535
|
+
plugin.id,
|
|
536
|
+
"latest",
|
|
537
|
+
indexCatalog([plugin, sibling]),
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
const out = pluginRowText(rowItem(plugin, status));
|
|
541
|
+
|
|
542
|
+
expect(status.action).toBe("unknown");
|
|
543
|
+
expect(out.text).toContain(status.note ?? "<missing note>");
|
|
544
|
+
});
|
|
545
|
+
});
|