From 6a00be5f90ed41b9496033af09b9292d22483b3d Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Apr 2026 11:38:36 +0100 Subject: [PATCH] fix(update): complete channel switch follow-up work --- .../openclaw-live-and-e2e-checks-reusable.yml | 5 + docs/help/testing.md | 3 +- package.json | 1 + scripts/e2e/Dockerfile | 2 +- scripts/e2e/update-channel-switch-docker.sh | 165 ++++++++++++++++++ scripts/test-docker-all.mjs | 8 + src/cli/update-cli.test.ts | 67 ++++--- src/cli/update-cli/update-command.ts | 123 +++++-------- src/docker-build-cache.test.ts | 2 +- 9 files changed, 270 insertions(+), 106 deletions(-) create mode 100755 scripts/e2e/update-channel-switch-docker.sh diff --git a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml index 5a1e6e7415c..cbe4ae1a639 100644 --- a/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml +++ b/.github/workflows/openclaw-live-and-e2e-checks-reusable.yml @@ -430,6 +430,11 @@ jobs: command: pnpm test:docker:doctor-switch timeout_minutes: 60 release_path: true + - suite_id: docker-update-channel-switch + label: Update Channel Switch Docker E2E + command: pnpm test:docker:update-channel-switch + timeout_minutes: 60 + release_path: true - suite_id: docker-session-runtime-context label: Session Runtime Context Docker E2E command: pnpm test:docker:session-runtime-context diff --git a/docs/help/testing.md b/docs/help/testing.md index 800255e9489..33b8728efb6 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -607,7 +607,7 @@ These Docker runners split into two buckets: `OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=90000`. Override those env vars when you explicitly want the larger exhaustive scan. - `test:docker:all` builds the live Docker image once via `test:docker:live-build`, then reuses it for the live Docker lanes. It also builds one shared `scripts/e2e/Dockerfile` image via `test:docker:e2e-build` and reuses it for the E2E container smoke runners that exercise the built app. The aggregate uses a weighted local scheduler: `OPENCLAW_DOCKER_ALL_PARALLELISM` controls process slots, while resource caps keep heavy live, npm-install, and multi-service lanes from all starting at once. Defaults are 10 slots, `OPENCLAW_DOCKER_ALL_LIVE_LIMIT=6`, `OPENCLAW_DOCKER_ALL_NPM_LIMIT=8`, and `OPENCLAW_DOCKER_ALL_SERVICE_LIMIT=7`; tune `OPENCLAW_DOCKER_ALL_WEIGHT_LIMIT` or `OPENCLAW_DOCKER_ALL_DOCKER_LIMIT` only when the Docker host has more headroom. The runner performs a Docker preflight by default, removes stale OpenClaw E2E containers, prints status every 30 seconds, stores successful lane timings in `.artifacts/docker-tests/lane-timings.json`, and uses those timings to start longer lanes first on later runs. Use `OPENCLAW_DOCKER_ALL_DRY_RUN=1` to print the weighted lane manifest without building or running Docker. -- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:npm-onboard-channel-agent`, `test:docker:session-runtime-context`, `test:docker:agents-delete-shared-workspace`, `test:docker:gateway-network`, `test:docker:browser-cdp-snapshot`, `test:docker:mcp-channels`, `test:docker:pi-bundle-mcp-tools`, `test:docker:cron-mcp-cleanup`, `test:docker:plugins`, `test:docker:plugin-update`, and `test:docker:config-reload` boot one or more real containers and verify higher-level integration paths. +- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:npm-onboard-channel-agent`, `test:docker:update-channel-switch`, `test:docker:session-runtime-context`, `test:docker:agents-delete-shared-workspace`, `test:docker:gateway-network`, `test:docker:browser-cdp-snapshot`, `test:docker:mcp-channels`, `test:docker:pi-bundle-mcp-tools`, `test:docker:cron-mcp-cleanup`, `test:docker:plugins`, `test:docker:plugin-update`, and `test:docker:config-reload` boot one or more real containers and verify higher-level integration paths. The live-model Docker runners also bind-mount only the needed CLI auth homes (or all supported ones when the run is not narrowed), then copy them into the container home before the run so external-CLI OAuth can refresh tokens without mutating the host auth store: @@ -619,6 +619,7 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or - Open WebUI live smoke: `pnpm test:docker:openwebui` (script: `scripts/e2e/openwebui-docker.sh`) - Onboarding wizard (TTY, full scaffolding): `pnpm test:docker:onboard` (script: `scripts/e2e/onboard-docker.sh`) - Npm tarball onboarding/channel/agent smoke: `pnpm test:docker:npm-onboard-channel-agent` installs the packed OpenClaw tarball globally in Docker, configures OpenAI via env-ref onboarding plus Telegram by default, verifies doctor repairs activated plugin runtime deps, and runs one mocked OpenAI agent turn. Reuse a prebuilt tarball with `OPENCLAW_NPM_ONBOARD_PACKAGE_TGZ=/path/to/openclaw-*.tgz`, skip the host rebuild with `OPENCLAW_NPM_ONBOARD_HOST_BUILD=0`, or switch channel with `OPENCLAW_NPM_ONBOARD_CHANNEL=discord`. +- Update channel switch smoke: `pnpm test:docker:update-channel-switch` installs the packed OpenClaw tarball globally in Docker, switches from package `stable` to git `dev`, verifies the persisted channel and plugin post-update work, then switches back to package `stable` and checks update status. - Session runtime context smoke: `pnpm test:docker:session-runtime-context` verifies hidden runtime context transcript persistence plus doctor repair of affected duplicated prompt-rewrite branches. - Bun global install smoke: `bash scripts/e2e/bun-global-install-smoke.sh` packs the current tree, installs it with `bun install -g` in an isolated home, and verifies `openclaw infer image providers --json` returns bundled image providers instead of hanging. Reuse a prebuilt tarball with `OPENCLAW_BUN_GLOBAL_SMOKE_PACKAGE_TGZ=/path/to/openclaw-*.tgz`, skip the host build with `OPENCLAW_BUN_GLOBAL_SMOKE_HOST_BUILD=0`, or copy `dist/` from a built Docker image with `OPENCLAW_BUN_GLOBAL_SMOKE_DIST_IMAGE=openclaw-dockerfile-smoke:local`. - Installer Docker smoke: `bash scripts/test-install-sh-docker.sh` shares one npm cache across its root, update, and direct-npm containers. Update smoke defaults to npm `latest` as the stable baseline before upgrading to the candidate tarball. Non-root installer checks keep an isolated npm cache so root-owned cache entries do not mask user-local install behavior. Set `OPENCLAW_INSTALL_SMOKE_NPM_CACHE_DIR=/path/to/cache` to reuse the root/update/direct-npm cache across local reruns. diff --git a/package.json b/package.json index 9a098cb9e2f..ba3f9def3e5 100644 --- a/package.json +++ b/package.json @@ -1542,6 +1542,7 @@ "test:docker:plugins": "bash scripts/e2e/plugins-docker.sh", "test:docker:qr": "bash scripts/e2e/qr-import-docker.sh", "test:docker:session-runtime-context": "bash scripts/e2e/session-runtime-context-docker.sh", + "test:docker:update-channel-switch": "bash scripts/e2e/update-channel-switch-docker.sh", "test:e2e": "node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts", "test:e2e:openshell": "OPENCLAW_E2E_OPENSHELL=1 node scripts/run-vitest.mjs run --config test/vitest/vitest.e2e.config.ts extensions/openshell/src/backend.e2e.test.ts", "test:extension": "node scripts/test-extension.mjs", diff --git a/scripts/e2e/Dockerfile b/scripts/e2e/Dockerfile index 036e2c6ead4..91bbcffcd1b 100644 --- a/scripts/e2e/Dockerfile +++ b/scripts/e2e/Dockerfile @@ -40,7 +40,7 @@ RUN --mount=type=cache,id=openclaw-pnpm-store,target=/home/appuser/.local/share/ FROM deps AS build -COPY --chown=appuser:appuser tsconfig.json tsconfig.plugin-sdk.dts.json tsdown.config.ts vitest.config.ts openclaw.mjs ./ +COPY --chown=appuser:appuser .oxlintrc.json tsconfig.json tsconfig.plugin-sdk.dts.json tsconfig.oxlint*.json tsdown.config.ts vitest.config.ts openclaw.mjs ./ COPY --chown=appuser:appuser src ./src COPY --chown=appuser:appuser test ./test COPY --chown=appuser:appuser scripts ./scripts diff --git a/scripts/e2e/update-channel-switch-docker.sh b/scripts/e2e/update-channel-switch-docker.sh new file mode 100755 index 00000000000..203c211db4e --- /dev/null +++ b/scripts/e2e/update-channel-switch-docker.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" + +IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-update-channel-switch-e2e" OPENCLAW_UPDATE_CHANNEL_SWITCH_E2E_IMAGE)" +SKIP_BUILD="${OPENCLAW_UPDATE_CHANNEL_SWITCH_E2E_SKIP_BUILD:-0}" + +docker_e2e_build_or_reuse "$IMAGE_NAME" update-channel-switch "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "" "$SKIP_BUILD" + +echo "Running update channel switch E2E..." +docker run --rm \ + -e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ + -e OPENCLAW_SKIP_CHANNELS=1 \ + -e OPENCLAW_SKIP_PROVIDERS=1 \ + "$IMAGE_NAME" \ + bash -lc 'set -euo pipefail + +export npm_config_loglevel=error +export npm_config_fund=false +export npm_config_audit=false +export npm_config_prefix=/tmp/npm-prefix +export NPM_CONFIG_PREFIX=/tmp/npm-prefix +export PNPM_HOME=/tmp/pnpm-home +export PATH="/tmp/npm-prefix/bin:/tmp/pnpm-home:$PATH" +export CI=true +export OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 +export OPENCLAW_NO_ONBOARD=1 +export OPENCLAW_NO_PROMPT=1 + +cat > /app/.gitignore <<'"'"'GITIGNORE'"'"' +node_modules +**/node_modules/ +dist +dist-runtime +.turbo +coverage +GITIGNORE + +node --import tsx scripts/write-package-dist-inventory.ts + +git config --global user.email "docker-e2e@openclaw.local" +git config --global user.name "OpenClaw Docker E2E" +git config --global gc.auto 0 +git -C /app init -q +git -C /app config gc.auto 0 +git -C /app add -A +git -C /app commit -qm "test fixture" +fixture_sha="$(git -C /app rev-parse HEAD)" + +pkg_tgz="$(npm pack --ignore-scripts --silent --pack-destination /tmp /app | tail -n 1 | tr -d "\r")" +pkg_tgz_path="/tmp/$pkg_tgz" +if [ ! -f "$pkg_tgz_path" ]; then + echo "npm pack failed (expected $pkg_tgz_path)" + exit 1 +fi + +npm install -g --prefix /tmp/npm-prefix --omit=optional "$pkg_tgz_path" + +home_dir="$(mktemp -d /tmp/openclaw-update-channel-switch-home.XXXXXX)" +export HOME="$home_dir" +mkdir -p "$HOME/.openclaw" +cat > "$HOME/.openclaw/openclaw.json" <<'"'"'JSON'"'"' +{ + "update": { + "channel": "stable" + }, + "plugins": {} +} +JSON + +export OPENCLAW_GIT_DIR=/app +export OPENCLAW_UPDATE_DEV_TARGET_REF="$fixture_sha" + +echo "==> package -> git dev channel" +set +e +dev_json="$(openclaw update --channel dev --yes --json --no-restart)" +dev_status=$? +set -e +printf "%s\n" "$dev_json" +if [ "$dev_status" -ne 0 ]; then + exit "$dev_status" +fi +DEV_JSON="$dev_json" node - <<'"'"'NODE'"'"' +const payload = JSON.parse(process.env.DEV_JSON); +if (payload.status !== "ok") { + throw new Error(`expected dev update status ok, got ${payload.status}`); +} +if (payload.mode !== "git") { + throw new Error(`expected dev update mode git, got ${payload.mode}`); +} +if (payload.postUpdate?.plugins?.status !== "ok") { + throw new Error(`expected plugin post-update ok, got ${JSON.stringify(payload.postUpdate?.plugins)}`); +} +NODE + +node - <<'"'"'NODE'"'"' +const fs = require("node:fs"); +const path = require("node:path"); +const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json"); +const config = JSON.parse(fs.readFileSync(configPath, "utf8")); +if (config.update?.channel !== "dev") { + throw new Error(`expected persisted update.channel dev, got ${JSON.stringify(config.update?.channel)}`); +} +NODE + +status_json="$(openclaw update status --json)" +printf "%s\n" "$status_json" +STATUS_JSON="$status_json" node - <<'"'"'NODE'"'"' +const payload = JSON.parse(process.env.STATUS_JSON); +if (payload.update?.installKind !== "git") { + throw new Error(`expected git install after dev switch, got ${payload.update?.installKind}`); +} +if (payload.channel?.value !== "dev" || payload.channel?.source !== "config") { + throw new Error(`expected dev config channel after dev switch, got ${JSON.stringify(payload.channel)}`); +} +NODE + +echo "==> git -> package stable channel" +set +e +stable_json="$(openclaw update --channel stable --tag "$pkg_tgz_path" --yes --json --no-restart)" +stable_status=$? +set -e +printf "%s\n" "$stable_json" +if [ "$stable_status" -ne 0 ]; then + exit "$stable_status" +fi +STABLE_JSON="$stable_json" node - <<'"'"'NODE'"'"' +const payload = JSON.parse(process.env.STABLE_JSON); +if (payload.status !== "ok") { + throw new Error(`expected stable update status ok, got ${payload.status}`); +} +if (!["npm", "pnpm", "bun"].includes(payload.mode)) { + throw new Error(`expected package-manager mode after stable switch, got ${payload.mode}`); +} +if (payload.postUpdate?.plugins?.status !== "ok") { + throw new Error(`expected plugin post-update ok, got ${JSON.stringify(payload.postUpdate?.plugins)}`); +} +NODE + +node - <<'"'"'NODE'"'"' +const fs = require("node:fs"); +const path = require("node:path"); +const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json"); +const config = JSON.parse(fs.readFileSync(configPath, "utf8")); +if (config.update?.channel !== "stable") { + throw new Error(`expected persisted update.channel stable, got ${JSON.stringify(config.update?.channel)}`); +} +NODE + +status_json="$(openclaw update status --json)" +printf "%s\n" "$status_json" +STATUS_JSON="$status_json" node - <<'"'"'NODE'"'"' +const payload = JSON.parse(process.env.STATUS_JSON); +if (payload.update?.installKind !== "package") { + throw new Error(`expected package install after stable switch, got ${payload.update?.installKind}`); +} +if (payload.channel?.value !== "stable" || payload.channel?.source !== "config") { + throw new Error(`expected stable config channel after stable switch, got ${JSON.stringify(payload.channel)}`); +} +NODE + +echo "OK" +' diff --git a/scripts/test-docker-all.mjs b/scripts/test-docker-all.mjs index a0df9dd0f29..10aa26964a9 100644 --- a/scripts/test-docker-all.mjs +++ b/scripts/test-docker-all.mjs @@ -246,6 +246,14 @@ const lanes = [ npmLane("doctor-switch", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:doctor-switch", { weight: 3, }), + npmLane( + "update-channel-switch", + "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:update-channel-switch", + { + timeoutMs: 30 * 60 * 1000, + weight: 3, + }, + ), lane("plugins", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:plugins", { resources: ["npm", "service"], weight: 6, diff --git a/src/cli/update-cli.test.ts b/src/cli/update-cli.test.ts index a52affc945c..e60bbd18a07 100644 --- a/src/cli/update-cli.test.ts +++ b/src/cli/update-cli.test.ts @@ -1693,41 +1693,68 @@ describe("update-cli", () => { expect(syncConfig?.plugins?.entries).toBeUndefined(); }); - it("skips plugin sync in the old process after switching from package to git", async () => { + it("persists channel and runs post-update work after switching from package to git", async () => { const tempDir = createCaseDir("openclaw-update"); + const gitRoot = path.join(tempDir, "..", "openclaw"); const completionCacheSpy = vi .spyOn(updateCliShared, "tryWriteCompletionCache") .mockResolvedValue(undefined); mockPackageInstallStatus(tempDir); + vi.mocked(readConfigFileSnapshot).mockResolvedValue({ + ...baseSnapshot, + parsed: { update: { channel: "stable" } }, + resolved: { update: { channel: "stable" } } as OpenClawConfig, + sourceConfig: { update: { channel: "stable" } } as OpenClawConfig, + runtimeConfig: { update: { channel: "stable" } } as OpenClawConfig, + config: { update: { channel: "stable" } } as OpenClawConfig, + }); vi.mocked(runGatewayUpdate).mockResolvedValue( makeOkUpdateResult({ mode: "git", - root: path.join(tempDir, "..", "openclaw"), + root: gitRoot, after: { version: "2026.4.10" }, }), ); - serviceLoaded.mockResolvedValue(true); - syncPluginsForUpdateChannel.mockRejectedValue( - new Error("Config validation failed: old host version"), + syncPluginsForUpdateChannel.mockImplementation(async ({ config }) => ({ + changed: false, + config, + summary: { + switchedToBundled: [], + switchedToNpm: [], + warnings: [], + errors: [], + }, + })); + updateNpmInstalledPlugins.mockImplementation(async ({ config }) => ({ + changed: false, + config, + outcomes: [], + })); + + await updateCommand({ channel: "dev", yes: true, restart: false }); + + const persistedConfig = vi.mocked(replaceConfigFile).mock.calls[0]?.[0]?.nextConfig; + expect(persistedConfig?.update?.channel).toBe("dev"); + expect(syncPluginsForUpdateChannel).toHaveBeenCalledWith( + expect.objectContaining({ + channel: "dev", + config: expect.objectContaining({ + update: expect.objectContaining({ channel: "dev" }), + }), + workspaceDir: gitRoot, + }), ); - - await updateCommand({ channel: "dev", yes: true }); - - expect(syncPluginsForUpdateChannel).not.toHaveBeenCalled(); - expect(replaceConfigFile).not.toHaveBeenCalled(); - expect(completionCacheSpy).not.toHaveBeenCalled(); + expect(updateNpmInstalledPlugins).toHaveBeenCalledWith( + expect.objectContaining({ + config: expect.objectContaining({ + update: expect.objectContaining({ channel: "dev" }), + }), + }), + ); + expect(completionCacheSpy).toHaveBeenCalledWith(gitRoot, false); expect(runRestartScript).not.toHaveBeenCalled(); expect(runDaemonRestart).not.toHaveBeenCalled(); - expect(defaultRuntime.exit).toHaveBeenCalledWith(0); expect(defaultRuntime.exit).not.toHaveBeenCalledWith(1); - expect( - vi - .mocked(defaultRuntime.log) - .mock.calls.map((call) => String(call[0])) - .join("\n"), - ).toContain( - "Switched from a package install to a git checkout. Skipping remaining post-update work in the old CLI process; rerun follow-up commands from the new git install if needed.", - ); }); it("explains why git updates cannot run with edited files", async () => { vi.mocked(defaultRuntime.log).mockClear(); diff --git a/src/cli/update-cli/update-command.ts b/src/cli/update-cli/update-command.ts index d2250ddc2b8..78c243d9c9c 100644 --- a/src/cli/update-cli/update-command.ts +++ b/src/cli/update-cli/update-command.ts @@ -1339,54 +1339,30 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise { return; } - if (switchToGit && result.status === "ok" && result.mode === "git") { - if (!opts.json) { - defaultRuntime.log( - theme.muted( - "Switched from a package install to a git checkout. Skipping remaining post-update work in the old CLI process; rerun follow-up commands from the new git install if needed.", - ), - ); - } else { - defaultRuntime.writeJson(result); - } - defaultRuntime.exit(0); - return; - } - let postUpdateConfigSnapshot = configSnapshot; if (requestedChannel && configSnapshot.valid && requestedChannel !== storedChannel) { - if (switchToGit) { - if (!opts.json) { - defaultRuntime.log( - theme.muted( - `Skipped persisting update.channel=${requestedChannel} in the pre-update CLI process after switching to a git install.`, - ), - ); - } - } else { - const next = { - ...configSnapshot.sourceConfig, - update: { - ...configSnapshot.sourceConfig.update, - channel: requestedChannel, - }, - }; - await replaceConfigFile({ - nextConfig: next, - baseHash: configSnapshot.hash, - }); - postUpdateConfigSnapshot = { - ...configSnapshot, - hash: undefined, - parsed: next, - sourceConfig: asResolvedSourceConfig(next), - resolved: asResolvedSourceConfig(next), - runtimeConfig: asRuntimeConfig(next), - config: asRuntimeConfig(next), - }; - if (!opts.json) { - defaultRuntime.log(theme.muted(`Update channel set to ${requestedChannel}.`)); - } + const next = { + ...configSnapshot.sourceConfig, + update: { + ...configSnapshot.sourceConfig.update, + channel: requestedChannel, + }, + }; + await replaceConfigFile({ + nextConfig: next, + baseHash: configSnapshot.hash, + }); + postUpdateConfigSnapshot = { + ...configSnapshot, + hash: undefined, + parsed: next, + sourceConfig: asResolvedSourceConfig(next), + resolved: asResolvedSourceConfig(next), + runtimeConfig: asRuntimeConfig(next), + config: asRuntimeConfig(next), + }; + if (!opts.json) { + defaultRuntime.log(theme.muted(`Update channel set to ${requestedChannel}.`)); } } @@ -1409,16 +1385,7 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise { postCorePluginUpdate = freshProcessResult.pluginUpdate; } - const deferOldProcessPostUpdateWork = switchToGit && result.mode === "git"; - if (deferOldProcessPostUpdateWork) { - if (!opts.json) { - defaultRuntime.log( - theme.muted( - "Skipped plugin update sync in the pre-update CLI process after switching to a git install.", - ), - ); - } - } else if (!pluginsUpdatedInFreshProcess) { + if (!pluginsUpdatedInFreshProcess) { postCorePluginUpdate = await runPostCorePluginUpdate({ root: postUpdateRoot, channel, @@ -1468,34 +1435,24 @@ export async function updateCommand(opts: UpdateCommandOptions): Promise { } } - if (deferOldProcessPostUpdateWork) { - if (!opts.json) { - defaultRuntime.log( - theme.muted( - "Skipped completion/restart follow-ups in the pre-update CLI process after switching to a git install.", - ), - ); - } - } else { - await tryWriteCompletionCache(postUpdateRoot, Boolean(opts.json)); - await tryInstallShellCompletion({ - jsonMode: Boolean(opts.json), - skipPrompt: Boolean(opts.yes), - }); + await tryWriteCompletionCache(postUpdateRoot, Boolean(opts.json)); + await tryInstallShellCompletion({ + jsonMode: Boolean(opts.json), + skipPrompt: Boolean(opts.yes), + }); - const restartOk = await maybeRestartService({ - shouldRestart, - result: resultWithPostUpdate, - opts, - refreshServiceEnv: refreshGatewayServiceEnv, - gatewayPort, - restartScriptPath, - invocationCwd, - }); - if (!restartOk) { - defaultRuntime.exit(1); - return; - } + const restartOk = await maybeRestartService({ + shouldRestart, + result: resultWithPostUpdate, + opts, + refreshServiceEnv: refreshGatewayServiceEnv, + gatewayPort, + restartScriptPath, + invocationCwd, + }); + if (!restartOk) { + defaultRuntime.exit(1); + return; } if (!opts.json) { diff --git a/src/docker-build-cache.test.ts b/src/docker-build-cache.test.ts index 3cfc5b01d10..9854c135f9a 100644 --- a/src/docker-build-cache.test.ts +++ b/src/docker-build-cache.test.ts @@ -116,7 +116,7 @@ describe("docker build cache layout", () => { /^COPY(?:\s+--chown=\S+)?\s+scripts\/postinstall-bundled-plugins\.mjs scripts\/preinstall-package-manager-warning\.mjs scripts\/npm-runner\.mjs scripts\/windows-cmd-helpers\.mjs \.\/scripts\/$/m, ); expectPatternAfterInstall( - /^COPY(?:\s+--chown=\S+)?\s+tsconfig\.json tsconfig\.plugin-sdk\.dts\.json tsdown\.config\.ts vitest\.config\.ts openclaw\.mjs \.\/$/m, + /^COPY(?:\s+--chown=\S+)?\s+\.oxlintrc\.json tsconfig\.json tsconfig\.plugin-sdk\.dts\.json tsconfig\.oxlint\*\.json tsdown\.config\.ts vitest\.config\.ts openclaw\.mjs \.\/$/m, ); expectPatternAfterInstall(/^COPY(?:\s+--chown=\S+)?\s+src \.\/src$/m); expectPatternAfterInstall(/^COPY(?:\s+--chown=\S+)?\s+test \.\/test$/m);