























@@ -1,7 +1,7 @@
11import fs from "node:fs";
22import os from "node:os";
33import path from "node:path";
4-import { beforeEach, describe, expect, it } from "vitest";
4+import { afterEach, beforeEach, describe, expect, it } from "vitest";
55import { installedPluginRoot } from "../../test/helpers/bundled-plugin-paths.js";
66import type { OpenClawConfig } from "../config/config.js";
77import {
@@ -30,11 +30,18 @@ import {
3030} from "./plugins-cli-test-helpers.js";
31313232const CLI_STATE_ROOT = "/tmp/openclaw-state";
33+const ORIGINAL_OPENCLAW_STATE_DIR = process.env.OPENCLAW_STATE_DIR;
34+const PROFILE_STATE_ROOT = "/tmp/openclaw-ledger-profile";
33353436function cliInstallPath(pluginId: string): string {
3537return installedPluginRoot(CLI_STATE_ROOT, pluginId);
3638}
373940+function useProfileExtensionsDir(): string {
41+process.env.OPENCLAW_STATE_DIR = PROFILE_STATE_ROOT;
42+return path.join(PROFILE_STATE_ROOT, "extensions");
43+}
44+3845function createEnabledPluginConfig(pluginId: string): OpenClawConfig {
3946return {
4047plugins: {
@@ -227,6 +234,14 @@ describe("plugins cli install", () => {
227234resetPluginsCliTestState();
228235});
229236237+afterEach(() => {
238+if (ORIGINAL_OPENCLAW_STATE_DIR === undefined) {
239+delete process.env.OPENCLAW_STATE_DIR;
240+} else {
241+process.env.OPENCLAW_STATE_DIR = ORIGINAL_OPENCLAW_STATE_DIR;
242+}
243+});
244+230245it("shows the force overwrite option in install help", async () => {
231246const { Command } = await import("commander");
232247const { registerPluginsCli } = await import("./plugins-cli.js");
@@ -275,6 +290,22 @@ describe("plugins cli install", () => {
275290expect(writeConfigFile).not.toHaveBeenCalled();
276291});
277292293+it("passes the active profile extensions dir to marketplace installs", async () => {
294+const extensionsDir = useProfileExtensionsDir();
295+296+await expect(
297+runPluginsCommand(["plugins", "install", "alpha", "--marketplace", "local/repo"]),
298+).rejects.toThrow("__exit__:1");
299+300+expect(installPluginFromMarketplace).toHaveBeenCalledWith(
301+expect.objectContaining({
302+ extensionsDir,
303+marketplace: "local/repo",
304+plugin: "alpha",
305+}),
306+);
307+});
308+278309it("fails closed for unrelated invalid config before installer side effects", async () => {
279310const invalidConfigErr = new Error("config invalid");
280311(invalidConfigErr as { code?: string }).code = "INVALID_CONFIG";
@@ -421,6 +452,37 @@ describe("plugins cli install", () => {
421452expect(installPluginFromNpmSpec).not.toHaveBeenCalled();
422453});
423454455+it("passes the active profile extensions dir to ClawHub installs", async () => {
456+const extensionsDir = useProfileExtensionsDir();
457+const cfg = createEmptyPluginConfig();
458+const enabledCfg = createEnabledPluginConfig("demo");
459+460+loadConfig.mockReturnValue(cfg);
461+parseClawHubPluginSpec.mockReturnValue({ name: "demo" });
462+installPluginFromClawHub.mockResolvedValue(
463+createClawHubInstallResult({
464+pluginId: "demo",
465+packageName: "demo",
466+version: "1.2.3",
467+channel: "official",
468+}),
469+);
470+enablePluginInConfig.mockReturnValue({ config: enabledCfg });
471+applyExclusiveSlotSelection.mockReturnValue({
472+config: enabledCfg,
473+warnings: [],
474+});
475+476+await runPluginsCommand(["plugins", "install", "clawhub:demo"]);
477+478+expect(installPluginFromClawHub).toHaveBeenCalledWith(
479+expect.objectContaining({
480+ extensionsDir,
481+spec: "clawhub:demo",
482+}),
483+);
484+});
485+424486it("does not persist incomplete config entries for config-gated bundled installs", async () => {
425487const cfg = {
426488plugins: {
@@ -664,6 +726,30 @@ describe("plugins cli install", () => {
664726expect(writeConfigFile).toHaveBeenCalledWith(enabledCfg);
665727});
666728729+it("passes the active profile extensions dir to npm installs", async () => {
730+const extensionsDir = useProfileExtensionsDir();
731+const cfg = createEmptyPluginConfig();
732+const enabledCfg = createEnabledPluginConfig("demo");
733+734+loadConfig.mockReturnValue(cfg);
735+installPluginFromNpmSpec.mockResolvedValue(createNpmPluginInstallResult("demo"));
736+enablePluginInConfig.mockReturnValue({ config: enabledCfg });
737+recordPluginInstall.mockReturnValue(enabledCfg);
738+applyExclusiveSlotSelection.mockReturnValue({
739+config: enabledCfg,
740+warnings: [],
741+});
742+743+await runPluginsCommand(["plugins", "install", "npm:demo"]);
744+745+expect(installPluginFromNpmSpec).toHaveBeenCalledWith(
746+expect.objectContaining({
747+ extensionsDir,
748+spec: "demo",
749+}),
750+);
751+});
752+667753it("passes npm: prefix installs through npm options without ClawHub lookup", async () => {
668754const cfg = createEmptyPluginConfig();
669755const enabledCfg = createEnabledPluginConfig("demo");
@@ -903,6 +989,41 @@ describe("plugins cli install", () => {
903989}),
904990);
905991});
992+993+it("passes the active profile extensions dir to local path installs", async () => {
994+const extensionsDir = useProfileExtensionsDir();
995+const localPluginDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-local-plugin-"));
996+const cfg = createEmptyPluginConfig();
997+const enabledCfg = createEnabledPluginConfig("demo");
998+999+loadConfig.mockReturnValue(cfg);
1000+installPluginFromPath.mockResolvedValue({
1001+ok: true,
1002+pluginId: "demo",
1003+targetDir: path.join(extensionsDir, "demo"),
1004+version: "1.2.3",
1005+extensions: ["./dist/index.js"],
1006+});
1007+enablePluginInConfig.mockReturnValue({ config: enabledCfg });
1008+recordPluginInstall.mockReturnValue(enabledCfg);
1009+applyExclusiveSlotSelection.mockReturnValue({
1010+config: enabledCfg,
1011+warnings: [],
1012+});
1013+1014+try {
1015+await runPluginsCommand(["plugins", "install", localPluginDir]);
1016+} finally {
1017+fs.rmSync(localPluginDir, { recursive: true, force: true });
1018+}
1019+1020+expect(installPluginFromPath).toHaveBeenCalledWith(
1021+expect.objectContaining({
1022+ extensionsDir,
1023+path: localPluginDir,
1024+}),
1025+);
1026+});
9061027it("passes force through as overwrite mode for npm installs", async () => {
9071028primeNpmPluginFallback();
9081029此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。