diff --git a/package.json b/package.json index 40ea7ac93048..f63ea7030f84 100644 --- a/package.json +++ b/package.json @@ -1782,6 +1782,7 @@ "test:docker:live-models:gemini": "OPENCLAW_LIVE_PROVIDERS=google-gemini-cli OPENCLAW_LIVE_MODELS=google-gemini-cli/gemini-3.1-pro-preview bash scripts/test-live-models-docker.sh", "test:docker:live:all": "node scripts/run-with-env.mjs OPENCLAW_DOCKER_ALL_LIVE_MODE=only -- node scripts/test-docker-all.mjs", "test:docker:local:all": "node scripts/run-with-env.mjs OPENCLAW_DOCKER_ALL_LIVE_MODE=skip -- node scripts/test-docker-all.mjs", + "test:docker:cli-installer-distribution": "bash scripts/e2e/cli-installer-distribution-docker.sh", "test:docker:mcp-channels": "bash scripts/e2e/mcp-channels-docker.sh", "test:docker:mcp-code-mode-gateway": "bash scripts/e2e/mcp-code-mode-gateway-docker.sh", "test:docker:multi-node-update": "bash scripts/e2e/multi-node-update-docker.sh", diff --git a/qa/scenarios/cli/installer-distribution.yaml b/qa/scenarios/cli/installer-distribution.yaml new file mode 100644 index 000000000000..ee95e4993ddf --- /dev/null +++ b/qa/scenarios/cli/installer-distribution.yaml @@ -0,0 +1,33 @@ +title: CLI installer distribution + +scenario: + id: cli-installer-distribution + surface: cli + category: cli.cli-setup + coverage: + primary: + - cli.local-prefix-install + - cli.source-checkout-install + secondary: + - cli.installer-scripts + objective: Verify the hosted installer installs an exact package candidate and the CLI installer builds an exact source checkout into a dedicated Node and OpenClaw prefix. + successCriteria: + - "`install.sh` validates the supported Node runtime, installs the exact package candidate, honors no-onboard mode, and exposes a PATH-resolved CLI." + - "`install-cli.sh` provisions prefix-owned Node and an OpenClaw wrapper without using the system prefix." + - The source checkout is pinned to the exact tested commit, builds successfully, and the wrapper targets its built entrypoint. + - The installed source CLI serves help and version and reports git install kind through update status. + docsRefs: + - docs/install/index.md + - docs/install/updating.md + codeRefs: + - scripts/install.sh + - scripts/install-cli.sh + - scripts/e2e/cli-installer-distribution-docker.sh + execution: + kind: script + path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts + summary: Runs exact-candidate hosted-package and dedicated-prefix source-checkout installer proofs in clean Docker containers. + timeoutMs: 1800000 + args: + - --lane + - cli-installer-distribution diff --git a/qa/scenarios/cli/launcher-runtime.yaml b/qa/scenarios/cli/launcher-runtime.yaml new file mode 100644 index 000000000000..47e91e25f44f --- /dev/null +++ b/qa/scenarios/cli/launcher-runtime.yaml @@ -0,0 +1,26 @@ +title: CLI launcher runtime and recovery + +scenario: + id: cli-launcher-runtime + surface: cli + category: cli.cli-setup + coverage: + primary: + - cli.cli-entrypoint + - cli.supported-node-runtime + objective: Verify the actual OpenClaw launcher accepts supported runtimes, rejects unsupported runtimes, serves help and version, and reports source-build recovery guidance. + successCriteria: + - The copied launcher executes under every supported Node minimum and future supported majors. + - Below-floor and unsupported Node versions exit before loading the CLI with the documented runtime requirement. + - "`--help` and `--version` execute through the launcher fast path." + - A source checkout without built output reports the supported recovery command. + docsRefs: + - docs/install/index.md + - docs/help/troubleshooting.md + codeRefs: + - openclaw.mjs + - test/openclaw-launcher.e2e.test.ts + execution: + kind: vitest + path: test/openclaw-launcher.e2e.test.ts + summary: Executes copied launcher processes across supported and rejected runtime versions, CLI fast paths, and missing-build recovery. diff --git a/qa/scenarios/runtime/docker-package-install.yaml b/qa/scenarios/runtime/docker-package-install.yaml index b63bc65ee30c..ff0e783a7f03 100644 --- a/qa/scenarios/runtime/docker-package-install.yaml +++ b/qa/scenarios/runtime/docker-package-install.yaml @@ -5,16 +5,19 @@ scenario: surface: containers category: containers.image-release-and-validation coverage: + primary: + - cli.package-manager-installs secondary: - containers.docker-e2e-package-artifact-generation - - cli.package-manager-installs - objective: Build the real OpenClaw package artifact, install and execute it in the scheduler-selected bare container image, and record exact package, image, and container identities. + - cli.cli-entrypoint + objective: Build the real OpenClaw package artifact, install it globally with npm, pnpm, and Bun in clean containers, execute each manager-created PATH entrypoint, and record exact identities. successCriteria: - The canonical Docker package helper builds and validates the real npm tarball. - The scheduler prepares the package and bare target image through the shared planner and catalog. - - A clean target container installs the tarball with npm and executes the installed OpenClaw CLI. - - The installed package name and version match the tarball metadata. - - Evidence records the package digest and version, bare image ID, target container ID, installed package root, and executed CLI version. + - Clean target containers install the same tarball globally with npm, pnpm, and Bun. + - Each package manager creates a PATH-resolved OpenClaw executable that serves help and the candidate version. + - Every installed package version matches the tarball metadata. + - Evidence records the package digest and version, bare image ID, manager container IDs, package roots, executable paths, and CLI versions. docsRefs: - docs/install/updating.md - docs/help/testing.md @@ -27,7 +30,7 @@ scenario: execution: kind: script path: test/e2e/qa-lab/runtime/docker-artifact-proof.ts - summary: Builds the real package artifact, installs and executes it in a clean target container, and records artifact identities. + summary: Builds one real package artifact, installs and executes it through npm, pnpm, and Bun PATH entrypoints in clean containers, and records artifact identities. timeoutMs: 1800000 args: - --artifact-base diff --git a/qa/scenarios/runtime/docker-update-channel-switch.yaml b/qa/scenarios/runtime/docker-update-channel-switch.yaml index 84b16667bb95..1a131aef917c 100644 --- a/qa/scenarios/runtime/docker-update-channel-switch.yaml +++ b/qa/scenarios/runtime/docker-update-channel-switch.yaml @@ -7,14 +7,15 @@ scenario: coverage: primary: - cli.install-kind-switching - - cli.update-status-and-rpc - secondary: - cli.update-channels - objective: Verify package-to-git and git-to-package update switching plus machine-readable update status in Docker. + - cli.update-status-and-rpc + objective: Verify stable, beta, and dev channel selection, package-to-git and git-to-package switching, and machine-readable update status in Docker. successCriteria: + - The package install resolves the exact candidate through a hermetic beta dist-tag fixture, persists beta, and remains a package install. - The package install updates to a package-derived git dev fixture. - The git install updates back to the package tarball. - - "`openclaw update status --json` reports the expected git and package status after each switch." + - The beta, dev, and stable selections persist in canonical config. + - "`openclaw update status --json` reports the expected package, git, and package status after each switch." docsRefs: - docs/cli/update.md - docs/install/updating.md @@ -25,7 +26,7 @@ scenario: execution: kind: script path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts - summary: Runs the existing update-channel-switch Docker E2E lane and records QA Lab evidence for install-kind switching and update status coverage. + summary: Runs stable, beta, and dev update selections through the existing Docker E2E lane and records install-kind, persisted-channel, and update-status evidence. args: - --lane - update-channel-switch diff --git a/scripts/e2e/bun-global-install-smoke.sh b/scripts/e2e/bun-global-install-smoke.sh index cb5ad1385631..490a3e13b9ad 100755 --- a/scripts/e2e/bun-global-install-smoke.sh +++ b/scripts/e2e/bun-global-install-smoke.sh @@ -177,12 +177,35 @@ NODE fi echo "==> OpenClaw version through Bun global install" - run_with_timeout "$COMMAND_TIMEOUT_MS" "$openclaw_bin" --version + local openclaw_version + openclaw_version="$(run_with_timeout "$COMMAND_TIMEOUT_MS" "$openclaw_bin" --version)" + printf "%s\n" "$openclaw_version" + + echo "==> OpenClaw help through Bun global install" + run_with_timeout "$COMMAND_TIMEOUT_MS" "$openclaw_bin" --help >/dev/null echo "==> OpenClaw image providers through Bun global install" local providers_json providers_json="$(run_with_timeout "$COMMAND_TIMEOUT_MS" "$openclaw_bin" infer image providers --json)" OPENCLAW_IMAGE_PROVIDERS_JSON="$providers_json" node scripts/e2e/lib/bun-global-install/assertions.mjs assert-image-providers + + if [ -n "${OPENCLAW_BUN_GLOBAL_SMOKE_PROOF_PATH:-}" ]; then + node --input-type=module - \ + "$OPENCLAW_BUN_GLOBAL_SMOKE_PROOF_PATH" \ + "$bun_path" \ + "$openclaw_bin" \ + "$openclaw_version" <<'NODE' +import fs from "node:fs"; +import path from "node:path"; + +const [, , proofPath, bunPath, openclawPath, openclawVersion] = process.argv; +fs.mkdirSync(path.dirname(proofPath), { recursive: true }); +fs.writeFileSync( + proofPath, + `${JSON.stringify({ bunPath, openclawPath, openclawVersion }, null, 2)}\n`, +); +NODE + fi } main "$@" diff --git a/scripts/e2e/cli-installer-distribution-docker.sh b/scripts/e2e/cli-installer-distribution-docker.sh new file mode 100755 index 000000000000..43872afe3ad6 --- /dev/null +++ b/scripts/e2e/cli-installer-distribution-docker.sh @@ -0,0 +1,164 @@ +#!/usr/bin/env bash +# Proves hosted npm installation plus dedicated-prefix source installation. +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-cli-installer-distribution:local")" +PACKAGE_TGZ="$( + docker_e2e_prepare_package_tgz cli-installer-distribution "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}" +)" +HOSTED_PROOF_CONTAINER="openclaw-hosted-installer-proof-$$" +SOURCE_PROOF_CONTAINER="openclaw-source-installer-proof-$$" +SOURCE_BUNDLE="$(mktemp "${TMPDIR:-/tmp}/openclaw-source.XXXXXX.bundle")" +SOURCE_PROOF_SCRIPT="$(mktemp "${TMPDIR:-/tmp}/openclaw-source-proof.XXXXXX.sh")" +SOURCE_SHA="$(git -C "$ROOT_DIR" rev-parse HEAD)" +SOURCE_MEMORY="${OPENCLAW_CLI_INSTALLER_SOURCE_MEMORY:-16g}" + +cleanup() { + docker_e2e_docker_cmd rm -f \ + "$HOSTED_PROOF_CONTAINER" \ + "$SOURCE_PROOF_CONTAINER" >/dev/null 2>&1 || true + docker_e2e_cleanup_package_tgz "$PACKAGE_TGZ" + rm -f "$SOURCE_BUNDLE" "$SOURCE_PROOF_SCRIPT" +} +trap cleanup EXIT + +git -C "$ROOT_DIR" bundle create "$SOURCE_BUNDLE" HEAD +cat >"$SOURCE_PROOF_SCRIPT" <<'SOURCE_PROOF' +#!/usr/bin/env bash +set -euo pipefail + +test -r "$0" +test -x "$0" +command -v curl >/dev/null +git clone -q /tmp/openclaw-source.bundle /tmp/openclaw-source +git -C /tmp/openclaw-source checkout -q --detach "$OPENCLAW_SOURCE_SHA" +bash /tmp/openclaw-source/scripts/install-cli.sh \ + --install-method git \ + --git-dir /tmp/openclaw-source \ + --version "$OPENCLAW_SOURCE_SHA" \ + --no-git-update \ + --prefix /tmp/openclaw-prefix \ + --node-version 24.15.0 \ + --no-onboard + +prefix_node=/tmp/openclaw-prefix/tools/node/bin/node +prefix_cli=/tmp/openclaw-prefix/bin/openclaw +test -x "$prefix_node" +test -x "$prefix_cli" +grep -Fq "exec \"$prefix_node\"" "$prefix_cli" +grep -Fq "/tmp/openclaw-source/dist/entry.js" "$prefix_cli" +export PATH="/tmp/openclaw-prefix/bin:$PATH" +test "$(command -v openclaw)" = "$prefix_cli" +test "$(git -C /tmp/openclaw-source rev-parse HEAD)" = "$OPENCLAW_SOURCE_SHA" +openclaw_version="$(openclaw --version)" +openclaw --help >/tmp/openclaw-help +test -s /tmp/openclaw-help +status_json="$(openclaw update status --json)" +STATUS_JSON="$status_json" node -e " + const status = JSON.parse(process.env.STATUS_JSON); + if (status.update?.installKind !== \"git\") { + throw new Error(\`expected git install kind, got \${status.update?.installKind}\`); + } +" +printf "prefixNode=%s@%s\n" "$prefix_node" "$("$prefix_node" --version)" +printf "prefixOpenClaw=%s@%s\n" "$prefix_cli" "$openclaw_version" +printf "sourceHead=%s installKind=git\n" "$OPENCLAW_SOURCE_SHA" +printf "sourceOnboard=disabled\n" +touch /tmp/openclaw-proof-ready +exec sleep infinity +SOURCE_PROOF +chmod 0555 "$SOURCE_PROOF_SCRIPT" + +docker_e2e_build_or_reuse \ + "$IMAGE_NAME" \ + cli-installer-distribution \ + "$ROOT_DIR/scripts/e2e/Dockerfile" \ + "$ROOT_DIR" \ + bare + +echo "==> Hosted install.sh exact-candidate proof" +docker_e2e_docker_run_cmd run -d \ + --name "$HOSTED_PROOF_CONTAINER" \ + -e HOME=/tmp/openclaw-hosted-home \ + -e OPENCLAW_NO_ONBOARD=1 \ + -e OPENCLAW_NO_PROMPT=1 \ + -v "$PACKAGE_TGZ:/tmp/openclaw-current.tgz:ro" \ + -v "$ROOT_DIR/scripts/install.sh:/tmp/install.sh:ro" \ + "$IMAGE_NAME" \ + bash -lc ' + set -euo pipefail + mkdir -p "$HOME" + bash /tmp/install.sh \ + --install-method npm \ + --version file:/tmp/openclaw-current.tgz \ + --no-onboard \ + --no-prompt + source "$HOME/.bashrc" + hash -r + openclaw_path="$(command -v openclaw)" + test -n "$openclaw_path" + node_path="$(command -v node)" + node_version="$(node --version)" + openclaw_version="$(openclaw --version)" + openclaw --help >/tmp/openclaw-help + test -s /tmp/openclaw-help + printf "hostedNode=%s@%s\n" "$node_path" "$node_version" + printf "hostedOpenClaw=%s@%s\n" "$openclaw_path" "$openclaw_version" + printf "hostedOnboard=disabled\n" + touch /tmp/openclaw-proof-ready + exec sleep infinity + ' >/dev/null + +# A full source install builds every workspace package; the shared 8g cap OOMs before wrapper creation. +echo "==> install-cli.sh dedicated-prefix source-checkout proof" +docker_e2e_docker_run_cmd run -d \ + --name "$SOURCE_PROOF_CONTAINER" \ + --memory "$SOURCE_MEMORY" \ + -e HOME=/tmp/openclaw-source-home \ + -e OPENCLAW_NO_ONBOARD=1 \ + -e OPENCLAW_NO_PROMPT=1 \ + -e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ + -e "OPENCLAW_SOURCE_SHA=$SOURCE_SHA" \ + --user root \ + -v "$SOURCE_BUNDLE:/tmp/openclaw-source.bundle:ro" \ + -v "$SOURCE_PROOF_SCRIPT:/tmp/source-proof.sh:ro" \ + "$IMAGE_NAME" \ + bash -lc ' + set -euo pipefail + apt-get update + DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends curl + rm -rf /var/lib/apt/lists/* + command -v curl >/dev/null + install -d -o appuser -g appuser "$HOME" + exec runuser -u appuser -- env \ + HOME="$HOME" \ + OPENCLAW_NO_ONBOARD="$OPENCLAW_NO_ONBOARD" \ + OPENCLAW_NO_PROMPT="$OPENCLAW_NO_PROMPT" \ + COREPACK_ENABLE_DOWNLOAD_PROMPT="$COREPACK_ENABLE_DOWNLOAD_PROMPT" \ + OPENCLAW_SOURCE_SHA="$OPENCLAW_SOURCE_SHA" \ + bash /tmp/source-proof.sh + ' >/dev/null + +wait_for_proof() { + local container_name="$1" + for _ in $(seq 1 1200); do + if docker exec "$container_name" test -f /tmp/openclaw-proof-ready; then + docker logs "$container_name" + return 0 + fi + if [ "$(docker inspect --format '{{.State.Running}}' "$container_name")" != "true" ]; then + docker logs "$container_name" >&2 + return 1 + fi + sleep 1 + done + docker logs "$container_name" >&2 + return 1 +} + +wait_for_proof "$HOSTED_PROOF_CONTAINER" +wait_for_proof "$SOURCE_PROOF_CONTAINER" +echo "CLI installer distribution proof passed." diff --git a/scripts/e2e/docker-package-install.sh b/scripts/e2e/docker-package-install.sh index 16fe9c025eb7..ed1e3e289bd1 100755 --- a/scripts/e2e/docker-package-install.sh +++ b/scripts/e2e/docker-package-install.sh @@ -7,62 +7,157 @@ source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-docker-e2e-bare:local")" PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz docker-package-install "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")" IDENTITY_PATH="${OPENCLAW_DOCKER_ARTIFACT_IDENTITY_PATH:-$ROOT_DIR/.artifacts/docker-tests/docker-package-install-identities.json}" -CONTAINER_NAME="openclaw-package-proof-$$" +NPM_PROOF_CONTAINER="openclaw-package-npm-proof-$$" +PNPM_PROOF_CONTAINER="openclaw-package-pnpm-proof-$$" +BUN_PROOF_CONTAINER="openclaw-package-bun-proof-$$" DOCKER_RUN_TIMEOUT="${OPENCLAW_DOCKER_PACKAGE_INSTALL_RUN_TIMEOUT:-120s}" +BUN_HARNESS_DIR="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-bun-harness.XXXXXX")" cleanup() { - docker_e2e_docker_cmd rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true + docker_e2e_docker_cmd rm -f \ + "$NPM_PROOF_CONTAINER" \ + "$PNPM_PROOF_CONTAINER" \ + "$BUN_PROOF_CONTAINER" >/dev/null 2>&1 || true docker_e2e_cleanup_package_tgz "$PACKAGE_TGZ" + rm -rf "$BUN_HARNESS_DIR" } trap cleanup EXIT docker_e2e_build_or_reuse "$IMAGE_NAME" docker-package-install "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" bare -echo "Installing the real OpenClaw package artifact in the target container..." +for harness_path in \ + scripts/e2e/bun-global-install-smoke.sh \ + scripts/e2e/lib/bun-global-install/assertions.mjs \ + scripts/lib/docker-e2e-container.sh \ + scripts/lib/docker-e2e-logs.sh \ + scripts/lib/docker-e2e-package.sh \ + scripts/lib/docker-e2e-resource-diagnostics.sh; do + mkdir -p "$BUN_HARNESS_DIR/$(dirname "$harness_path")" + cp "$ROOT_DIR/$harness_path" "$BUN_HARNESS_DIR/$harness_path" +done +chmod -R a+rX "$BUN_HARNESS_DIR" + +echo "Installing the real OpenClaw package artifact with npm..." DOCKER_COMMAND_TIMEOUT="$DOCKER_RUN_TIMEOUT" docker_e2e_docker_run_cmd run -d \ - --name "$CONTAINER_NAME" \ + --name "$NPM_PROOF_CONTAINER" \ -v "$PACKAGE_TGZ:/tmp/openclaw-current.tgz:ro" \ "$IMAGE_NAME" \ bash -lc ' set -euo pipefail npm install -g --prefix /tmp/openclaw-proof /tmp/openclaw-current.tgz --no-fund --no-audit + export PATH="/tmp/openclaw-proof/bin:$PATH" package_root=/tmp/openclaw-proof/lib/node_modules/openclaw - "$package_root/openclaw.mjs" --version > /tmp/openclaw-version - "$package_root/openclaw.mjs" --help > /tmp/openclaw-help + test "$(command -v openclaw)" = "/tmp/openclaw-proof/bin/openclaw" + openclaw --version > /tmp/openclaw-version + openclaw --help > /tmp/openclaw-help test -s /tmp/openclaw-help touch /tmp/openclaw-proof-ready exec sleep infinity ' >/dev/null -for _ in $(seq 1 240); do - if docker exec "$CONTAINER_NAME" test -f /tmp/openclaw-proof-ready; then - break - fi - if [ "$(docker inspect --format '{{.State.Running}}' "$CONTAINER_NAME")" != "true" ]; then - docker logs "$CONTAINER_NAME" >&2 +echo "Installing the real OpenClaw package artifact with pnpm..." +DOCKER_COMMAND_TIMEOUT="$DOCKER_RUN_TIMEOUT" docker_e2e_docker_run_cmd run -d \ + --name "$PNPM_PROOF_CONTAINER" \ + -v "$PACKAGE_TGZ:/tmp/openclaw-current.tgz:ro" \ + "$IMAGE_NAME" \ + bash -lc ' + set -euo pipefail + export PNPM_HOME=/tmp/pnpm-home + export PATH="$PNPM_HOME:$PATH" + corepack prepare pnpm@11.15.1 --activate + pnpm config set global-bin-dir "$PNPM_HOME" + pnpm config set global-dir /tmp/pnpm-global + pnpm add --global --allow-build=openclaw /tmp/openclaw-current.tgz + test "$(command -v openclaw)" = "$PNPM_HOME/openclaw" + package_root="$(pnpm root --global)/openclaw" + printf "%s\n" "$package_root" > /tmp/openclaw-package-root + openclaw --version > /tmp/openclaw-version + openclaw --help > /tmp/openclaw-help + test -s /tmp/openclaw-help + touch /tmp/openclaw-proof-ready + exec sleep infinity + ' >/dev/null + +echo "Installing the real OpenClaw package artifact with Bun..." +DOCKER_COMMAND_TIMEOUT="$DOCKER_RUN_TIMEOUT" docker_e2e_docker_run_cmd run -d \ + --name "$BUN_PROOF_CONTAINER" \ + -v "$PACKAGE_TGZ:/tmp/openclaw-current.tgz:ro" \ + -v "$BUN_HARNESS_DIR:/repo:ro" \ + "$IMAGE_NAME" \ + bash -lc ' + set -euo pipefail + npm install -g --prefix /tmp/bun-runtime bun@1.3.14 --no-fund --no-audit + cd /repo + BUN_BIN=/tmp/bun-runtime/bin/bun \ + OPENCLAW_BUN_GLOBAL_SMOKE_HOST_BUILD=0 \ + OPENCLAW_BUN_GLOBAL_SMOKE_PACKAGE_TGZ=/tmp/openclaw-current.tgz \ + OPENCLAW_BUN_GLOBAL_SMOKE_PROOF_PATH=/tmp/openclaw-bun-proof.json \ + bash scripts/e2e/bun-global-install-smoke.sh + touch /tmp/openclaw-proof-ready + exec sleep infinity + ' >/dev/null + +wait_for_proof() { + local container_name="$1" + for _ in $(seq 1 240); do + if docker exec "$container_name" test -f /tmp/openclaw-proof-ready; then + return 0 + fi + if [ "$(docker inspect --format '{{.State.Running}}' "$container_name")" != "true" ]; then + docker logs "$container_name" >&2 + return 1 + fi + sleep 1 + done + docker logs "$container_name" >&2 + return 1 +} + +for container_name in "$NPM_PROOF_CONTAINER" "$PNPM_PROOF_CONTAINER" "$BUN_PROOF_CONTAINER"; do + wait_for_proof "$container_name" +done + +NPM_PACKAGE_ROOT="/tmp/openclaw-proof/lib/node_modules/openclaw" +NPM_INSTALLED_VERSION="$(docker exec "$NPM_PROOF_CONTAINER" cat /tmp/openclaw-version | tr -d '\r\n')" +PNPM_PACKAGE_ROOT="$(docker exec "$PNPM_PROOF_CONTAINER" cat /tmp/openclaw-package-root | tr -d '\r\n')" +PNPM_INSTALLED_VERSION="$(docker exec "$PNPM_PROOF_CONTAINER" cat /tmp/openclaw-version | tr -d '\r\n')" +BUN_OPENCLAW_PATH="$( + docker exec "$BUN_PROOF_CONTAINER" \ + node -p 'JSON.parse(require("node:fs").readFileSync("/tmp/openclaw-bun-proof.json", "utf8")).openclawPath' +)" +BUN_INSTALLED_VERSION="$( + docker exec "$BUN_PROOF_CONTAINER" \ + node -p 'JSON.parse(require("node:fs").readFileSync("/tmp/openclaw-bun-proof.json", "utf8")).openclawVersion' +)" +PACKAGE_VERSION="$(docker exec "$NPM_PROOF_CONTAINER" node -p "require('$NPM_PACKAGE_ROOT/package.json').version")" +for installed_version in "$NPM_INSTALLED_VERSION" "$PNPM_INSTALLED_VERSION" "$BUN_INSTALLED_VERSION"; do + if [[ "$installed_version" != *"$PACKAGE_VERSION"* ]]; then + echo "installed CLI output $installed_version does not contain package version $PACKAGE_VERSION" >&2 exit 1 fi - sleep 1 done -docker exec "$CONTAINER_NAME" test -f /tmp/openclaw-proof-ready - -INSTALLED_VERSION="$(docker exec "$CONTAINER_NAME" cat /tmp/openclaw-version | tr -d '\r\n')" -PACKAGE_ROOT="/tmp/openclaw-proof/lib/node_modules/openclaw" -PACKAGE_VERSION="$(docker exec "$CONTAINER_NAME" node -p "require('$PACKAGE_ROOT/package.json').version")" -if [[ "$INSTALLED_VERSION" != *"$PACKAGE_VERSION"* ]]; then - echo "installed CLI output $INSTALLED_VERSION does not contain package version $PACKAGE_VERSION" >&2 - exit 1 -fi node --import tsx "$ROOT_DIR/scripts/e2e/lib/docker-artifact-proof/write-identities.ts" \ --scenario docker-package-install \ --output "$IDENTITY_PATH" \ --image "$IMAGE_NAME" \ --package "$PACKAGE_TGZ" \ - --container "target=$CONTAINER_NAME" \ - --detail "target:installedPackageRoot=$PACKAGE_ROOT" \ - --detail "target:installedPackageVersion=$PACKAGE_VERSION" \ - --detail "target:openclawVersion=$INSTALLED_VERSION" \ - --detail "target:helpCommand=passed" + --container "npm=$NPM_PROOF_CONTAINER" \ + --container "pnpm=$PNPM_PROOF_CONTAINER" \ + --container "bun=$BUN_PROOF_CONTAINER" \ + --detail "npm:installedPackageRoot=$NPM_PACKAGE_ROOT" \ + --detail "npm:installedPackageVersion=$PACKAGE_VERSION" \ + --detail "npm:openclawVersion=$NPM_INSTALLED_VERSION" \ + --detail "npm:openclawPath=/tmp/openclaw-proof/bin/openclaw" \ + --detail "npm:helpCommand=passed" \ + --detail "pnpm:installedPackageRoot=$PNPM_PACKAGE_ROOT" \ + --detail "pnpm:installedPackageVersion=$PACKAGE_VERSION" \ + --detail "pnpm:openclawVersion=$PNPM_INSTALLED_VERSION" \ + --detail "pnpm:openclawPath=/tmp/pnpm-home/openclaw" \ + --detail "pnpm:helpCommand=passed" \ + --detail "bun:installedPackageVersion=$PACKAGE_VERSION" \ + --detail "bun:openclawVersion=$BUN_INSTALLED_VERSION" \ + --detail "bun:openclawPath=$BUN_OPENCLAW_PATH" \ + --detail "bun:helpCommand=passed" -echo "Package artifact container proof passed." +echo "npm, pnpm, and Bun package artifact proofs passed." diff --git a/scripts/e2e/lib/plugins/npm-registry-server.mjs b/scripts/e2e/lib/plugins/npm-registry-server.mjs index e10c3f8ce353..dff4b8f17ba3 100644 --- a/scripts/e2e/lib/plugins/npm-registry-server.mjs +++ b/scripts/e2e/lib/plugins/npm-registry-server.mjs @@ -30,6 +30,21 @@ function normalizeUpstreamRegistry(raw) { } const upstreamRegistry = normalizeUpstreamRegistry(process.env.OPENCLAW_NPM_REGISTRY_UPSTREAM); +const distTagOverrides = new Map( + (process.env.OPENCLAW_NPM_REGISTRY_DIST_TAGS ?? "") + .split(",") + .map((entry) => entry.trim()) + .filter(Boolean) + .map((entry) => { + const separator = entry.indexOf("="); + if (separator <= 0 || separator === entry.length - 1) { + throw new Error( + "OPENCLAW_NPM_REGISTRY_DIST_TAGS must contain comma-separated tag=version entries", + ); + } + return [entry.slice(0, separator).trim(), entry.slice(separator + 1).trim()]; + }), +); // Match other E2E package-download budgets while keeping public-registry hops // inside the install deadline and decoded bodies inside a fixed memory budget. const UPSTREAM_REQUEST_TIMEOUT_MS = 120_000; @@ -88,7 +103,10 @@ for (let index = 0; index < packageArgs.length; index += 3) { const metadataFor = (entry, baseUrl) => ({ name: entry.packageName, - "dist-tags": { latest: entry.latestVersion }, + "dist-tags": { + latest: entry.latestVersion, + ...Object.fromEntries(distTagOverrides), + }, versions: Object.fromEntries( [...entry.versions.entries()].map(([version, versionEntry]) => [ version, diff --git a/scripts/e2e/lib/update-channel-switch/assertions.mjs b/scripts/e2e/lib/update-channel-switch/assertions.mjs index b022e3a4142e..65882ed225bc 100644 --- a/scripts/e2e/lib/update-channel-switch/assertions.mjs +++ b/scripts/e2e/lib/update-channel-switch/assertions.mjs @@ -8,7 +8,7 @@ const controlUiHtml = "fixture\n"; function usage() { console.error( - "usage: assertions.mjs [...]", + "usage: assertions.mjs [...]", ); process.exit(2); } @@ -163,8 +163,8 @@ function assertUpdate(channel) { if (channel === "dev" && payload.mode !== "git") { throw new Error(`expected dev update mode git, got ${payload.mode}`); } - if (channel === "stable" && !["npm", "pnpm", "bun"].includes(payload.mode)) { - throw new Error(`expected package-manager mode after stable switch, got ${payload.mode}`); + if (["stable", "beta"].includes(channel) && !["npm", "pnpm", "bun"].includes(payload.mode)) { + throw new Error(`expected package-manager mode after ${channel} switch, got ${payload.mode}`); } if (payload.postUpdate?.plugins && payload.postUpdate.plugins.status !== "ok") { throw new Error( @@ -196,6 +196,15 @@ function assertStatusKind(kind) { } } +function assertInstalledVersion(root, expectedVersion) { + const manifest = readJson(path.join(root, "package.json")); + if (manifest.version !== expectedVersion) { + throw new Error( + `expected installed openclaw ${expectedVersion}, got ${String(manifest.version)}`, + ); + } +} + switch (command) { case "prepare-git-fixture": prepareGitFixture(args[0] ?? "/tmp/openclaw-git"); @@ -212,6 +221,9 @@ switch (command) { case "assert-status-kind": assertStatusKind(args[0]); break; + case "assert-installed-version": + assertInstalledVersion(args[0], args[1]); + break; default: usage(); } diff --git a/scripts/e2e/update-channel-switch-docker.sh b/scripts/e2e/update-channel-switch-docker.sh index 500b6b0f08d5..b40c311d0a3e 100755 --- a/scripts/e2e/update-channel-switch-docker.sh +++ b/scripts/e2e/update-channel-switch-docker.sh @@ -93,11 +93,69 @@ export OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT command -v openclaw >/dev/null openclaw_e2e_enable_openclaw_cli_timeout +registry_port_file=/tmp/openclaw-update-channel-registry.port +registry_log=/tmp/openclaw-update-channel-registry.log +registry_pid="" +cleanup_registry() { + if [ -n "$registry_pid" ]; then + kill "$registry_pid" 2>/dev/null || true + wait "$registry_pid" 2>/dev/null || true + fi +} +trap cleanup_registry EXIT +rm -f "$registry_port_file" +OPENCLAW_NPM_REGISTRY_DIST_TAGS="latest=0.0.0,beta=$package_version" \ + OPENCLAW_NPM_REGISTRY_UPSTREAM=https://registry.npmjs.org \ + node scripts/e2e/lib/plugins/npm-registry-server.mjs \ + "$registry_port_file" \ + openclaw \ + "$package_version" \ + "$package_tgz" \ + >"$registry_log" 2>&1 & +registry_pid="$!" +for _ in $(seq 1 100); do + if [ -s "$registry_port_file" ]; then + break + fi + if ! kill -0 "$registry_pid" 2>/dev/null; then + openclaw_e2e_print_log "$registry_log" >&2 + exit 1 + fi + sleep 0.1 +done +if [ ! -s "$registry_port_file" ]; then + openclaw_e2e_print_log "$registry_log" >&2 + echo "Timed out waiting for update-channel npm fixture registry." >&2 + exit 1 +fi +export NPM_CONFIG_REGISTRY="http://127.0.0.1:$(cat "$registry_port_file")" +export npm_config_registry="$NPM_CONFIG_REGISTRY" + openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}" export OPENCLAW_GIT_DIR="$git_root" export OPENCLAW_UPDATE_DEV_TARGET_REF="$fixture_sha" +echo "==> package stable -> package beta channel" +set +e +beta_json="$(openclaw update --channel beta --yes --json --no-restart)" +beta_status=$? +set -e +printf "%s\n" "$beta_json" +if [ "$beta_status" -ne 0 ]; then + exit "$beta_status" +fi +UPDATE_JSON="$beta_json" node scripts/e2e/lib/update-channel-switch/assertions.mjs assert-update beta +node scripts/e2e/lib/update-channel-switch/assertions.mjs assert-config-channel beta +node scripts/e2e/lib/update-channel-switch/assertions.mjs \ + assert-installed-version \ + /tmp/npm-prefix/lib/node_modules/openclaw \ + "$package_version" + +status_json="$(openclaw update status --json)" +printf "%s\n" "$status_json" +STATUS_JSON="$status_json" node scripts/e2e/lib/update-channel-switch/assertions.mjs assert-status-kind package + echo "==> package -> git dev channel" set +e dev_json="$(openclaw update --channel dev --yes --json --no-restart)" diff --git a/scripts/lib/docker-e2e-scenarios.mjs b/scripts/lib/docker-e2e-scenarios.mjs index 3f7d1ac6481a..0e5cfb80f99f 100644 --- a/scripts/lib/docker-e2e-scenarios.mjs +++ b/scripts/lib/docker-e2e-scenarios.mjs @@ -313,6 +313,15 @@ export const mainLanes = [ timeoutMs: 20 * 60 * 1000, weight: 3, }), + npmLane( + "cli-installer-distribution", + "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:cli-installer-distribution", + { + stateScenario: "empty", + timeoutMs: 30 * 60 * 1000, + weight: 3, + }, + ), npmLane( "docker-package-install", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:package-install", diff --git a/test/e2e/qa-lab/runtime/docker-e2e-lane.fixture.test.ts b/test/e2e/qa-lab/runtime/docker-e2e-lane.fixture.test.ts index 15722b2a9b74..c4061c6ce5b1 100644 --- a/test/e2e/qa-lab/runtime/docker-e2e-lane.fixture.test.ts +++ b/test/e2e/qa-lab/runtime/docker-e2e-lane.fixture.test.ts @@ -13,6 +13,7 @@ describe("QA Docker E2E lane fixture", () => { expect(listQaDockerE2eLaneNames()).toEqual( expect.arrayContaining([ "agent-bundle-mcp-tools", + "cli-installer-distribution", "codex-on-demand", "system-agent-first-run", "gateway-network", @@ -48,6 +49,9 @@ describe("QA Docker E2E lane fixture", () => { expect(resolveQaDockerE2eLane("codex-on-demand", {}).script).toBe( "scripts/e2e/codex-on-demand-docker.sh", ); + expect(resolveQaDockerE2eLane("cli-installer-distribution", {}).script).toBe( + "scripts/e2e/cli-installer-distribution-docker.sh", + ); const updateMigration = resolveQaDockerE2eLane("update-migration", { OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC: "openclaw@custom", diff --git a/test/e2e/qa-lab/runtime/docker-e2e-lane.fixture.ts b/test/e2e/qa-lab/runtime/docker-e2e-lane.fixture.ts index 9ba7ae1b7c50..b979dc51fb70 100644 --- a/test/e2e/qa-lab/runtime/docker-e2e-lane.fixture.ts +++ b/test/e2e/qa-lab/runtime/docker-e2e-lane.fixture.ts @@ -31,6 +31,9 @@ const QA_DOCKER_E2E_LANES = { "codex-on-demand": { script: "scripts/e2e/codex-on-demand-docker.sh", }, + "cli-installer-distribution": { + script: "scripts/e2e/cli-installer-distribution-docker.sh", + }, "system-agent-first-run": { script: "scripts/e2e/system-agent-first-run-docker.sh", }, diff --git a/test/scripts/docker-all-scheduler.test.ts b/test/scripts/docker-all-scheduler.test.ts index 4bbdac3f7d7d..2cc8f801b624 100644 --- a/test/scripts/docker-all-scheduler.test.ts +++ b/test/scripts/docker-all-scheduler.test.ts @@ -209,6 +209,28 @@ describe("scripts/test-docker-all scheduler", () => { expect(result.stderr).not.toContain("at "); }); + it("selects the CLI installer distribution lane through the scheduler catalog", () => { + const result = spawnSync(process.execPath, ["scripts/test-docker-all.mjs"], { + cwd: process.cwd(), + encoding: "utf8", + env: { + ...process.env, + OPENCLAW_DOCKER_ALL_BUILD: "0", + OPENCLAW_DOCKER_ALL_DRY_RUN: "1", + OPENCLAW_DOCKER_ALL_LANES: "cli-installer-distribution", + OPENCLAW_DOCKER_ALL_PREFLIGHT: "0", + OPENCLAW_DOCKER_ALL_TIMINGS: "0", + }, + }); + + expect(result.status, result.stderr).toBe(0); + expect(result.stdout).toContain("Selected lanes: cli-installer-distribution"); + expect(result.stdout).toContain( + "cli-installer-distribution(w=3 r=docker,npm timeout=1800s image=bare state=empty)", + ); + expect(result.stdout).toContain("Dry run complete"); + }); + it("reuses only registry-backed images in generated workflow reruns", () => { const localCommand = githubWorkflowRerunCommand(["install-e2e"], "a".repeat(40), { GITHUB_REF_NAME: "full-release-validation-temp-deleted", diff --git a/test/scripts/docker-build-helper.test.ts b/test/scripts/docker-build-helper.test.ts index 5817d85d814d..c627cc900526 100644 --- a/test/scripts/docker-build-helper.test.ts +++ b/test/scripts/docker-build-helper.test.ts @@ -15,6 +15,7 @@ const DOCKER_E2E_PACKAGE_HELPER_PATH = "scripts/lib/docker-e2e-package.sh"; const DOCKER_E2E_IMAGE_HELPER_PATH = "scripts/lib/docker-e2e-image.sh"; const DOCKER_E2E_SCENARIOS_PATH = "scripts/lib/docker-e2e-scenarios.mjs"; const COMPOSE_SETUP_E2E_PATH = "scripts/e2e/compose-setup.sh"; +const CLI_INSTALLER_DISTRIBUTION_E2E_PATH = "scripts/e2e/cli-installer-distribution-docker.sh"; const DOCKER_PACKAGE_INSTALL_E2E_PATH = "scripts/e2e/docker-package-install.sh"; const INSTALL_E2E_RUNNER_PATH = "scripts/docker/install-sh-e2e/run.sh"; const CLEANUP_DOCKER_SMOKE_PATH = "scripts/test-cleanup-docker.sh"; @@ -3518,8 +3519,8 @@ heartbeat_elapsed="\${BASH_REMATCH[1]}" expect(composeRunner).toContain('docker_e2e_docker_cmd rm -f "$CLI_NAME"'); const packageRunner = readFileSync(DOCKER_PACKAGE_INSTALL_E2E_PATH, "utf8"); - expect(packageRunner).not.toMatch(/(^|\n)\s*docker rm -f "\$CONTAINER_NAME"/u); - expect(packageRunner).toContain('docker_e2e_docker_cmd rm -f "$CONTAINER_NAME"'); + expect(packageRunner).not.toMatch(/(^|\n)\s*docker rm -f/u); + expect(packageRunner).toContain("docker_e2e_docker_cmd rm -f"); expect(packageRunner).toContain( 'DOCKER_RUN_TIMEOUT="${OPENCLAW_DOCKER_PACKAGE_INSTALL_RUN_TIMEOUT:-120s}"', ); @@ -3534,6 +3535,66 @@ heartbeat_elapsed="\${BASH_REMATCH[1]}" } }); + it("executes each CLI distribution boundary instead of promoting metadata", () => { + const installerRunner = readFileSync(CLI_INSTALLER_DISTRIBUTION_E2E_PATH, "utf8"); + const packageRunner = readFileSync(DOCKER_PACKAGE_INSTALL_E2E_PATH, "utf8"); + const updateRunner = readFileSync(UPDATE_CHANNEL_SWITCH_DOCKER_E2E_PATH, "utf8"); + + expectTextToIncludeAll(packageRunner, [ + "npm install -g --prefix /tmp/openclaw-proof", + "pnpm add --global --allow-build=openclaw", + "bun@1.3.14", + 'test "$(command -v openclaw)" = "/tmp/openclaw-proof/bin/openclaw"', + 'test "$(command -v openclaw)" = "$PNPM_HOME/openclaw"', + "OPENCLAW_BUN_GLOBAL_SMOKE_PROOF_PATH", + 'BUN_HARNESS_DIR="$(mktemp -d', + "chmod -R a+rX", + '-v "$BUN_HARNESS_DIR:/repo:ro"', + '--container "npm=$NPM_PROOF_CONTAINER"', + '--container "pnpm=$PNPM_PROOF_CONTAINER"', + '--container "bun=$BUN_PROOF_CONTAINER"', + ]); + expect(packageRunner).not.toContain('-v "$ROOT_DIR:/repo:ro"'); + expectTextToIncludeAll(installerRunner, [ + "bash /tmp/install.sh", + "--version file:/tmp/openclaw-current.tgz", + 'source "$HOME/.bashrc"', + "hash -r", + "bash /tmp/openclaw-source/scripts/install-cli.sh", + "--install-method git", + "--prefix /tmp/openclaw-prefix", + "--node-version 24.15.0", + "apt-get install -y --no-install-recommends curl", + "command -v curl >/dev/null", + 'chmod 0555 "$SOURCE_PROOF_SCRIPT"', + 'SOURCE_MEMORY="${OPENCLAW_CLI_INSTALLER_SOURCE_MEMORY:-16g}"', + '--memory "$SOURCE_MEMORY"', + "runuser -u appuser", + 'test -r "$0"', + 'test -x "$0"', + 'grep -Fq "/tmp/openclaw-source/dist/entry.js" "$prefix_cli"', + "openclaw update status --json", + "expected git install kind", + ]); + expect(installerRunner.match(/--memory "\$SOURCE_MEMORY"/gu)).toHaveLength(1); + expect(installerRunner.indexOf('--memory "$SOURCE_MEMORY"')).toBeGreaterThan( + installerRunner.indexOf('echo "==> install-cli.sh dedicated-prefix source-checkout proof"'), + ); + expectTextToIncludeAll(updateRunner, [ + "openclaw update --channel beta", + 'OPENCLAW_NPM_REGISTRY_DIST_TAGS="latest=0.0.0,beta=$package_version"', + "OPENCLAW_NPM_REGISTRY_UPSTREAM=https://registry.npmjs.org", + "assert-update beta", + "assert-config-channel beta", + "assert-installed-version", + "assert-status-kind package", + "openclaw update --channel dev", + "openclaw update --channel stable", + ]); + expect(updateRunner).toContain("openclaw update --channel beta --yes --json --no-restart"); + expect(updateRunner).not.toContain("openclaw update --channel beta --tag"); + }); + it("routes the gateway network client through the timeout-aware run helper", () => { const runner = readFileSync(GATEWAY_NETWORK_DOCKER_E2E_PATH, "utf8"); expect(runner).toContain( @@ -3856,6 +3917,7 @@ heartbeat_elapsed="\${BASH_REMATCH[1]}" "Package $package_version must support gateway install --wrapper.", ); expect(updateChannel).toContain("assert-config-channel dev"); + expect(updateChannel).toContain("assert-config-channel beta"); expect(updateChannelAssertions).toContain("expected persisted update.channel ${channel}"); expect(pluginsAssertions).toContain("expected modern installRecords in installed plugin index"); }); @@ -4346,7 +4408,8 @@ heartbeat_elapsed="\${BASH_REMATCH[1]}" ]); expectTextToIncludeAll(npmRegistry, [ - '"dist-tags": { latest: entry.latestVersion }', + "OPENCLAW_NPM_REGISTRY_DIST_TAGS", + "Object.fromEntries(distTagOverrides)", "existing.latestVersion = version", "packageArgs.length % 3", ]); diff --git a/test/scripts/docker-e2e-plan.test.ts b/test/scripts/docker-e2e-plan.test.ts index 23fe57f7cc34..5513b3d3b5ac 100644 --- a/test/scripts/docker-e2e-plan.test.ts +++ b/test/scripts/docker-e2e-plan.test.ts @@ -244,12 +244,22 @@ describe("scripts/lib/docker-e2e-plan", () => { expect(lane.command).toContain('bash "$harness/scripts/e2e/codex-npm-plugin-live-docker.sh"'); }); - it("plans package-backed Compose and package artifact proofs", () => { + it("plans package-backed installer, Compose, and package artifact proofs", () => { const plan = planFor({ - selectedLaneNames: ["compose-setup", "docker-package-install"], + selectedLaneNames: ["cli-installer-distribution", "compose-setup", "docker-package-install"], }); expect(plan.lanes.map(summarizeLane)).toEqual([ + { + command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:cli-installer-distribution", + imageKind: "bare", + live: false, + name: "cli-installer-distribution", + resources: ["docker", "npm"], + stateScenario: "empty", + timeoutMs: 1_800_000, + weight: 3, + }, { command: "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:compose-setup", imageKind: "functional", diff --git a/test/scripts/plugins-assertions.test.ts b/test/scripts/plugins-assertions.test.ts index eeee3834479a..b9d8491eb328 100644 --- a/test/scripts/plugins-assertions.test.ts +++ b/test/scripts/plugins-assertions.test.ts @@ -775,6 +775,10 @@ ${command} ], { cwd: process.cwd(), + env: { + ...process.env, + OPENCLAW_NPM_REGISTRY_DIST_TAGS: "latest=0.0.0,beta=2026.7.1-beta.3", + }, stdio: ["ignore", "pipe", "pipe"], }, ); @@ -787,6 +791,10 @@ ${command} const metadata = JSON.parse(response.body); expect(response.statusCode).toBe(200); + expect(metadata["dist-tags"]).toEqual({ + latest: "0.0.0", + beta: "2026.7.1-beta.3", + }); expect(metadata.versions["2026.7.1-beta.3"].dependencies).toEqual({ "@openclaw/ai": "2026.7.1-beta.3", zod: "4.3.6", diff --git a/test/scripts/test-install-sh-docker.test.ts b/test/scripts/test-install-sh-docker.test.ts index d3b3d1f797ba..acb15dbe6343 100644 --- a/test/scripts/test-install-sh-docker.test.ts +++ b/test/scripts/test-install-sh-docker.test.ts @@ -1608,6 +1608,8 @@ describe("bun global install smoke", () => { expect(script).toContain("--output-name openclaw-current.tgz"); expect(script).not.toContain("npm pack --ignore-scripts --json --pack-destination"); expect(script).toContain('"$bun_path" install -g "$PACKAGE_TGZ" --no-progress'); + expect(script).toContain('"$openclaw_bin" --help'); + expect(script).toContain("OPENCLAW_BUN_GLOBAL_SMOKE_PROOF_PATH"); expect(script).toContain("infer image providers --json"); expect(script).toContain("assert-image-providers"); expect(assertions).toContain("image providers output is missing bundled provider"); diff --git a/test/scripts/test-projects.test.ts b/test/scripts/test-projects.test.ts index f4101b1bb3aa..c06dc9813016 100644 --- a/test/scripts/test-projects.test.ts +++ b/test/scripts/test-projects.test.ts @@ -763,6 +763,10 @@ describe("scripts/test-projects changed-target routing", () => { "test/scripts/docker-e2e-plan.test.ts", "test/scripts/package-acceptance-workflow.test.ts", ], + "scripts/e2e/cli-installer-distribution-docker.sh": [ + "test/scripts/docker-build-helper.test.ts", + "test/scripts/docker-e2e-plan.test.ts", + ], "scripts/e2e/update-channel-switch-docker.sh": [ "test/scripts/docker-build-helper.test.ts", "test/scripts/docker-e2e-plan.test.ts",