diff --git a/src/cli/update-cli.test.ts b/src/cli/update-cli.test.ts index f5d5f8ff78f9..02c4237d1184 100644 --- a/src/cli/update-cli.test.ts +++ b/src/cli/update-cli.test.ts @@ -2877,6 +2877,51 @@ describe("update-cli", () => { expect(updateCall?.beforeGitMutation).toEqual(expect.any(Function)); }); + it("stops a managed gateway rooted at the git checkout when switching package installs to dev", async () => { + const packageRoot = createCaseDir("openclaw-update-package-root"); + const gitRoot = await createTrackedTempDir("openclaw-update-git-service-root-"); + const serviceEntrypoint = path.join(gitRoot, "dist", "index.js"); + await fs.mkdir(path.join(gitRoot, ".git"), { recursive: true }); + await fs.mkdir(path.dirname(serviceEntrypoint), { recursive: true }); + await fs.writeFile( + path.join(gitRoot, "package.json"), + JSON.stringify({ name: "openclaw", version: "2026.4.21" }), + "utf-8", + ); + await fs.writeFile(serviceEntrypoint, "export {};\n", "utf-8"); + mockPackageInstallStatus(packageRoot); + pathExists.mockImplementation(async (candidate: string) => candidate === gitRoot); + serviceReadCommand.mockResolvedValue({ + programArguments: ["node", serviceEntrypoint, "gateway", "run"], + environment: { + OPENCLAW_SERVICE_MARKER: "openclaw", + OPENCLAW_SERVICE_KIND: "gateway", + }, + }); + serviceLoaded.mockResolvedValue(true); + serviceReadRuntime.mockResolvedValue({ + status: "running", + pid: 4242, + state: "running", + }); + mockGitUpdateAfterMutation( + makeOkUpdateResult({ + mode: "git", + root: gitRoot, + }), + ); + + await withEnvAsync({ OPENCLAW_GIT_DIR: gitRoot }, async () => { + await updateCommand({ channel: "dev", yes: true }); + }); + + expect(serviceStop).toHaveBeenCalledTimes(1); + expect(runGatewayUpdate).toHaveBeenCalledTimes(1); + const updateCall = vi.mocked(runGatewayUpdate).mock.calls[0]?.[0]; + expect(updateCall?.cwd).toBe(gitRoot); + expect(updateCall?.beforeGitMutation).toEqual(expect.any(Function)); + }); + it("does not stop or restart a managed gateway owned by another git checkout", async () => { const otherRoot = await createTrackedTempDir("openclaw-update-other-service-root-"); const otherEntrypoint = path.join(otherRoot, "dist", "index.js"); diff --git a/src/cli/update-cli/update-command.ts b/src/cli/update-cli/update-command.ts index 70ada82a317a..e036cf451f82 100644 --- a/src/cli/update-cli/update-command.ts +++ b/src/cli/update-cli/update-command.ts @@ -3469,14 +3469,16 @@ async function updateCommandInternal(opts: UpdateCommandOptions): Promise const preUpdatePluginInstallRecords = await loadInstalledPluginIndexInstallRecords(); let preManagedServiceStop: PreManagedServiceStop | undefined; - const stopManagedServiceBeforeMutableUpdate = async () => { + const gitMutationRoot = + updateInstallKind === "git" ? (switchToGit ? resolveGitInstallDir() : root) : null; + const stopManagedServiceBeforeMutableUpdate = async (mutationRoot: string = root) => { if (updateInstallKind !== "package" && updateInstallKind !== "git") { return; } try { preManagedServiceStop = await maybeStopManagedServiceBeforeMutableUpdate({ updateInstallKind, - root, + root: mutationRoot, shouldRestart, jsonMode: Boolean(opts.json), }); @@ -3552,7 +3554,9 @@ async function updateCommandInternal(opts: UpdateCommandOptions): Promise stop, devTargetRef, beforeGitMutation: - updateInstallKind === "git" ? stopManagedServiceBeforeMutableUpdate : undefined, + updateInstallKind === "git" + ? () => stopManagedServiceBeforeMutableUpdate(gitMutationRoot ?? root) + : undefined, }); } catch (err) { stop();