diff --git a/src/cli/update-cli.test.ts b/src/cli/update-cli.test.ts index 9dd47175a85..993f90a15b8 100644 --- a/src/cli/update-cli.test.ts +++ b/src/cli/update-cli.test.ts @@ -132,6 +132,7 @@ vi.mock("../utils.js", () => ({ isRecord: (value: unknown) => typeof value === "object" && value !== null && !Array.isArray(value), pathExists: (...args: unknown[]) => pathExists(...args), + resolveConfigDir: () => "/tmp/openclaw-config", })); vi.mock("../plugins/update.js", () => ({ @@ -1006,6 +1007,34 @@ describe("update-cli", () => { expect(lastWrite?.nextConfig?.update?.channel).toBe("beta"); }); + it("skips plugin sync in the old process after switching from package to git", async () => { + const tempDir = createCaseDir("openclaw-update"); + mockPackageInstallStatus(tempDir); + vi.mocked(runGatewayUpdate).mockResolvedValue( + makeOkUpdateResult({ + mode: "git", + root: path.join(tempDir, "..", "openclaw"), + after: { version: "2026.4.5" }, + }), + ); + syncPluginsForUpdateChannel.mockRejectedValue( + new Error("Config validation failed: old host version"), + ); + + await updateCommand({ channel: "dev", yes: true }); + + expect(syncPluginsForUpdateChannel).not.toHaveBeenCalled(); + expect(defaultRuntime.exit).not.toHaveBeenCalledWith(1); + expect( + vi + .mocked(defaultRuntime.log) + .mock.calls.map((call) => String(call[0])) + .join("\n"), + ).toContain( + "Skipped plugin update sync in the pre-update CLI process after switching to a git install.", + ); + }); + it.each([ { name: "refreshes service env when already installed", diff --git a/src/cli/update-cli/update-command.ts b/src/cli/update-cli/update-command.ts index 363a280798f..37aea15d255 100644 --- a/src/cli/update-cli/update-command.ts +++ b/src/cli/update-cli/update-command.ts @@ -1061,12 +1061,26 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise { } } - await updatePluginsAfterCoreUpdate({ - root, - channel, - configSnapshot: postUpdateConfigSnapshot, - opts, - }); + // A package -> git switch still runs inside the pre-update CLI process. + // Plugin sync/validation can then compare new bundled plugin minima against + // the old host version and fail even though the install itself succeeded. + const deferPluginSync = switchToGit && result.mode === "git"; + if (deferPluginSync) { + if (!opts.json) { + defaultRuntime.log( + theme.muted( + "Skipped plugin update sync in the pre-update CLI process after switching to a git install.", + ), + ); + } + } else { + await updatePluginsAfterCoreUpdate({ + root, + channel, + configSnapshot: postUpdateConfigSnapshot, + opts, + }); + } await tryWriteCompletionCache(root, Boolean(opts.json)); await tryInstallShellCompletion({