
































11// Plugin install persist tests cover saving installed plugin records after install.
2+import fs from "node:fs";
3+import os from "node:os";
4+import path from "node:path";
25import { beforeEach, describe, expect, it } from "vitest";
36import type { OpenClawConfig } from "../config/config.js";
7+import { hasRetainedManagedNpmInstallMarker } from "../plugins/managed-npm-retention.js";
48import {
59applyExclusiveSlotSelection,
610buildPluginDiagnosticsReport,
@@ -291,6 +295,108 @@ describe("persistPluginInstall", () => {
291295expect(applyPluginUninstallDirectoryRemoval).not.toHaveBeenCalled();
292296});
293297298+it("preserves replaced npm install directories across generation updates", async () => {
299+const { persistPluginInstall } = await import("./plugins-install-persist.js");
300+const baseConfig = {
301+plugins: {
302+entries: {},
303+},
304+} as OpenClawConfig;
305+const enabledConfig = {
306+plugins: {
307+entries: {
308+codex: { enabled: true },
309+},
310+},
311+} as OpenClawConfig;
312+enablePluginInConfig.mockReturnValue({ config: enabledConfig });
313+const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-persist-"));
314+const previousProjectRoot = path.join(tempRoot, "npm", "projects", "codex-v1");
315+const previousInstallPath = path.join(
316+previousProjectRoot,
317+"node_modules",
318+"@openclaw",
319+"codex",
320+);
321+const nextInstallPath = path.join(
322+tempRoot,
323+"npm",
324+"projects",
325+"codex-v2",
326+"node_modules",
327+"@openclaw",
328+"codex",
329+);
330+fs.mkdirSync(previousInstallPath, { recursive: true });
331+setInstalledPluginIndexInstallRecords({
332+codex: {
333+source: "npm",
334+spec: "@openclaw/codex@1.0.0",
335+installPath: previousInstallPath,
336+},
337+});
338+planPluginUninstall.mockReturnValueOnce({
339+ok: true,
340+config: {} as OpenClawConfig,
341+pluginId: "codex",
342+actions: {
343+entry: false,
344+install: true,
345+allowlist: false,
346+denylist: false,
347+loadPath: false,
348+memorySlot: false,
349+contextEngineSlot: false,
350+channelConfig: false,
351+directory: false,
352+},
353+directoryRemoval: {
354+target: previousInstallPath,
355+cleanup: {
356+kind: "npm",
357+npmRoot: previousProjectRoot,
358+packageName: "@openclaw/codex",
359+},
360+},
361+});
362+363+try {
364+await persistPluginInstall({
365+snapshot: {
366+config: baseConfig,
367+baseHash: "config-1",
368+writeOptions: installWriteOptions,
369+},
370+pluginId: "codex",
371+install: {
372+source: "npm",
373+spec: "@openclaw/codex@2.0.0",
374+installPath: nextInstallPath,
375+},
376+});
377+378+expect(planPluginUninstall).toHaveBeenCalledWith({
379+config: {
380+plugins: {
381+installs: {
382+codex: {
383+source: "npm",
384+spec: "@openclaw/codex@1.0.0",
385+installPath: previousInstallPath,
386+},
387+},
388+},
389+},
390+pluginId: "codex",
391+deleteFiles: true,
392+});
393+expect(applyPluginUninstallDirectoryRemoval).not.toHaveBeenCalled();
394+expect(hasRetainedManagedNpmInstallMarker(previousInstallPath)).toBe(true);
395+} finally {
396+fs.rmSync(tempRoot, { recursive: true, force: true });
397+}
398+});
399+294400it("warns when an installed npm plugin remains shadowed by a config-selected source", async () => {
295401const { persistPluginInstall } = await import("./plugins-install-persist.js");
296402const baseConfig = {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。