diff --git a/.github/workflows/install-smoke-reusable.yml b/.github/workflows/install-smoke-reusable.yml index 041d5a3e51cf..b43c6e108f2a 100644 --- a/.github/workflows/install-smoke-reusable.yml +++ b/.github/workflows/install-smoke-reusable.yml @@ -13,7 +13,7 @@ on: required: false type: string run_bun_global_install_smoke: - description: Run the Bun global install image-provider smoke + description: Run the Bun global install and runtime smoke required: false default: true type: boolean diff --git a/.github/workflows/install-smoke.yml b/.github/workflows/install-smoke.yml index 70906837ffa8..e3ecd237fcdc 100644 --- a/.github/workflows/install-smoke.yml +++ b/.github/workflows/install-smoke.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: inputs: run_bun_global_install_smoke: - description: Run the Bun global install image-provider smoke + description: Run the Bun global install and runtime smoke required: false default: false type: boolean diff --git a/docs/ci.md b/docs/ci.md index e6eafc49958b..c72c8e61012b 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -603,7 +603,7 @@ The `Install Smoke` workflow no longer runs on pull requests or `main` pushes. I - The root Dockerfile smoke image is built once per target SHA, bound to the workflow revision and producer attempt in an immutable artifact, then loaded by the CLI smoke, agents delete shared-workspace CLI smoke, container gateway-network E2E, and bundled `matrix` plugin build-arg smoke. The plugin smoke verifies runtime dependency install mirroring and that the plugin loads without entry-escape diagnostics. - QR package install and the installer/update Docker smokes (including Rocky Linux installer lanes and an update lane against a configurable `update_baseline_version` npm baseline) run as separate jobs so installer work does not wait behind the root image smokes. -The slow Bun global install image-provider smoke is separately gated by `run_bun_global_install_smoke`. It runs on the nightly schedule, defaults on for workflow calls from release checks, and manual `Install Smoke` dispatches can opt into it. Normal PR CI still runs the fast Bun launcher regression lane for Node-relevant changes. QR and installer Docker tests keep their own install-focused Dockerfiles. +The slow Bun global install and runtime smoke is separately gated by `run_bun_global_install_smoke`. It installs the candidate with trusted lifecycle scripts, then verifies representative CLI, local-agent, and Gateway paths under Bun 1.4 or newer. It runs on the nightly schedule, defaults on for workflow calls from release checks, and manual `Install Smoke` dispatches can opt into it. Normal PR CI still runs the fast Bun launcher regression lane for Node-relevant changes. QR and installer Docker tests keep their own install-focused Dockerfiles. ## Local Docker E2E diff --git a/docs/help/faq-first-run.md b/docs/help/faq-first-run.md index d8c3b28fa1d5..77a426f18fbe 100644 --- a/docs/help/faq-first-run.md +++ b/docs/help/faq-first-run.md @@ -138,8 +138,8 @@ and troubleshooting see the main [FAQ](/help/faq). - Node **22.22.3+**, **24.15+**, or **25.9+** is required (Node 26 recommended). `pnpm` is the repo package manager. - Bun can install dependencies and run package scripts, but it cannot run the OpenClaw CLI or Gateway because it lacks `node:sqlite`. + Node **22.22.3+**, **24.15+**, or **25.9+** is the primary and default runtime (Node 26 recommended). `pnpm` is the repo package manager. + Bun 1.4+ builds with WAL-reset-safe `node:sqlite` can run the CLI and Gateway as an explicit opt-in. @@ -675,9 +675,10 @@ and troubleshooting see the main [FAQ](/help/faq). - You can use Bun to install dependencies or run package scripts. The OpenClaw CLI and - Gateway require **Node** because the canonical state store uses `node:sqlite`; Bun does - not provide that API. + Yes. Node remains the primary, default, and recommended runtime, but Bun 1.4+ + with WAL-reset-safe `node:sqlite` can run the CLI and Gateway as an explicit + opt-in. Bun can also run package scripts; use `pnpm install` for dependency + installation. diff --git a/docs/help/testing.md b/docs/help/testing.md index 891087adca41..a48417214031 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -941,7 +941,7 @@ without mutating the host auth store: - Upgrade survivor smoke: `pnpm test:docker:upgrade-survivor` installs the packed OpenClaw tarball over a dirty old-user fixture with agents, channel config, plugin allowlists, stale plugin dependency state, and existing workspace/session files. It runs package update plus non-interactive doctor without live provider or channel keys, then starts a loopback Gateway and checks config/state preservation plus startup/status budgets. - Published upgrade survivor smoke: `pnpm test:docker:published-upgrade-survivor` installs `openclaw@latest` by default, seeds realistic existing-user files, configures that baseline with a baked command recipe, validates the resulting config, updates that published install to the candidate tarball, runs non-interactive doctor, writes `.artifacts/upgrade-survivor/summary.json`, then starts a loopback Gateway and checks configured intents, state preservation, startup, `/healthz`, `/readyz`, and RPC status budgets. Override one baseline with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC`, ask the aggregate scheduler to expand exact local baselines with `OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS` such as `openclaw@2026.5.2 openclaw@2026.4.23 openclaw@2026.4.15`, and expand issue-shaped fixtures with `OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS` such as `reported-issues`; the reported-issues set includes `configured-plugin-installs` for automatic external OpenClaw plugin install repair. Package Acceptance exposes those as `published_upgrade_survivor_baseline`, `published_upgrade_survivor_baselines`, and `published_upgrade_survivor_scenarios`, resolves meta baseline tokens such as `last-stable-4` or `all-since-2026.4.23`, and Full Release Validation expands the release-soak package gate to `last-stable-4 2026.4.23 2026.5.2 2026.4.15` plus `reported-issues`. - 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`. +- Bun global install and runtime smoke: `bash scripts/e2e/bun-global-install-smoke.sh` packs the current tree, installs it with `bun install -g --trust` in an isolated home, verifies OpenClaw's lifecycle scripts ran, and executes the installed package with Bun 1.4 or newer. It checks representative CLI state, bundled image providers, a mocked local agent turn, Gateway readiness and health, and a mocked agent turn through the Bun-hosted Gateway. 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. Override with `OPENCLAW_INSTALL_SMOKE_UPDATE_BASELINE=2026.4.22` locally, or with the Install Smoke workflow's `update_baseline_version` input on GitHub. 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. - Install Smoke CI skips the duplicate direct-npm global update with `OPENCLAW_INSTALL_SMOKE_SKIP_NPM_GLOBAL=1`; run the script locally without that env when direct `npm install -g` coverage is needed. - Agents delete shared workspace CLI smoke: `pnpm test:docker:agents-delete-shared-workspace` (script: `scripts/e2e/agents-delete-shared-workspace-docker.sh`) builds the root Dockerfile image by default, seeds two agents with one workspace in an isolated container home, runs `agents delete --json`, and verifies valid JSON plus retained workspace behavior. Reuse the install-smoke image with `OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_E2E_IMAGE=openclaw-dockerfile-smoke:local OPENCLAW_AGENTS_DELETE_SHARED_WORKSPACE_E2E_SKIP_BUILD=1`. diff --git a/docs/install/index.md b/docs/install/index.md index 268111712884..36416cf15e3c 100644 --- a/docs/install/index.md +++ b/docs/install/index.md @@ -138,8 +138,9 @@ If you already manage Node yourself: `--trust` allows OpenClaw's package lifecycle scripts for this install. Bun - can install the global package, but the resulting `openclaw` executable - requires a supported Node runtime because OpenClaw state uses `node:sqlite`. + 1.4 or newer can also run OpenClaw's CLI, local agent, and Gateway. Node + remains the primary runtime, so the plain `openclaw` executable keeps its + Node shebang. Use `bun run --bun openclaw ` to force the Bun runtime. diff --git a/docs/reference/full-release-validation.md b/docs/reference/full-release-validation.md index e1fd88b7c392..e2a441586cd2 100644 --- a/docs/reference/full-release-validation.md +++ b/docs/reference/full-release-validation.md @@ -256,7 +256,7 @@ artifact when package or Docker-facing stages need it. | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Release target | **Job:** `Resolve target ref`
**Backing workflow:** none
**Tests:** selected ref, optional expected Validation SHA, profile, concrete release-check groups, and focused live suite filter.
**Rerun:** select the concrete group for the failed surface. | | Package artifact | **Job:** `Prepare release package artifact`
**Backing workflow:** none
**Tests:** validates the umbrella's immutable package tuple, or packs one candidate tarball for a direct/focused Release Checks dispatch, then exposes it to downstream package-facing checks.
**Rerun:** the affected package, cross-OS, or live/E2E group. | -| Install smoke | **Job:** `Run install smoke`
**Backing workflow:** `Install Smoke`
**Tests:** full install path with root Dockerfile smoke image reuse, QR package install, root and gateway Docker smokes, installer Docker tests, and Bun global install image-provider smoke.
**Rerun:** `rerun_group=install-smoke`. | +| Install smoke | **Job:** `Run install smoke`
**Backing workflow:** `Install Smoke`
**Tests:** full install path with root Dockerfile smoke image reuse, QR package install, root and gateway Docker smokes, installer Docker tests, and Bun global install plus CLI/local-agent/Gateway runtime smoke.
**Rerun:** `rerun_group=install-smoke`. | | Cross-OS | **Job:** `cross_os_release_checks`
**Backing workflow:** `OpenClaw Cross-OS Release Checks (Reusable)`
**Tests:** fresh and upgrade lanes on Linux, Windows, and macOS for the selected provider and mode, using the candidate tarball plus a baseline package.
**Rerun:** `rerun_group=cross-os`. | | Repo and live E2E | **Job:** `Run repo/live E2E validation`
**Backing workflow:** `OpenClaw Live And E2E Checks (Reusable)`
**Tests:** repository E2E, live cache, OpenAI websocket streaming, native live provider and plugin shards, and Docker-backed live model/backend/gateway harnesses selected by `release_profile`.
**Runs:** `run_release_soak=true`, `release_profile=full`, or focused `rerun_group=live-e2e`.
**Rerun:** `rerun_group=live-e2e`, optionally with `live_suite_filter`. | | Docker release path | **Job:** `Run Docker release-path validation`
**Backing workflow:** `OpenClaw Live And E2E Checks (Reusable)`
**Tests:** release-path Docker chunks against the shared package artifact.
**Runs:** `run_release_soak=true`, `release_profile=full`, or focused `rerun_group=live-e2e`.
**Rerun:** `rerun_group=live-e2e`. | diff --git a/scripts/e2e/bun-global-install-smoke.sh b/scripts/e2e/bun-global-install-smoke.sh index 2d0dc52cc549..ba9d70c87280 100755 --- a/scripts/e2e/bun-global-install-smoke.sh +++ b/scripts/e2e/bun-global-install-smoke.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash -set -euo pipefail +set -Eeuo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" +source "$ROOT_DIR/scripts/lib/openclaw-e2e-instance.sh" read_positive_int_env() { local name="${1:?missing environment variable name}" @@ -27,8 +28,22 @@ DOCKER_COMMAND_TIMEOUT="${DOCKER_COMMAND_TIMEOUT:-${OPENCLAW_BUN_GLOBAL_SMOKE_DO AI_PACKAGE_TGZ="" SMOKE_DIR="" PACK_DIR="" +MOCK_PID="" +GATEWAY_PID="" +INSTALL_LOG="" +UNTRUSTED_LOG="" +CLI_STATUS_LOG="" +CLI_PLUGINS_LOG="" +MOCK_LOG="" +MOCK_REQUEST_LOG="" +LOCAL_AGENT_LOG="" +GATEWAY_LOG="" +GATEWAY_HEALTH_LOG="" +GATEWAY_AGENT_LOG="" cleanup() { + openclaw_e2e_stop_process "${GATEWAY_PID:-}" + openclaw_e2e_stop_process "${MOCK_PID:-}" if [ -n "${SMOKE_DIR:-}" ]; then rm -rf "$SMOKE_DIR" fi @@ -37,6 +52,22 @@ cleanup() { fi } +dump_failure_logs() { + local status="$1" + echo "bun global install smoke failed with exit code $status" >&2 + openclaw_e2e_dump_logs \ + "$INSTALL_LOG" \ + "$UNTRUSTED_LOG" \ + "$CLI_STATUS_LOG" \ + "$CLI_PLUGINS_LOG" \ + "$MOCK_LOG" \ + "$MOCK_REQUEST_LOG" \ + "$LOCAL_AGENT_LOG" \ + "$GATEWAY_LOG" \ + "$GATEWAY_HEALTH_LOG" \ + "$GATEWAY_AGENT_LOG" >&2 || true +} + prepare_ai_candidate() { local ai_manifest local ai_package_dir @@ -71,11 +102,64 @@ prepare_ai_candidate() { } trap cleanup EXIT +trap 'status=$?; dump_failure_logs "$status"; exit "$status"' ERR run_with_timeout() { local timeout_ms="$1" shift - node scripts/e2e/lib/bun-global-install/assertions.mjs run-with-timeout "$timeout_ms" "$@" + node "$ROOT_DIR/scripts/e2e/lib/bun-global-install/assertions.mjs" \ + run-with-timeout \ + "$timeout_ms" \ + "$@" +} + +reserve_runtime_ports() { + node --input-type=module <<'NODE' +import net from "node:net"; + +const servers = [net.createServer(), net.createServer()]; +await Promise.all( + servers.map( + (server) => + new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }), + ), +); +console.log(servers.map((server) => server.address().port).join(" ")); +await Promise.all(servers.map((server) => new Promise((resolve) => server.close(resolve)))); +NODE +} + +scrub_official_external_plugin_env() { + local env_name + while IFS= read -r env_name; do + unset "$env_name" + done < <( + node --input-type=module - \ + "$ROOT_DIR/scripts/lib/official-external-provider-catalog.json" <<'NODE' +import fs from "node:fs"; + +const catalog = JSON.parse(fs.readFileSync(process.argv[2], "utf8")); +const envNames = new Set(); +const visit = (value) => { + if (Array.isArray(value)) { + value.forEach(visit); + return; + } + if (!value || typeof value !== "object") { + return; + } + if (Array.isArray(value.envVars)) { + value.envVars.forEach((name) => envNames.add(name)); + } + Object.values(value).forEach(visit); +}; +visit(catalog); +console.log([...envNames].toSorted().join("\n")); +NODE + ) } resolve_package_tgz() { @@ -135,8 +219,16 @@ main() { prepare_ai_candidate local bun_path + local bun_version + local gateway_port + local mock_port + local openclaw_entry local openclaw_bin + local package_root + local success_marker bun_path="$(command -v "$BUN_BIN")" + bun_version="$("$bun_path" --version)" + node scripts/e2e/lib/bun-global-install/assertions.mjs assert-bun-version "$bun_version" SMOKE_DIR="$(mktemp -d "${TMPDIR:-/tmp}/openclaw-bun-global.XXXXXX")" export HOME="$SMOKE_DIR/home" @@ -144,8 +236,18 @@ main() { export XDG_CACHE_HOME="$SMOKE_DIR/cache" export OPENCLAW_NO_ONBOARD=1 export OPENCLAW_DISABLE_UPDATE_CHECK=1 + export OPENCLAW_STATE_DIR="$SMOKE_DIR/state" + export OPENCLAW_CONFIG_PATH="$OPENCLAW_STATE_DIR/openclaw.json" + scrub_official_external_plugin_env + export OPENAI_API_KEY="openclaw-bun-global-smoke-key" + export OPENCLAW_GATEWAY_TOKEN="openclaw-bun-global-smoke-token" export NO_COLOR=1 - mkdir -p "$HOME" "$BUN_INSTALL/bin" "$BUN_INSTALL/install/global" "$XDG_CACHE_HOME" + mkdir -p \ + "$HOME" \ + "$BUN_INSTALL/bin" \ + "$BUN_INSTALL/install/global" \ + "$XDG_CACHE_HOME" \ + "$OPENCLAW_STATE_DIR" export PATH="$BUN_INSTALL/bin:$(dirname "$(command -v node)"):$PATH" # Release publishes @openclaw/ai first. Pin the local tarball install to # exact candidate bytes instead of allowing public-registry resolution. @@ -161,11 +263,20 @@ fs.writeFileSync( ); NODE - echo "==> Bun version" - "$bun_path" --version + INSTALL_LOG="$SMOKE_DIR/install.log" + UNTRUSTED_LOG="$SMOKE_DIR/untrusted.log" + CLI_STATUS_LOG="$SMOKE_DIR/status.json" + CLI_PLUGINS_LOG="$SMOKE_DIR/plugins.json" + MOCK_LOG="$SMOKE_DIR/mock-openai.log" + MOCK_REQUEST_LOG="$SMOKE_DIR/mock-openai-requests.jsonl" + LOCAL_AGENT_LOG="$SMOKE_DIR/local-agent.log" + GATEWAY_LOG="$SMOKE_DIR/gateway.log" + GATEWAY_HEALTH_LOG="$SMOKE_DIR/gateway-health.json" + GATEWAY_AGENT_LOG="$SMOKE_DIR/gateway-agent.log" - echo "==> Bun global install packed OpenClaw" - "$bun_path" install -g "$PACKAGE_TGZ" --no-progress + echo "==> Install packed OpenClaw with trusted lifecycle scripts on Bun $bun_version" + run_with_timeout "$COMMAND_TIMEOUT_MS" \ + "$bun_path" install -g --trust "$PACKAGE_TGZ" --no-progress >"$INSTALL_LOG" 2>&1 openclaw_bin="$BUN_INSTALL/bin/openclaw" if [ ! -x "$openclaw_bin" ]; then @@ -175,6 +286,20 @@ NODE echo "Bun global install did not create an executable openclaw binary" >&2 exit 1 fi + openclaw_entry="$( + node -e 'const fs = require("node:fs"); process.stdout.write(fs.realpathSync(process.argv[1]));' \ + "$openclaw_bin" + )" + package_root="$(dirname "$openclaw_entry")" + export OPENCLAW_E2E_REDACTOR_MODULE="$package_root/dist/plugin-sdk/logging-core.js" + + echo "==> Verify OpenClaw lifecycle scripts were trusted and executed" + run_with_timeout "$COMMAND_TIMEOUT_MS" "$bun_path" pm -g untrusted >"$UNTRUSTED_LOG" 2>&1 + node scripts/e2e/lib/bun-global-install/assertions.mjs \ + assert-openclaw-trusted \ + "$package_root" \ + "$BUN_INSTALL/install/global/package.json" \ + "$UNTRUSTED_LOG" echo "==> OpenClaw version through Bun global install" local openclaw_version @@ -184,11 +309,80 @@ NODE 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" + run_bun_cli() { + run_with_timeout "$COMMAND_TIMEOUT_MS" "$bun_path" "$openclaw_entry" "$@" + } + + echo "==> Installed package entry under Bun" + run_bun_cli --version + run_bun_cli --help >/dev/null + pushd "$HOME" >/dev/null + run_with_timeout "$COMMAND_TIMEOUT_MS" "$bun_path" run --bun openclaw --version + popd >/dev/null + + echo "==> OpenClaw image providers under Bun" local providers_json - providers_json="$(run_with_timeout "$COMMAND_TIMEOUT_MS" "$openclaw_bin" infer image providers --json)" + providers_json="$(run_bun_cli infer image providers --json)" OPENCLAW_IMAGE_PROVIDERS_JSON="$providers_json" node scripts/e2e/lib/bun-global-install/assertions.mjs assert-image-providers + read -r gateway_port mock_port < <(reserve_runtime_ports) + success_marker="OPENCLAW_BUN_GLOBAL_RUNTIME_OK" + export SUCCESS_MARKER="$success_marker" MOCK_REQUEST_LOG + node scripts/e2e/lib/bun-global-install/assertions.mjs \ + configure-runtime \ + "$OPENCLAW_CONFIG_PATH" \ + "$mock_port" \ + "$gateway_port" + + echo "==> Representative CLI state under Bun" + run_bun_cli status --json --timeout 1 >"$CLI_STATUS_LOG" 2>&1 + run_bun_cli plugins list --json >"$CLI_PLUGINS_LOG" 2>&1 + + echo "==> Local mocked agent turn under Bun" + MOCK_PID="$(openclaw_e2e_start_mock_openai "$mock_port" "$MOCK_LOG")" + openclaw_e2e_wait_mock_openai "$mock_port" + : >"$MOCK_REQUEST_LOG" + run_bun_cli agent --local \ + --agent main \ + --session-id bun-global-local-agent \ + --message "Return marker $success_marker" \ + --thinking off \ + --json >"$LOCAL_AGENT_LOG" 2>&1 + node scripts/e2e/lib/bun-global-install/assertions.mjs \ + assert-agent-turn \ + "$success_marker" \ + "$LOCAL_AGENT_LOG" \ + "$MOCK_REQUEST_LOG" + + echo "==> Gateway health and mocked agent turn under Bun" + : >"$MOCK_REQUEST_LOG" + GATEWAY_PID="$( + openclaw_e2e_start_tracked_process \ + "$GATEWAY_LOG" \ + "$bun_path" \ + "$openclaw_entry" \ + gateway \ + --port "$gateway_port" \ + --bind loopback + )" + openclaw_e2e_wait_gateway_ready "$GATEWAY_PID" "$GATEWAY_LOG" 300 "$gateway_port" + run_bun_cli gateway health \ + --token "$OPENCLAW_GATEWAY_TOKEN" \ + --json >"$GATEWAY_HEALTH_LOG" 2>&1 + run_bun_cli agent \ + --agent main \ + --session-id bun-global-gateway-agent \ + --message "Return marker $success_marker" \ + --thinking off \ + --json >"$GATEWAY_AGENT_LOG" 2>&1 + node scripts/e2e/lib/bun-global-install/assertions.mjs \ + assert-agent-turn \ + "$success_marker" \ + "$GATEWAY_AGENT_LOG" \ + "$MOCK_REQUEST_LOG" + + echo "bun-global-install-smoke: Bun $bun_version package, CLI, local agent, and Gateway runtime OK" + if [ -n "${OPENCLAW_BUN_GLOBAL_SMOKE_PROOF_PATH:-}" ]; then node --input-type=module - \ "$OPENCLAW_BUN_GLOBAL_SMOKE_PROOF_PATH" \ diff --git a/scripts/e2e/lib/bun-global-install/assertions.mjs b/scripts/e2e/lib/bun-global-install/assertions.mjs index 9656106f5fd7..0d8f0ada61a9 100644 --- a/scripts/e2e/lib/bun-global-install/assertions.mjs +++ b/scripts/e2e/lib/bun-global-install/assertions.mjs @@ -1,13 +1,22 @@ // Assertions for Bun global install E2E validation. import { spawn } from "node:child_process"; import fs from "node:fs"; +import path from "node:path"; +import { + assertAgentReplyContainsMarker, + assertOpenAiRequestLogUsed, +} from "../agent-turn-output.mjs"; +import { + applyMockOpenAiModelConfig, + parseMockOpenAiPort, +} from "../fixtures/mock-openai-config.mjs"; const DEFAULT_TIMEOUT_KILL_GRACE_MS = 30_000; const PARENT_TERMINATION_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"]; const usage = () => { console.error( - "Usage: assertions.mjs [...]", + "Usage: assertions.mjs [...]", ); process.exit(2); }; @@ -181,6 +190,20 @@ if (mode === "run-with-timeout") { await runWithTimeout(timeout, command, commandArgs); } +if (mode === "assert-bun-version") { + const [version] = args; + const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/u.exec(version ?? ""); + if (!match) { + throw new Error(`invalid Bun version: ${version ?? ""}`); + } + const major = Number(match[1]); + const minor = Number(match[2]); + if (major < 1 || (major === 1 && minor < 4)) { + throw new Error(`Bun 1.4 or newer is required; found ${version}`); + } + process.exit(0); +} + if (mode === "assert-image-providers") { const raw = process.env.OPENCLAW_IMAGE_PROVIDERS_JSON ?? ""; let parsed; @@ -231,4 +254,55 @@ if (mode === "assert-release-versions") { process.exit(0); } +if (mode === "assert-openclaw-trusted") { + const [packageRoot, globalManifestPath, untrustedOutputPath] = args; + if (!packageRoot || !globalManifestPath || !untrustedOutputPath) { + usage(); + } + const globalManifest = JSON.parse(fs.readFileSync(globalManifestPath, "utf8")); + if (!globalManifest.trustedDependencies?.includes?.("openclaw")) { + throw new Error("Bun global manifest does not trust OpenClaw lifecycle scripts"); + } + const untrustedOutput = fs.readFileSync(untrustedOutputPath, "utf8"); + if (/(?:^|\s)(?:\.?[\\/])?node_modules[\\/]openclaw(?:\s|@|$)/imu.test(untrustedOutput)) { + throw new Error(`OpenClaw lifecycle scripts remain blocked by Bun:\n${untrustedOutput}`); + } + const installGuardPath = path.join(packageRoot, "dist", "openclaw-install-guard"); + if (fs.existsSync(installGuardPath)) { + throw new Error(`OpenClaw preinstall lifecycle did not remove ${installGuardPath}`); + } + process.exit(0); +} + +if (mode === "configure-runtime") { + const [configPath, mockPortValue, gatewayPortValue] = args; + if (!configPath || !mockPortValue || !gatewayPortValue) { + usage(); + } + const mockPort = parseMockOpenAiPort(mockPortValue); + const gatewayPort = parseMockOpenAiPort(gatewayPortValue, "Gateway port"); + const config = { + gateway: { + mode: "local", + bind: "loopback", + port: gatewayPort, + auth: { mode: "token" }, + }, + }; + applyMockOpenAiModelConfig(config, { mockPort }); + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`); + process.exit(0); +} + +if (mode === "assert-agent-turn") { + const [marker, outputPath, requestLogPath] = args; + if (!marker || !outputPath || !requestLogPath) { + usage(); + } + assertAgentReplyContainsMarker(marker, outputPath); + assertOpenAiRequestLogUsed(requestLogPath); + process.exit(0); +} + usage(); diff --git a/test/scripts/test-install-sh-docker.test.ts b/test/scripts/test-install-sh-docker.test.ts index 4bcbc09895d8..1fc0c4ede560 100644 --- a/test/scripts/test-install-sh-docker.test.ts +++ b/test/scripts/test-install-sh-docker.test.ts @@ -1826,7 +1826,7 @@ describe("install-sh smoke runner", () => { }); describe("bun global install smoke", () => { - it("packs the current tree and verifies image-provider discovery through Bun", () => { + it("packs the current tree and verifies the installed package runtime through Bun", () => { const script = readFileSync(BUN_GLOBAL_SMOKE_PATH, "utf8"); const assertions = readFileSync(BUN_GLOBAL_ASSERTIONS_PATH, "utf8"); const packageHelper = readFileSync(DOCKER_E2E_PACKAGE_HELPER_PATH, "utf8"); @@ -1841,11 +1841,15 @@ describe("bun global install smoke", () => { expect(script).toContain("--skip-build"); 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('"$bun_path" install -g --trust "$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(script).toContain("assert-openclaw-trusted"); + expect(script).toContain("agent --local"); + expect(script).toContain("gateway health"); + expect(script).toContain("openclaw_e2e_wait_gateway_ready"); expect(assertions).toContain("image providers output is missing bundled provider"); expect(script).toContain("OPENCLAW_BUN_GLOBAL_SMOKE_DIST_IMAGE"); expect(script).toContain('source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh"'); @@ -1944,6 +1948,77 @@ describe("bun global install smoke", () => { ); }); + it("requires Bun 1.4 or newer", () => { + const supported = spawnSync( + process.execPath, + [BUN_GLOBAL_ASSERTIONS_PATH, "assert-bun-version", "1.4.0"], + { encoding: "utf8" }, + ); + expect(supported.status, supported.stderr).toBe(0); + + const unsupported = spawnSync( + process.execPath, + [BUN_GLOBAL_ASSERTIONS_PATH, "assert-bun-version", "1.3.14"], + { encoding: "utf8" }, + ); + expect(unsupported.status).not.toBe(0); + expect(unsupported.stderr).toContain("Bun 1.4 or newer is required; found 1.3.14"); + }); + + it("requires Bun to trust and execute OpenClaw lifecycle scripts", () => { + const tempDir = tempDirs.make("openclaw-bun-trusted-lifecycle-"); + const packageRoot = join(tempDir, "node_modules", "openclaw"); + const globalManifestPath = join(tempDir, "package.json"); + const untrustedOutputPath = join(tempDir, "untrusted.txt"); + mkdirSync(join(packageRoot, "dist"), { recursive: true }); + writeFileSync(globalManifestPath, JSON.stringify({ trustedDependencies: ["openclaw"] })); + writeFileSync(untrustedOutputPath, "./node_modules/koffi [install]\n"); + + const trusted = spawnSync( + process.execPath, + [ + BUN_GLOBAL_ASSERTIONS_PATH, + "assert-openclaw-trusted", + packageRoot, + globalManifestPath, + untrustedOutputPath, + ], + { encoding: "utf8" }, + ); + expect(trusted.status, trusted.stderr).toBe(0); + + writeFileSync(untrustedOutputPath, "./node_modules/openclaw [preinstall, postinstall]\n"); + const blocked = spawnSync( + process.execPath, + [ + BUN_GLOBAL_ASSERTIONS_PATH, + "assert-openclaw-trusted", + packageRoot, + globalManifestPath, + untrustedOutputPath, + ], + { encoding: "utf8" }, + ); + expect(blocked.status).not.toBe(0); + expect(blocked.stderr).toContain("OpenClaw lifecycle scripts remain blocked by Bun"); + + writeFileSync(untrustedOutputPath, ""); + writeFileSync(join(packageRoot, "dist", "openclaw-install-guard"), "pending\n"); + const skipped = spawnSync( + process.execPath, + [ + BUN_GLOBAL_ASSERTIONS_PATH, + "assert-openclaw-trusted", + packageRoot, + globalManifestPath, + untrustedOutputPath, + ], + { encoding: "utf8" }, + ); + expect(skipped.status).not.toBe(0); + expect(skipped.stderr).toContain("OpenClaw preinstall lifecycle did not remove"); + }); + it.runIf(process.platform !== "win32")( "uses bundled AI bytes when a prebuilt tarball is provided", () => { @@ -1979,25 +2054,77 @@ describe("bun global install smoke", () => { `#!/usr/bin/env bash set -euo pipefail if [ "\${1:-}" = "--version" ]; then - echo "1.3.14" + echo "1.4.0" exit 0 fi +if [ "\${1:-}" = "pm" ] && [ "\${2:-}" = "-g" ] && [ "\${3:-}" = "untrusted" ]; then + echo './node_modules/koffi [install]' + exit 0 +fi +if [ "\${1:-}" = "run" ] && [ "\${2:-}" = "--bun" ]; then + echo "OpenClaw 2026.6.17" + exit 0 +fi +if [[ "\${1:-}" == */openclaw.mjs ]]; then + shift + if [ "\${1:-}" = "--version" ]; then + echo "OpenClaw 2026.6.17" + elif [ "\${1:-}" = "--help" ]; then + echo "Usage: openclaw" + elif [ "\${1:-}" = "infer" ]; then + printf '[{"id":"google"},{"id":"openai"},{"id":"xai"}]\n' + elif [ "\${1:-}" = "status" ] || { [ "\${1:-}" = "plugins" ] && [ "\${2:-}" = "list" ]; }; then + echo '{}' + elif [ "\${1:-}" = "agent" ]; then + printf '{"path":"/v1/responses"}\n' >>"$MOCK_REQUEST_LOG" + printf '{"payloads":[{"text":"%s"}]}\n' "$SUCCESS_MARKER" + elif [ "\${1:-}" = "gateway" ] && [ "\${2:-}" = "health" ]; then + echo '{"ok":true}' + elif [ "\${1:-}" = "gateway" ]; then + port="" + while [ "$#" -gt 0 ]; do + if [ "$1" = "--port" ]; then + port="$2" + break + fi + shift + done + exec node -e 'const http=require("node:http"); const port=Number(process.argv[1]); http.createServer((req,res)=>{res.writeHead(200,{"content-type":"text/plain"});res.end("ok")}).listen(port,"127.0.0.1",()=>console.log("[gateway] ready at http://127.0.0.1:"+port))' "$port" + else + echo "unsupported fake OpenClaw command: $*" >&2 + exit 1 + fi + exit 0 +fi +test "\${1:-}" = "install" +case " $* " in + *' --trust '*) ;; + *) echo 'missing --trust' >&2; exit 1 ;; +esac override="$(node -e 'const p=require(process.argv[1]);process.stdout.write(p.overrides["@openclaw/ai"])' "$BUN_INSTALL/install/global/package.json")" case "\${override#file:}" in *.tgz) ;; *) exit 1 ;; esac test -f "\${override#file:}" -mkdir -p "$BUN_INSTALL/bin" -cat >"$BUN_INSTALL/bin/openclaw" <<'OPENCLAW' -#!/usr/bin/env bash -if [ "\${1:-}" = "--version" ]; then - echo "OpenClaw 2026.6.17" -else - printf '[{"id":"google"},{"id":"openai"},{"id":"xai"}]\n' -fi +package_root="$BUN_INSTALL/install/global/node_modules/openclaw" +mkdir -p "$BUN_INSTALL/bin" "$package_root/dist" +cat >"$package_root/openclaw.mjs" <<'OPENCLAW' +#!/usr/bin/env node +const args = process.argv.slice(2); +if (args[0] === "--version") { + console.log("OpenClaw 2026.6.17"); +} else if (args[0] === "--help") { + console.log("Usage: openclaw"); +} else if (args[0] === "infer") { + console.log(JSON.stringify([{ id: "google" }, { id: "openai" }, { id: "xai" }])); +} else { + process.exit(1); +} OPENCLAW -chmod +x "$BUN_INSTALL/bin/openclaw" +chmod +x "$package_root/openclaw.mjs" +ln -s "$package_root/openclaw.mjs" "$BUN_INSTALL/bin/openclaw" +node -e 'const fs=require("node:fs");const p=process.argv[1];const value=JSON.parse(fs.readFileSync(p,"utf8"));value.trustedDependencies=["openclaw"];fs.writeFileSync(p,JSON.stringify(value))' "$BUN_INSTALL/install/global/package.json" `, ); chmodSync(bunPath, 0o755); @@ -2015,6 +2142,9 @@ chmod +x "$BUN_INSTALL/bin/openclaw" expect(result.status, result.stderr).toBe(0); expect(result.stdout).toContain("bun-global-install-smoke: image providers OK (3 providers)"); + expect(result.stdout).toContain( + "bun-global-install-smoke: Bun 1.4.0 package, CLI, local agent, and Gateway runtime OK", + ); }, );