diff --git a/src/cli/update-cli.test.ts b/src/cli/update-cli.test.ts index a5d9cfc4a2..a34d059d01 100644 --- a/src/cli/update-cli.test.ts +++ b/src/cli/update-cli.test.ts @@ -15,6 +15,7 @@ const resolveGlobalManager = vi.fn(); const serviceLoaded = vi.fn(); const prepareRestartScript = vi.fn(); const runRestartScript = vi.fn(); +const mockedRunDaemonInstall = vi.fn(); vi.mock("@clack/prompts", () => ({ confirm, @@ -93,6 +94,7 @@ vi.mock("../commands/doctor.js", () => ({ })); // Mock the daemon-cli module vi.mock("./daemon-cli.js", () => ({ + runDaemonInstall: mockedRunDaemonInstall, runDaemonRestart: vi.fn(), })); @@ -111,7 +113,7 @@ const { readConfigFileSnapshot, writeConfigFile } = await import("../config/conf const { checkUpdateStatus, fetchNpmTagVersion, resolveNpmChannelTag } = await import("../infra/update-check.js"); const { runCommandWithTimeout } = await import("../process/exec.js"); -const { runDaemonRestart } = await import("./daemon-cli.js"); +const { runDaemonRestart, runDaemonInstall } = await import("./daemon-cli.js"); const { doctorCommand } = await import("../commands/doctor.js"); const { defaultRuntime } = await import("../runtime.js"); const { updateCommand, registerUpdateCli, updateStatusCommand, updateWizardCommand } = @@ -219,6 +221,7 @@ describe("update-cli", () => { vi.mocked(resolveNpmChannelTag).mockReset(); vi.mocked(runCommandWithTimeout).mockReset(); vi.mocked(runDaemonRestart).mockReset(); + vi.mocked(mockedRunDaemonInstall).mockReset(); vi.mocked(doctorCommand).mockReset(); vi.mocked(defaultRuntime.log).mockReset(); vi.mocked(defaultRuntime.error).mockReset(); @@ -278,6 +281,7 @@ describe("update-cli", () => { serviceLoaded.mockResolvedValue(false); prepareRestartScript.mockResolvedValue("/tmp/openclaw-restart-test.sh"); runRestartScript.mockResolvedValue(undefined); + runDaemonInstall.mockResolvedValue(undefined); setTty(false); setStdoutTty(false); }); @@ -460,6 +464,61 @@ describe("update-cli", () => { expect(runDaemonRestart).toHaveBeenCalled(); }); + it("updateCommand refreshes gateway service env when service is already installed", async () => { + const mockResult: UpdateRunResult = { + status: "ok", + mode: "git", + steps: [], + durationMs: 100, + }; + + vi.mocked(runGatewayUpdate).mockResolvedValue(mockResult); + vi.mocked(runDaemonInstall).mockResolvedValue(undefined); + serviceLoaded.mockResolvedValue(true); + + await updateCommand({}); + + expect(runDaemonInstall).toHaveBeenCalledWith({ + force: true, + json: undefined, + }); + expect(runDaemonRestart).not.toHaveBeenCalled(); + }); + + it("updateCommand falls back to restart when env refresh install fails", async () => { + const mockResult: UpdateRunResult = { + status: "ok", + mode: "git", + steps: [], + durationMs: 100, + }; + + vi.mocked(runGatewayUpdate).mockResolvedValue(mockResult); + vi.mocked(runDaemonInstall).mockRejectedValueOnce(new Error("refresh failed")); + prepareRestartScript.mockResolvedValue(null); + serviceLoaded.mockResolvedValue(true); + vi.mocked(runDaemonRestart).mockResolvedValue(true); + + await updateCommand({}); + + expect(runDaemonInstall).toHaveBeenCalledWith({ + force: true, + json: undefined, + }); + expect(runDaemonRestart).toHaveBeenCalled(); + }); + + it("updateCommand does not refresh service env when --no-restart is set", async () => { + vi.mocked(runGatewayUpdate).mockResolvedValue(makeOkUpdateResult()); + serviceLoaded.mockResolvedValue(true); + + await updateCommand({ restart: false }); + + expect(runDaemonInstall).not.toHaveBeenCalled(); + expect(runRestartScript).not.toHaveBeenCalled(); + expect(runDaemonRestart).not.toHaveBeenCalled(); + }); + it("updateCommand continues after doctor sub-step and clears update flag", async () => { const envSnapshot = captureEnv(["OPENCLAW_UPDATE_IN_PROGRESS"]); const randomSpy = vi.spyOn(Math, "random").mockReturnValue(0); diff --git a/src/cli/update-cli/update-command.ts b/src/cli/update-cli/update-command.ts index 872a06def1..c47893dd07 100644 --- a/src/cli/update-cli/update-command.ts +++ b/src/cli/update-cli/update-command.ts @@ -33,7 +33,7 @@ import { pathExists } from "../../utils.js"; import { replaceCliName, resolveCliName } from "../cli-name.js"; import { formatCliCommand } from "../command-format.js"; import { installCompletion } from "../completion-cli.js"; -import { runDaemonRestart } from "../daemon-cli.js"; +import { runDaemonInstall, runDaemonRestart } from "../daemon-cli.js"; import { createUpdateProgress, printResult } from "./progress.js"; import { prepareRestartScript, runRestartScript } from "./restart-helper.js"; import { @@ -391,6 +391,7 @@ async function maybeRestartService(params: { shouldRestart: boolean; result: UpdateRunResult; opts: UpdateCommandOptions; + refreshServiceEnv: boolean; restartScriptPath?: string | null; }): Promise { if (params.shouldRestart) { @@ -402,11 +403,29 @@ async function maybeRestartService(params: { try { let restarted = false; let restartInitiated = false; - if (params.restartScriptPath) { + let serviceRefreshed = false; + if (params.refreshServiceEnv) { + try { + await runDaemonInstall({ force: true, json: params.opts.json }); + serviceRefreshed = true; + restarted = true; + } catch (err) { + if (!params.opts.json) { + defaultRuntime.log( + theme.warn( + `Failed to refresh gateway service environment from updated install: ${String(err)}`, + ), + ); + } + } + } + if (!serviceRefreshed && params.restartScriptPath) { await runRestartScript(params.restartScriptPath); restartInitiated = true; } else { - restarted = await runDaemonRestart(); + if (!serviceRefreshed) { + restarted = await runDaemonRestart(); + } } if (!params.opts.json && restarted) { @@ -586,11 +605,13 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise { const startedAt = Date.now(); let restartScriptPath: string | null = null; + let refreshGatewayServiceEnv = false; if (shouldRestart) { try { const loaded = await resolveGatewayService().isLoaded({ env: process.env }); if (loaded) { restartScriptPath = await prepareRestartScript(process.env); + refreshGatewayServiceEnv = true; } } catch { // Ignore errors during pre-check; fallback to standard restart @@ -669,6 +690,7 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise { shouldRestart, result, opts, + refreshServiceEnv: refreshGatewayServiceEnv, restartScriptPath, });