mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-29 01:52:04 +00:00
test: move plugin update selection to pure tests
This commit is contained in:
@@ -138,105 +138,6 @@ describe("plugins cli update", () => {
|
||||
expect(runtimeLogs.at(-1)).toBe("No tracked plugins or hook packs to update.");
|
||||
});
|
||||
|
||||
it("maps an explicit unscoped npm dist-tag update to the tracked plugin id", async () => {
|
||||
const config = {
|
||||
plugins: {
|
||||
installs: {
|
||||
"openclaw-codex-app-server": {
|
||||
source: "npm",
|
||||
spec: "openclaw-codex-app-server",
|
||||
installPath: "/tmp/openclaw-codex-app-server",
|
||||
resolvedName: "openclaw-codex-app-server",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
loadConfig.mockReturnValue(config);
|
||||
updateNpmInstalledPlugins.mockResolvedValue({
|
||||
config,
|
||||
changed: false,
|
||||
outcomes: [],
|
||||
});
|
||||
|
||||
await runPluginsCommand(["plugins", "update", "openclaw-codex-app-server@beta"]);
|
||||
|
||||
expect(updateNpmInstalledPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config,
|
||||
pluginIds: ["openclaw-codex-app-server"],
|
||||
specOverrides: {
|
||||
"openclaw-codex-app-server": "openclaw-codex-app-server@beta",
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("maps an explicit scoped npm dist-tag update to the tracked plugin id", async () => {
|
||||
const config = {
|
||||
plugins: {
|
||||
installs: {
|
||||
"voice-call": {
|
||||
source: "npm",
|
||||
spec: "@openclaw/voice-call",
|
||||
installPath: "/tmp/voice-call",
|
||||
resolvedName: "@openclaw/voice-call",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
loadConfig.mockReturnValue(config);
|
||||
updateNpmInstalledPlugins.mockResolvedValue({
|
||||
config,
|
||||
changed: false,
|
||||
outcomes: [],
|
||||
});
|
||||
|
||||
await runPluginsCommand(["plugins", "update", "@openclaw/voice-call@beta"]);
|
||||
|
||||
expect(updateNpmInstalledPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config,
|
||||
pluginIds: ["voice-call"],
|
||||
specOverrides: {
|
||||
"voice-call": "@openclaw/voice-call@beta",
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("maps an explicit npm version update to the tracked plugin id", async () => {
|
||||
const config = {
|
||||
plugins: {
|
||||
installs: {
|
||||
"openclaw-codex-app-server": {
|
||||
source: "npm",
|
||||
spec: "openclaw-codex-app-server",
|
||||
installPath: "/tmp/openclaw-codex-app-server",
|
||||
resolvedName: "openclaw-codex-app-server",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
loadConfig.mockReturnValue(config);
|
||||
updateNpmInstalledPlugins.mockResolvedValue({
|
||||
config,
|
||||
changed: false,
|
||||
outcomes: [],
|
||||
});
|
||||
|
||||
await runPluginsCommand(["plugins", "update", "openclaw-codex-app-server@0.2.0-beta.4"]);
|
||||
|
||||
expect(updateNpmInstalledPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config,
|
||||
pluginIds: ["openclaw-codex-app-server"],
|
||||
specOverrides: {
|
||||
"openclaw-codex-app-server": "openclaw-codex-app-server@0.2.0-beta.4",
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes dangerous force unsafe install to plugin updates", async () => {
|
||||
const config = createTrackedPluginConfig({
|
||||
pluginId: "openclaw-codex-app-server",
|
||||
@@ -265,41 +166,6 @@ describe("plugins cli update", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps using the recorded npm tag when update is invoked by plugin id", async () => {
|
||||
const config = {
|
||||
plugins: {
|
||||
installs: {
|
||||
"openclaw-codex-app-server": {
|
||||
source: "npm",
|
||||
spec: "openclaw-codex-app-server@beta",
|
||||
installPath: "/tmp/openclaw-codex-app-server",
|
||||
resolvedName: "openclaw-codex-app-server",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
loadConfig.mockReturnValue(config);
|
||||
updateNpmInstalledPlugins.mockResolvedValue({
|
||||
config,
|
||||
changed: false,
|
||||
outcomes: [],
|
||||
});
|
||||
|
||||
await runPluginsCommand(["plugins", "update", "openclaw-codex-app-server"]);
|
||||
|
||||
expect(updateNpmInstalledPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
config,
|
||||
pluginIds: ["openclaw-codex-app-server"],
|
||||
}),
|
||||
);
|
||||
expect(updateNpmInstalledPlugins).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
specOverrides: expect.anything(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("writes updated config when updater reports changes", async () => {
|
||||
const cfg = {
|
||||
plugins: {
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { HookInstallRecord } from "../config/types.hooks.js";
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
import { parseRegistryNpmSpec } from "../infra/npm-registry-spec.js";
|
||||
import { CLAWHUB_INSTALL_ERROR_CODE } from "../plugins/clawhub.js";
|
||||
import { applyExclusiveSlotSelection } from "../plugins/slots.js";
|
||||
@@ -9,6 +7,11 @@ import { defaultRuntime } from "../runtime.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
|
||||
export {
|
||||
extractInstalledNpmHookPackageName,
|
||||
extractInstalledNpmPackageName,
|
||||
} from "./plugins-install-records.js";
|
||||
|
||||
type HookInternalEntryLike = Record<string, unknown> & { enabled?: boolean };
|
||||
|
||||
export function resolveFileNpmSpecToLocalPath(
|
||||
@@ -101,31 +104,6 @@ export function enableInternalHookEntries(
|
||||
};
|
||||
}
|
||||
|
||||
export function extractInstalledNpmPackageName(install: PluginInstallRecord): string | undefined {
|
||||
if (install.source !== "npm") {
|
||||
return undefined;
|
||||
}
|
||||
const resolvedName = install.resolvedName?.trim();
|
||||
if (resolvedName) {
|
||||
return resolvedName;
|
||||
}
|
||||
return (
|
||||
(install.spec ? parseRegistryNpmSpec(install.spec)?.name : undefined) ??
|
||||
(install.resolvedSpec ? parseRegistryNpmSpec(install.resolvedSpec)?.name : undefined)
|
||||
);
|
||||
}
|
||||
|
||||
export function extractInstalledNpmHookPackageName(install: HookInstallRecord): string | undefined {
|
||||
const resolvedName = install.resolvedName?.trim();
|
||||
if (resolvedName) {
|
||||
return resolvedName;
|
||||
}
|
||||
return (
|
||||
(install.spec ? parseRegistryNpmSpec(install.spec)?.name : undefined) ??
|
||||
(install.resolvedSpec ? parseRegistryNpmSpec(install.resolvedSpec)?.name : undefined)
|
||||
);
|
||||
}
|
||||
|
||||
export function formatPluginInstallWithHookFallbackError(
|
||||
pluginError: string,
|
||||
hookError: string,
|
||||
|
||||
28
src/cli/plugins-install-records.ts
Normal file
28
src/cli/plugins-install-records.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { HookInstallRecord } from "../config/types.hooks.js";
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
import { parseRegistryNpmSpec } from "../infra/npm-registry-spec.js";
|
||||
|
||||
export function extractInstalledNpmPackageName(install: PluginInstallRecord): string | undefined {
|
||||
if (install.source !== "npm") {
|
||||
return undefined;
|
||||
}
|
||||
const resolvedName = install.resolvedName?.trim();
|
||||
if (resolvedName) {
|
||||
return resolvedName;
|
||||
}
|
||||
return (
|
||||
(install.spec ? parseRegistryNpmSpec(install.spec)?.name : undefined) ??
|
||||
(install.resolvedSpec ? parseRegistryNpmSpec(install.resolvedSpec)?.name : undefined)
|
||||
);
|
||||
}
|
||||
|
||||
export function extractInstalledNpmHookPackageName(install: HookInstallRecord): string | undefined {
|
||||
const resolvedName = install.resolvedName?.trim();
|
||||
if (resolvedName) {
|
||||
return resolvedName;
|
||||
}
|
||||
return (
|
||||
(install.spec ? parseRegistryNpmSpec(install.spec)?.name : undefined) ??
|
||||
(install.resolvedSpec ? parseRegistryNpmSpec(install.resolvedSpec)?.name : undefined)
|
||||
);
|
||||
}
|
||||
@@ -1,92 +1,14 @@
|
||||
import { loadConfig, readConfigFileSnapshot, replaceConfigFile } from "../config/config.js";
|
||||
import type { HookInstallRecord } from "../config/types.hooks.js";
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
import { updateNpmInstalledHookPacks } from "../hooks/update.js";
|
||||
import { parseRegistryNpmSpec } from "../infra/npm-registry-spec.js";
|
||||
import { updateNpmInstalledPlugins } from "../plugins/update.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
import { theme } from "../terminal/theme.js";
|
||||
import {
|
||||
extractInstalledNpmHookPackageName,
|
||||
extractInstalledNpmPackageName,
|
||||
} from "./plugins-command-helpers.js";
|
||||
resolveHookPackUpdateSelection,
|
||||
resolvePluginUpdateSelection,
|
||||
} from "./plugins-update-selection.js";
|
||||
import { promptYesNo } from "./prompt.js";
|
||||
|
||||
function resolvePluginUpdateSelection(params: {
|
||||
installs: Record<string, PluginInstallRecord>;
|
||||
rawId?: string;
|
||||
all?: boolean;
|
||||
}): { pluginIds: string[]; specOverrides?: Record<string, string> } {
|
||||
if (params.all) {
|
||||
return { pluginIds: Object.keys(params.installs) };
|
||||
}
|
||||
if (!params.rawId) {
|
||||
return { pluginIds: [] };
|
||||
}
|
||||
|
||||
const parsedSpec = parseRegistryNpmSpec(params.rawId);
|
||||
if (!parsedSpec || parsedSpec.selectorKind === "none") {
|
||||
return { pluginIds: [params.rawId] };
|
||||
}
|
||||
|
||||
const matches = Object.entries(params.installs).filter(([, install]) => {
|
||||
return extractInstalledNpmPackageName(install) === parsedSpec.name;
|
||||
});
|
||||
if (matches.length !== 1) {
|
||||
return { pluginIds: [params.rawId] };
|
||||
}
|
||||
|
||||
const [pluginId] = matches[0];
|
||||
if (!pluginId) {
|
||||
return { pluginIds: [params.rawId] };
|
||||
}
|
||||
return {
|
||||
pluginIds: [pluginId],
|
||||
specOverrides: {
|
||||
[pluginId]: parsedSpec.raw,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function resolveHookPackUpdateSelection(params: {
|
||||
installs: Record<string, HookInstallRecord>;
|
||||
rawId?: string;
|
||||
all?: boolean;
|
||||
}): { hookIds: string[]; specOverrides?: Record<string, string> } {
|
||||
if (params.all) {
|
||||
return { hookIds: Object.keys(params.installs) };
|
||||
}
|
||||
if (!params.rawId) {
|
||||
return { hookIds: [] };
|
||||
}
|
||||
if (params.rawId in params.installs) {
|
||||
return { hookIds: [params.rawId] };
|
||||
}
|
||||
|
||||
const parsedSpec = parseRegistryNpmSpec(params.rawId);
|
||||
if (!parsedSpec || parsedSpec.selectorKind === "none") {
|
||||
return { hookIds: [] };
|
||||
}
|
||||
|
||||
const matches = Object.entries(params.installs).filter(([, install]) => {
|
||||
return extractInstalledNpmHookPackageName(install) === parsedSpec.name;
|
||||
});
|
||||
if (matches.length !== 1) {
|
||||
return { hookIds: [] };
|
||||
}
|
||||
|
||||
const [hookId] = matches[0];
|
||||
if (!hookId) {
|
||||
return { hookIds: [] };
|
||||
}
|
||||
return {
|
||||
hookIds: [hookId],
|
||||
specOverrides: {
|
||||
[hookId]: parsedSpec.raw,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function runPluginUpdateCommand(params: {
|
||||
id?: string;
|
||||
opts: { all?: boolean; dryRun?: boolean; dangerouslyForceUnsafeInstall?: boolean };
|
||||
|
||||
95
src/cli/plugins-update-selection.test.ts
Normal file
95
src/cli/plugins-update-selection.test.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
import { resolvePluginUpdateSelection } from "./plugins-update-selection.js";
|
||||
|
||||
function createNpmInstall(params: {
|
||||
spec: string;
|
||||
installPath?: string;
|
||||
resolvedName?: string;
|
||||
}): PluginInstallRecord {
|
||||
return {
|
||||
source: "npm",
|
||||
spec: params.spec,
|
||||
installPath: params.installPath ?? "/tmp/plugin",
|
||||
...(params.resolvedName ? { resolvedName: params.resolvedName } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
describe("resolvePluginUpdateSelection", () => {
|
||||
it("maps an explicit unscoped npm dist-tag update to the tracked plugin id", () => {
|
||||
expect(
|
||||
resolvePluginUpdateSelection({
|
||||
installs: {
|
||||
"openclaw-codex-app-server": createNpmInstall({
|
||||
spec: "openclaw-codex-app-server",
|
||||
installPath: "/tmp/openclaw-codex-app-server",
|
||||
resolvedName: "openclaw-codex-app-server",
|
||||
}),
|
||||
},
|
||||
rawId: "openclaw-codex-app-server@beta",
|
||||
}),
|
||||
).toEqual({
|
||||
pluginIds: ["openclaw-codex-app-server"],
|
||||
specOverrides: {
|
||||
"openclaw-codex-app-server": "openclaw-codex-app-server@beta",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("maps an explicit scoped npm dist-tag update to the tracked plugin id", () => {
|
||||
expect(
|
||||
resolvePluginUpdateSelection({
|
||||
installs: {
|
||||
"voice-call": createNpmInstall({
|
||||
spec: "@openclaw/voice-call",
|
||||
installPath: "/tmp/voice-call",
|
||||
resolvedName: "@openclaw/voice-call",
|
||||
}),
|
||||
},
|
||||
rawId: "@openclaw/voice-call@beta",
|
||||
}),
|
||||
).toEqual({
|
||||
pluginIds: ["voice-call"],
|
||||
specOverrides: {
|
||||
"voice-call": "@openclaw/voice-call@beta",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("maps an explicit npm version update to the tracked plugin id", () => {
|
||||
expect(
|
||||
resolvePluginUpdateSelection({
|
||||
installs: {
|
||||
"openclaw-codex-app-server": createNpmInstall({
|
||||
spec: "openclaw-codex-app-server",
|
||||
installPath: "/tmp/openclaw-codex-app-server",
|
||||
resolvedName: "openclaw-codex-app-server",
|
||||
}),
|
||||
},
|
||||
rawId: "openclaw-codex-app-server@0.2.0-beta.4",
|
||||
}),
|
||||
).toEqual({
|
||||
pluginIds: ["openclaw-codex-app-server"],
|
||||
specOverrides: {
|
||||
"openclaw-codex-app-server": "openclaw-codex-app-server@0.2.0-beta.4",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps recorded npm tags when update is invoked by plugin id", () => {
|
||||
expect(
|
||||
resolvePluginUpdateSelection({
|
||||
installs: {
|
||||
"openclaw-codex-app-server": createNpmInstall({
|
||||
spec: "openclaw-codex-app-server@beta",
|
||||
installPath: "/tmp/openclaw-codex-app-server",
|
||||
resolvedName: "openclaw-codex-app-server",
|
||||
}),
|
||||
},
|
||||
rawId: "openclaw-codex-app-server",
|
||||
}),
|
||||
).toEqual({
|
||||
pluginIds: ["openclaw-codex-app-server"],
|
||||
});
|
||||
});
|
||||
});
|
||||
82
src/cli/plugins-update-selection.ts
Normal file
82
src/cli/plugins-update-selection.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { HookInstallRecord } from "../config/types.hooks.js";
|
||||
import type { PluginInstallRecord } from "../config/types.plugins.js";
|
||||
import { parseRegistryNpmSpec } from "../infra/npm-registry-spec.js";
|
||||
import {
|
||||
extractInstalledNpmHookPackageName,
|
||||
extractInstalledNpmPackageName,
|
||||
} from "./plugins-install-records.js";
|
||||
|
||||
export function resolvePluginUpdateSelection(params: {
|
||||
installs: Record<string, PluginInstallRecord>;
|
||||
rawId?: string;
|
||||
all?: boolean;
|
||||
}): { pluginIds: string[]; specOverrides?: Record<string, string> } {
|
||||
if (params.all) {
|
||||
return { pluginIds: Object.keys(params.installs) };
|
||||
}
|
||||
if (!params.rawId) {
|
||||
return { pluginIds: [] };
|
||||
}
|
||||
|
||||
const parsedSpec = parseRegistryNpmSpec(params.rawId);
|
||||
if (!parsedSpec || parsedSpec.selectorKind === "none") {
|
||||
return { pluginIds: [params.rawId] };
|
||||
}
|
||||
|
||||
const matches = Object.entries(params.installs).filter(([, install]) => {
|
||||
return extractInstalledNpmPackageName(install) === parsedSpec.name;
|
||||
});
|
||||
if (matches.length !== 1) {
|
||||
return { pluginIds: [params.rawId] };
|
||||
}
|
||||
|
||||
const [pluginId] = matches[0];
|
||||
if (!pluginId) {
|
||||
return { pluginIds: [params.rawId] };
|
||||
}
|
||||
return {
|
||||
pluginIds: [pluginId],
|
||||
specOverrides: {
|
||||
[pluginId]: parsedSpec.raw,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveHookPackUpdateSelection(params: {
|
||||
installs: Record<string, HookInstallRecord>;
|
||||
rawId?: string;
|
||||
all?: boolean;
|
||||
}): { hookIds: string[]; specOverrides?: Record<string, string> } {
|
||||
if (params.all) {
|
||||
return { hookIds: Object.keys(params.installs) };
|
||||
}
|
||||
if (!params.rawId) {
|
||||
return { hookIds: [] };
|
||||
}
|
||||
if (params.rawId in params.installs) {
|
||||
return { hookIds: [params.rawId] };
|
||||
}
|
||||
|
||||
const parsedSpec = parseRegistryNpmSpec(params.rawId);
|
||||
if (!parsedSpec || parsedSpec.selectorKind === "none") {
|
||||
return { hookIds: [] };
|
||||
}
|
||||
|
||||
const matches = Object.entries(params.installs).filter(([, install]) => {
|
||||
return extractInstalledNpmHookPackageName(install) === parsedSpec.name;
|
||||
});
|
||||
if (matches.length !== 1) {
|
||||
return { hookIds: [] };
|
||||
}
|
||||
|
||||
const [hookId] = matches[0];
|
||||
if (!hookId) {
|
||||
return { hookIds: [] };
|
||||
}
|
||||
return {
|
||||
hookIds: [hookId],
|
||||
specOverrides: {
|
||||
[hookId]: parsedSpec.raw,
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user