From 7e66a8fcfe32ad59c4358bc27df9966ac0e2c801 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 11 Apr 2026 08:12:34 +0100 Subject: [PATCH] test: move plugin uninstall selection to pure tests --- src/cli/plugins-cli.ts | 40 +----------- src/cli/plugins-cli.uninstall.test.ts | 70 --------------------- src/cli/plugins-uninstall-selection.test.ts | 50 +++++++++++++++ src/cli/plugins-uninstall-selection.ts | 43 +++++++++++++ src/infra/clawhub-spec.ts | 24 +++++++ src/infra/clawhub.ts | 24 +------ 6 files changed, 119 insertions(+), 132 deletions(-) create mode 100644 src/cli/plugins-uninstall-selection.test.ts create mode 100644 src/cli/plugins-uninstall-selection.ts create mode 100644 src/infra/clawhub-spec.ts diff --git a/src/cli/plugins-cli.ts b/src/cli/plugins-cli.ts index 48ef9724d9d..a38ae6500b5 100644 --- a/src/cli/plugins-cli.ts +++ b/src/cli/plugins-cli.ts @@ -5,7 +5,6 @@ import type { OpenClawConfig } from "../config/config.js"; import { loadConfig, readConfigFileSnapshot, replaceConfigFile } from "../config/config.js"; import { resolveStateDir } from "../config/paths.js"; import type { PluginInstallRecord } from "../config/types.plugins.js"; -import { parseClawHubPluginSpec } from "../infra/clawhub.js"; import { enablePluginInConfig } from "../plugins/enable.js"; import { listMarketplacePlugins } from "../plugins/marketplace.js"; import type { PluginRecord } from "../plugins/registry.js"; @@ -36,6 +35,7 @@ import { } from "./plugins-command-helpers.js"; import { setPluginEnabledInConfig } from "./plugins-config.js"; import { runPluginInstallCommand } from "./plugins-install-command.js"; +import { resolvePluginUninstallId } from "./plugins-uninstall-selection.js"; import { runPluginUpdateCommand } from "./plugins-update-command.js"; import { promptYesNo } from "./prompt.js"; @@ -67,44 +67,6 @@ export type PluginUninstallOptions = { dryRun?: boolean; }; -function resolvePluginUninstallId(params: { - rawId: string; - config: OpenClawConfig; - plugins: PluginRecord[]; -}): { pluginId: string; plugin?: PluginRecord } { - const rawId = params.rawId.trim(); - const plugin = params.plugins.find((entry) => entry.id === rawId || entry.name === rawId); - if (plugin) { - return { pluginId: plugin.id, plugin }; - } - - for (const [pluginId, install] of Object.entries(params.config.plugins?.installs ?? {})) { - if ( - install.spec === rawId || - install.resolvedSpec === rawId || - install.resolvedName === rawId || - install.marketplacePlugin === rawId - ) { - return { pluginId }; - } - } - - const requestedClawHub = parseClawHubPluginSpec(rawId); - if (requestedClawHub) { - for (const [pluginId, install] of Object.entries(params.config.plugins?.installs ?? {})) { - const installedClawHubName = - install.clawhubPackage ?? - parseClawHubPluginSpec(install.spec ?? "")?.name ?? - parseClawHubPluginSpec(install.resolvedSpec ?? "")?.name; - if (installedClawHubName === requestedClawHub.name) { - return { pluginId }; - } - } - } - - return { pluginId: rawId }; -} - function formatPluginLine(plugin: PluginRecord, verbose = false): string { const status = plugin.status === "loaded" diff --git a/src/cli/plugins-cli.uninstall.test.ts b/src/cli/plugins-cli.uninstall.test.ts index d735a1f0b3c..052f8a29bb6 100644 --- a/src/cli/plugins-cli.uninstall.test.ts +++ b/src/cli/plugins-cli.uninstall.test.ts @@ -4,7 +4,6 @@ import type { OpenClawConfig } from "../config/config.js"; import { buildPluginDiagnosticsReport, loadConfig, - parseClawHubPluginSpec, promptYesNo, resetPluginsCliTestState, runPluginsCommand, @@ -123,73 +122,4 @@ describe("plugins cli uninstall", () => { expect(runtimeErrors.at(-1)).toContain("is not managed by plugins config/install records"); expect(uninstallPlugin).not.toHaveBeenCalled(); }); - - it("accepts the recorded ClawHub spec as an uninstall target", async () => { - loadConfig.mockReturnValue({ - plugins: { - entries: { - "linkmind-context": { enabled: true }, - }, - installs: { - "linkmind-context": { - source: "npm", - spec: "clawhub:linkmind-context", - clawhubPackage: "linkmind-context", - }, - }, - }, - } as OpenClawConfig); - buildPluginDiagnosticsReport.mockReturnValue({ - plugins: [{ id: "linkmind-context", name: "linkmind-context" }], - diagnostics: [], - }); - parseClawHubPluginSpec.mockImplementation((raw: string) => - raw === "clawhub:linkmind-context" ? { name: "linkmind-context" } : null, - ); - - await runPluginsCommand(["plugins", "uninstall", "clawhub:linkmind-context", "--force"]); - - expect(uninstallPlugin).toHaveBeenCalledWith( - expect.objectContaining({ - pluginId: "linkmind-context", - }), - ); - }); - - it("accepts a versionless ClawHub spec when the install was pinned", async () => { - loadConfig.mockReturnValue({ - plugins: { - entries: { - "linkmind-context": { enabled: true }, - }, - installs: { - "linkmind-context": { - source: "npm", - spec: "clawhub:linkmind-context@1.2.3", - }, - }, - }, - } as OpenClawConfig); - buildPluginDiagnosticsReport.mockReturnValue({ - plugins: [{ id: "linkmind-context", name: "linkmind-context" }], - diagnostics: [], - }); - parseClawHubPluginSpec.mockImplementation((raw: string) => { - if (raw === "clawhub:linkmind-context") { - return { name: "linkmind-context" }; - } - if (raw === "clawhub:linkmind-context@1.2.3") { - return { name: "linkmind-context", version: "1.2.3" }; - } - return null; - }); - - await runPluginsCommand(["plugins", "uninstall", "clawhub:linkmind-context", "--force"]); - - expect(uninstallPlugin).toHaveBeenCalledWith( - expect.objectContaining({ - pluginId: "linkmind-context", - }), - ); - }); }); diff --git a/src/cli/plugins-uninstall-selection.test.ts b/src/cli/plugins-uninstall-selection.test.ts new file mode 100644 index 00000000000..c91f20f07aa --- /dev/null +++ b/src/cli/plugins-uninstall-selection.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from "vitest"; +import type { OpenClawConfig } from "../config/config.js"; +import { resolvePluginUninstallId } from "./plugins-uninstall-selection.js"; + +describe("resolvePluginUninstallId", () => { + it("accepts the recorded ClawHub spec as an uninstall target", () => { + const result = resolvePluginUninstallId({ + rawId: "clawhub:linkmind-context", + config: { + plugins: { + entries: { + "linkmind-context": { enabled: true }, + }, + installs: { + "linkmind-context": { + source: "npm", + spec: "clawhub:linkmind-context", + clawhubPackage: "linkmind-context", + }, + }, + }, + } as OpenClawConfig, + plugins: [{ id: "linkmind-context", name: "linkmind-context" }], + }); + + expect(result.pluginId).toBe("linkmind-context"); + }); + + it("accepts a versionless ClawHub spec when the install was pinned", () => { + const result = resolvePluginUninstallId({ + rawId: "clawhub:linkmind-context", + config: { + plugins: { + entries: { + "linkmind-context": { enabled: true }, + }, + installs: { + "linkmind-context": { + source: "npm", + spec: "clawhub:linkmind-context@1.2.3", + }, + }, + }, + } as OpenClawConfig, + plugins: [{ id: "linkmind-context", name: "linkmind-context" }], + }); + + expect(result.pluginId).toBe("linkmind-context"); + }); +}); diff --git a/src/cli/plugins-uninstall-selection.ts b/src/cli/plugins-uninstall-selection.ts new file mode 100644 index 00000000000..0332a6136c5 --- /dev/null +++ b/src/cli/plugins-uninstall-selection.ts @@ -0,0 +1,43 @@ +import type { OpenClawConfig } from "../config/config.js"; +import { parseClawHubPluginSpec } from "../infra/clawhub-spec.js"; +import type { PluginRecord } from "../plugins/registry.js"; + +export function resolvePluginUninstallId< + TPlugin extends Pick, +>(params: { + rawId: string; + config: OpenClawConfig; + plugins: TPlugin[]; +}): { pluginId: string; plugin?: TPlugin } { + const rawId = params.rawId.trim(); + const plugin = params.plugins.find((entry) => entry.id === rawId || entry.name === rawId); + if (plugin) { + return { pluginId: plugin.id, plugin }; + } + + for (const [pluginId, install] of Object.entries(params.config.plugins?.installs ?? {})) { + if ( + install.spec === rawId || + install.resolvedSpec === rawId || + install.resolvedName === rawId || + install.marketplacePlugin === rawId + ) { + return { pluginId }; + } + } + + const requestedClawHub = parseClawHubPluginSpec(rawId); + if (requestedClawHub) { + for (const [pluginId, install] of Object.entries(params.config.plugins?.installs ?? {})) { + const installedClawHubName = + install.clawhubPackage ?? + parseClawHubPluginSpec(install.spec ?? "")?.name ?? + parseClawHubPluginSpec(install.resolvedSpec ?? "")?.name; + if (installedClawHubName === requestedClawHub.name) { + return { pluginId }; + } + } + } + + return { pluginId: rawId }; +} diff --git a/src/infra/clawhub-spec.ts b/src/infra/clawhub-spec.ts new file mode 100644 index 00000000000..6e7ac3defbb --- /dev/null +++ b/src/infra/clawhub-spec.ts @@ -0,0 +1,24 @@ +import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; + +export function parseClawHubPluginSpec(raw: string): { + name: string; + version?: string; + baseUrl?: string; +} | null { + const trimmed = raw.trim(); + if (!normalizeLowercaseStringOrEmpty(trimmed).startsWith("clawhub:")) { + return null; + } + const spec = trimmed.slice("clawhub:".length).trim(); + if (!spec) { + return null; + } + const atIndex = spec.lastIndexOf("@"); + if (atIndex <= 0 || atIndex >= spec.length - 1) { + return { name: spec }; + } + return { + name: spec.slice(0, atIndex).trim(), + version: spec.slice(atIndex + 1).trim() || undefined, + }; +} diff --git a/src/infra/clawhub.ts b/src/infra/clawhub.ts index b48962ed3f3..c9b1ab05fce 100644 --- a/src/infra/clawhub.ts +++ b/src/infra/clawhub.ts @@ -9,6 +9,7 @@ import { import { isAtLeast, parseSemver } from "./runtime-guard.js"; import { compareComparableSemver, parseComparableSemver } from "./semver-compare.js"; import { createTempDownloadTarget } from "./temp-download.js"; +export { parseClawHubPluginSpec } from "./clawhub-spec.js"; const DEFAULT_CLAWHUB_URL = "https://clawhub.ai"; const DEFAULT_FETCH_TIMEOUT_MS = 30_000; @@ -453,29 +454,6 @@ export function normalizeClawHubSha256Hex(value: string): string | null { return normalizeLowercaseStringOrEmpty(trimmed); } -export function parseClawHubPluginSpec(raw: string): { - name: string; - version?: string; - baseUrl?: string; -} | null { - const trimmed = raw.trim(); - if (!normalizeLowercaseStringOrEmpty(trimmed).startsWith("clawhub:")) { - return null; - } - const spec = trimmed.slice("clawhub:".length).trim(); - if (!spec) { - return null; - } - const atIndex = spec.lastIndexOf("@"); - if (atIndex <= 0 || atIndex >= spec.length - 1) { - return { name: spec }; - } - return { - name: spec.slice(0, atIndex).trim(), - version: spec.slice(atIndex + 1).trim() || undefined, - }; -} - export async function fetchClawHubPackageDetail(params: { name: string; baseUrl?: string;