From c4cc557604ed0594bbcec9ee5983c61ce3aca297 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 6 Apr 2026 00:57:40 +0100 Subject: [PATCH] fix: clarify dirty dev update error --- CHANGELOG.md | 1 + src/cli/update-cli.test.ts | 42 +++++++++++++++++++++++----- src/cli/update-cli/update-command.ts | 6 +++- 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33b7c43e852..7cbdb645bcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,6 +120,7 @@ Docs: https://docs.openclaw.ai - Discord/image generation: include the real generated `MEDIA:` paths in tool output and avoid duplicate plain-output media requeueing so Discord image replies stop pointing at missing local files. - Slack: route live DM replies back to the concrete inbound DM channel while keeping persisted routing metadata user-scoped, so normal assistant replies stop disappearing when pairing and system messages still arrive. (#59030) Thanks @afurm. - Discord/reply tags: strip leaked `[[reply_to_current]]` control tags from preview text and honor explicit reply-tag threading during final delivery, so Discord replies stay attached to the triggering message instead of printing reply metadata into chat. +- CLI/update: block `openclaw update --channel dev` with a clearer explainer when the git checkout has edited local files, instead of failing later once commit-switching work starts. - Telegram: fix current-model checks in the model picker, HTML-format non-default `/model` confirmations, explicit topic replies, persisted reaction ownership across restarts, caption-media placeholder and `file_id` preservation on download failure, and upgraded-install inbound image reads. (#60384, #60042, #59634, #59207, #59948, #59971) Thanks @sfuminya, @GitZhangChi, @dashhuang, @samzong, @v1p0r, and @neeravmakwana. - Telegram: restore DM voice-note preflight transcription so direct-message audio stops arriving as raw `` placeholders. (#61008) Thanks @manueltarouca. - Telegram/reasoning: only create a Telegram reasoning preview lane when the session is explicitly `reasoning:stream`, so hidden `` traces from streamed replies stop surfacing as chat previews on normal sessions. Thanks @vincentkoc. diff --git a/src/cli/update-cli.test.ts b/src/cli/update-cli.test.ts index 993f90a15b8..27c49ab92a5 100644 --- a/src/cli/update-cli.test.ts +++ b/src/cli/update-cli.test.ts @@ -127,13 +127,17 @@ vi.mock("../process/exec.js", () => ({ runCommandWithTimeout: vi.fn(), })); -vi.mock("../utils.js", () => ({ - displayString: (input: string) => input, - isRecord: (value: unknown) => - typeof value === "object" && value !== null && !Array.isArray(value), - pathExists: (...args: unknown[]) => pathExists(...args), - resolveConfigDir: () => "/tmp/openclaw-config", -})); +vi.mock("../utils.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + displayString: (input: string) => input, + 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", () => ({ syncPluginsForUpdateChannel: (...args: unknown[]) => syncPluginsForUpdateChannel(...args), @@ -1034,7 +1038,31 @@ describe("update-cli", () => { "Skipped plugin update sync in the pre-update CLI process after switching to a git install.", ); }); + it("explains why git updates cannot run with edited files", async () => { + vi.mocked(defaultRuntime.log).mockClear(); + vi.mocked(defaultRuntime.error).mockClear(); + vi.mocked(defaultRuntime.exit).mockClear(); + vi.mocked(runGatewayUpdate).mockResolvedValue({ + status: "skipped", + mode: "git", + reason: "dirty", + steps: [], + durationMs: 100, + } satisfies UpdateRunResult); + await updateCommand({ channel: "dev" }); + + const errors = vi.mocked(defaultRuntime.error).mock.calls.map((call) => String(call[0])); + const logs = vi.mocked(defaultRuntime.log).mock.calls.map((call) => String(call[0])); + expect(errors.join("\n")).toContain("Update blocked: local files are edited in this checkout."); + expect(logs.join("\n")).toContain( + "Git-based updates need a clean working tree before they can switch commits, fetch, or rebase.", + ); + expect(logs.join("\n")).toContain( + "Commit, stash, or discard the local changes, then rerun `openclaw update`.", + ); + expect(defaultRuntime.exit).toHaveBeenCalledWith(0); + }); 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 37aea15d255..bf99d9dd2a0 100644 --- a/src/cli/update-cli/update-command.ts +++ b/src/cli/update-cli/update-command.ts @@ -1012,11 +1012,15 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise { if (result.status === "skipped") { if (result.reason === "dirty") { + defaultRuntime.error(theme.error("Update blocked: local files are edited in this checkout.")); defaultRuntime.log( theme.warn( - "Skipped: working directory has uncommitted changes. Commit or stash them first.", + "Git-based updates need a clean working tree before they can switch commits, fetch, or rebase.", ), ); + defaultRuntime.log( + theme.muted("Commit, stash, or discard the local changes, then rerun `openclaw update`."), + ); } if (result.reason === "not-git-install") { defaultRuntime.log(