|
1 | 1 | // 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"; |
2 | 5 | import { beforeEach, describe, expect, it } from "vitest"; |
3 | 6 | import type { OpenClawConfig } from "../config/config.js"; |
| 7 | +import { hasRetainedManagedNpmInstallMarker } from "../plugins/managed-npm-retention.js"; |
4 | 8 | import { |
5 | 9 | applyExclusiveSlotSelection, |
6 | 10 | buildPluginDiagnosticsReport, |
@@ -291,6 +295,108 @@ describe("persistPluginInstall", () => { |
291 | 295 | expect(applyPluginUninstallDirectoryRemoval).not.toHaveBeenCalled(); |
292 | 296 | }); |
293 | 297 |
|
| 298 | + 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/[email protected]", |
| 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/[email protected]", |
| 374 | + installPath: nextInstallPath, |
| 375 | + }, |
| 376 | + }); |
| 377 | + |
| 378 | + expect(planPluginUninstall).toHaveBeenCalledWith({ |
| 379 | + config: { |
| 380 | + plugins: { |
| 381 | + installs: { |
| 382 | + codex: { |
| 383 | + source: "npm", |
| 384 | + spec: "@openclaw/[email protected]", |
| 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 | + |
294 | 400 | it("warns when an installed npm plugin remains shadowed by a config-selected source", async () => { |
295 | 401 | const { persistPluginInstall } = await import("./plugins-install-persist.js"); |
296 | 402 | const baseConfig = { |
|
0 commit comments