test(qa): cover CLI distribution workflows (#118948)

This commit is contained in:
Vincent Koc
2026-08-04 05:09:39 +08:00
committed by GitHub
parent 7f9994d4f9
commit 5b1b1e0632
20 changed files with 608 additions and 49 deletions
+1
View File
@@ -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",
@@ -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
+26
View File
@@ -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.
@@ -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
@@ -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
+24 -1
View File
@@ -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 "$@"
+164
View File
@@ -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."
+123 -28
View File
@@ -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."
@@ -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,
@@ -8,7 +8,7 @@ const controlUiHtml = "<!doctype html><title>fixture</title>\n";
function usage() {
console.error(
"usage: assertions.mjs <prepare-git-fixture|write-control-ui|assert-update|assert-config-channel|assert-status-kind> [...]",
"usage: assertions.mjs <prepare-git-fixture|write-control-ui|assert-update|assert-config-channel|assert-status-kind|assert-installed-version> [...]",
);
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();
}
@@ -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)"
+9
View File
@@ -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",
@@ -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",
@@ -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",
},
+22
View File
@@ -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",
+66 -3
View File
@@ -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",
]);
+12 -2
View File
@@ -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",
+8
View File
@@ -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",
@@ -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");
+4
View File
@@ -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",