diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ec076ec2ee..58b44593ebd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -107,6 +107,7 @@ Docs: https://docs.openclaw.ai - Docs: add a dedicated ds4 provider page with local DeepSeek V4 Flash config, on-demand startup, context sizing, and live verification steps. - Release validation: add a package-installed Docker user-journey lane that verifies onboarding, mocked model setup, external plugin install/uninstall, ClickClack outbound/inbound messaging, Gateway restart survival, and doctor. +- Release validation: add package-installed Docker lanes for real TTY onboarding, media and memory persistence, published-package upgrade journeys, and local marketplace plugin install/update/uninstall coverage. - Maintainers: add a Clawdtributor skill for Discrawl-backed contributor PR triage, live status checks, and compact review formatting. - Telegram: support Mini App `web_app` buttons in generic message presentation payloads, allowing `openclaw message send --presentation` to render Telegram Web App inline buttons for private chats. (#81356) Thanks @jzakirov. - Scripts: add `OPENCLAW_HEAVY_CHECK_LOCK_SCOPE=worktree` so high-capacity local worktrees can use independent heavy-check locks while shared locks remain the default. Fixes #80729. (#80734) Thanks @samzong. diff --git a/docs/help/testing.md b/docs/help/testing.md index ddc823886c7..2840bcac6b9 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -765,7 +765,7 @@ These Docker runners split into two buckets: - `Package Acceptance` is the GitHub-native package gate for "does this installable tarball work as a product?" It resolves one candidate package from `source=npm`, `source=ref`, `source=url`, or `source=artifact`, uploads it as `package-under-test`, then runs the reusable Docker E2E lanes against that exact tarball instead of repacking the selected ref. Profiles are ordered by breadth: `smoke`, `package`, `product`, and `full`. See [Testing updates and plugins](/help/testing-updates-plugins) for the package/update/plugin contract, published-upgrade survivor matrix, release defaults, and failure triage. - Build and release checks run `scripts/check-cli-bootstrap-imports.mjs` after tsdown. The guard walks the static built graph from `dist/entry.js` and `dist/cli/run-main.js` and fails if pre-dispatch startup imports package dependencies such as Commander, prompt UI, undici, or logging before command dispatch; it also keeps the bundled gateway run chunk under budget and rejects static imports of known cold gateway paths. Packaged CLI smoke also covers root help, onboard help, doctor help, status, config schema, and a model-list command. - Package Acceptance legacy compatibility is capped at `2026.4.25` (`2026.4.25-beta.*` included). Through that cutoff, the harness tolerates only shipped-package metadata gaps: omitted private QA inventory entries, missing `gateway install --wrapper`, missing patch files in the tarball-derived git fixture, missing persisted `update.channel`, legacy plugin install-record locations, missing marketplace install-record persistence, and config metadata migration during `plugins update`. For packages after `2026.4.25`, those paths are strict failures. -- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:npm-onboard-channel-agent`, `test:docker:release-user-journey`, `test:docker:skill-install`, `test:docker:update-channel-switch`, `test:docker:upgrade-survivor`, `test:docker:published-upgrade-survivor`, `test:docker:session-runtime-context`, `test:docker:agents-delete-shared-workspace`, `test:docker:gateway-network`, `test:docker:browser-cdp-snapshot`, `test:docker:mcp-channels`, `test:docker:pi-bundle-mcp-tools`, `test:docker:cron-mcp-cleanup`, `test:docker:plugins`, `test:docker:plugin-update`, `test:docker:plugin-lifecycle-matrix`, and `test:docker:config-reload` boot one or more real containers and verify higher-level integration paths. +- Container smoke runners: `test:docker:openwebui`, `test:docker:onboard`, `test:docker:npm-onboard-channel-agent`, `test:docker:release-user-journey`, `test:docker:release-typed-onboarding`, `test:docker:release-media-memory`, `test:docker:release-upgrade-user-journey`, `test:docker:release-plugin-marketplace`, `test:docker:skill-install`, `test:docker:update-channel-switch`, `test:docker:upgrade-survivor`, `test:docker:published-upgrade-survivor`, `test:docker:session-runtime-context`, `test:docker:agents-delete-shared-workspace`, `test:docker:gateway-network`, `test:docker:browser-cdp-snapshot`, `test:docker:mcp-channels`, `test:docker:pi-bundle-mcp-tools`, `test:docker:cron-mcp-cleanup`, `test:docker:plugins`, `test:docker:plugin-update`, `test:docker:plugin-lifecycle-matrix`, and `test:docker:config-reload` boot one or more real containers and verify higher-level integration paths. The live-model Docker runners also bind-mount only the needed CLI auth homes (or all supported ones when the run is not narrowed), then copy them into the container home before the run so external-CLI OAuth can refresh tokens without mutating the host auth store: @@ -780,6 +780,10 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or - Npm tarball onboarding/channel/agent smoke: `pnpm test:docker:npm-onboard-channel-agent` installs the packed OpenClaw tarball globally in Docker, configures OpenAI via env-ref onboarding plus Telegram by default, runs doctor, and runs one mocked OpenAI agent turn. Reuse a prebuilt tarball with `OPENCLAW_CURRENT_PACKAGE_TGZ=/path/to/openclaw-*.tgz`, skip the host rebuild with `OPENCLAW_NPM_ONBOARD_HOST_BUILD=0`, or switch channel with `OPENCLAW_NPM_ONBOARD_CHANNEL=discord` or `OPENCLAW_NPM_ONBOARD_CHANNEL=slack`. - Release user journey smoke: `pnpm test:docker:release-user-journey` installs the packed OpenClaw tarball globally in a clean Docker home, runs onboarding, configures a mocked OpenAI provider, runs an agent turn, installs/uninstalls external plugins, configures ClickClack against a local fixture, verifies outbound/inbound messaging, restarts Gateway, and runs doctor. +- Release typed onboarding smoke: `pnpm test:docker:release-typed-onboarding` installs the packed tarball, drives `openclaw onboard` through a real TTY, configures OpenAI as an env-ref provider, verifies no raw key persistence, and runs a mocked agent turn. +- Release media/memory smoke: `pnpm test:docker:release-media-memory` installs the packed tarball, verifies image understanding from a PNG attachment, OpenAI-compatible image generation output, memory search recall, and recall survival across Gateway restart. +- Release upgrade user journey smoke: `pnpm test:docker:release-upgrade-user-journey` installs `openclaw@latest` by default, configures provider/plugin/ClickClack state on the published package, upgrades to the candidate tarball, then reruns the core agent/plugin/channel journey. Override the baseline with `OPENCLAW_RELEASE_UPGRADE_BASELINE_SPEC=openclaw@`. +- Release plugin marketplace smoke: `pnpm test:docker:release-plugin-marketplace` installs from a local fixture marketplace, updates the installed plugin, uninstalls it, and verifies the plugin CLI disappears with install metadata pruned. - Skill install smoke: `pnpm test:docker:skill-install` installs the packed OpenClaw tarball globally in Docker, disables uploaded archive installs in config, resolves the current live ClawHub skill slug from search, installs it with `openclaw skills install`, and verifies the installed skill plus `.clawhub` origin/lock metadata. - Update channel switch smoke: `pnpm test:docker:update-channel-switch` installs the packed OpenClaw tarball globally in Docker, switches from package `stable` to git `dev`, verifies the persisted channel and plugin post-update work, then switches back to package `stable` and checks update status. - 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. diff --git a/package.json b/package.json index 74041345f99..f1d11a47372 100644 --- a/package.json +++ b/package.json @@ -1618,6 +1618,10 @@ "test:docker:npm-telegram-live": "bash scripts/e2e/npm-telegram-live-docker.sh", "test:docker:onboard": "bash scripts/e2e/onboard-docker.sh", "test:docker:release-user-journey": "bash scripts/e2e/release-user-journey-docker.sh", + "test:docker:release-typed-onboarding": "bash scripts/e2e/release-typed-onboarding-docker.sh", + "test:docker:release-media-memory": "bash scripts/e2e/release-media-memory-docker.sh", + "test:docker:release-upgrade-user-journey": "bash scripts/e2e/release-upgrade-user-journey-docker.sh", + "test:docker:release-plugin-marketplace": "bash scripts/e2e/release-plugin-marketplace-docker.sh", "test:docker:openai-chat-tools": "bash scripts/e2e/openai-chat-tools-docker.sh", "test:docker:openai-image-auth": "bash scripts/e2e/openai-image-auth-docker.sh", "test:docker:openai-web-search-minimal": "bash scripts/e2e/openai-web-search-minimal-docker.sh", diff --git a/scripts/e2e/lib/release-media-memory/scenario.sh b/scripts/e2e/lib/release-media-memory/scenario.sh new file mode 100755 index 00000000000..058f49b2aff --- /dev/null +++ b/scripts/e2e/lib/release-media-memory/scenario.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +set -euo pipefail +trap "" PIPE +export TERM=xterm-256color +export NO_COLOR=1 + +source scripts/lib/openclaw-e2e-instance.sh + +openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}" +openclaw_e2e_install_trash_shim + +export NPM_CONFIG_PREFIX="$HOME/.npm-global" +export PATH="$NPM_CONFIG_PREFIX/bin:$PATH" +export npm_config_loglevel=error +export npm_config_fund=false +export npm_config_audit=false +export OPENAI_API_KEY="sk-openclaw-release-media-memory" +export OPENCLAW_QA_ALLOW_LOCAL_IMAGE_PROVIDER=1 + +PORT="18789" +MOCK_PORT="44200" +SUCCESS_MARKER="OPENCLAW_E2E_OK_MEDIA_MEMORY" +MEMORY_MARKER="release-media-memory-saffron-$(date +%s)" +MOCK_REQUEST_LOG="/tmp/openclaw-release-media-memory-openai.jsonl" +export SUCCESS_MARKER MOCK_REQUEST_LOG + +mock_pid="" +gateway_pid="" +cleanup() { + openclaw_e2e_terminate_gateways "${gateway_pid:-}" + openclaw_e2e_stop_process "${mock_pid:-}" +} +trap cleanup EXIT + +dump_debug_logs() { + local status="$1" + echo "release media memory failed with exit code $status" >&2 + openclaw_e2e_dump_logs \ + /tmp/openclaw-release-media-memory-install.log \ + /tmp/openclaw-release-media-memory-onboard.log \ + /tmp/openclaw-release-media-memory-openai.log \ + "$MOCK_REQUEST_LOG" \ + /tmp/openclaw-release-media-memory-describe.json \ + /tmp/openclaw-release-media-memory-generate.json \ + /tmp/openclaw-release-media-memory-index.log \ + /tmp/openclaw-release-media-memory-search-before.json \ + /tmp/openclaw-release-media-memory-search-after.json \ + /tmp/openclaw-release-media-memory-gateway-1.log \ + /tmp/openclaw-release-media-memory-gateway-2.log +} +trap 'status=$?; dump_debug_logs "$status"; exit "$status"' ERR + +start_gateway() { + local log_path="$1" + gateway_pid="$(openclaw_e2e_start_gateway "$entry" "$PORT" "$log_path")" + openclaw_e2e_wait_gateway_ready "$gateway_pid" "$log_path" +} + +stop_gateway() { + openclaw_e2e_terminate_gateways "${gateway_pid:-}" + gateway_pid="" +} + +openclaw_e2e_install_package /tmp/openclaw-release-media-memory-install.log +command -v openclaw >/dev/null +package_root="$(openclaw_e2e_package_root)" +entry="$(openclaw_e2e_package_entrypoint "$package_root")" + +mock_pid="$(openclaw_e2e_start_mock_openai "$MOCK_PORT" /tmp/openclaw-release-media-memory-openai.log)" +openclaw_e2e_wait_mock_openai "$MOCK_PORT" + +openclaw onboard \ + --non-interactive \ + --accept-risk \ + --flow quickstart \ + --mode local \ + --auth-choice skip \ + --gateway-port "$PORT" \ + --gateway-bind loopback \ + --skip-daemon \ + --skip-ui \ + --skip-channels \ + --skip-skills \ + --skip-health >/tmp/openclaw-release-media-memory-onboard.log 2>&1 +node scripts/e2e/lib/release-scenarios/assertions.mjs configure-mock-openai "$MOCK_PORT" + +mkdir -p "$OPENCLAW_STATE_DIR/workspace/memory" /tmp/openclaw-release-media-memory +printf '%s' 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+yf7kAAAAASUVORK5CYII=' | base64 -d > /tmp/openclaw-release-media-memory/input.png + +openclaw infer image describe \ + --file /tmp/openclaw-release-media-memory/input.png \ + --model openai/gpt-5.5 \ + --prompt "Describe this image and return marker $SUCCESS_MARKER" \ + --json >/tmp/openclaw-release-media-memory-describe.json 2>&1 +node scripts/e2e/lib/release-scenarios/assertions.mjs assert-image-describe /tmp/openclaw-release-media-memory-describe.json "$MOCK_REQUEST_LOG" + +openclaw infer image generate \ + --model openai/gpt-image-1 \ + --prompt "Generate a tiny test image for $SUCCESS_MARKER" \ + --output /tmp/openclaw-release-media-memory/generated.png \ + --json >/tmp/openclaw-release-media-memory-generate.json 2>&1 +node scripts/e2e/lib/release-scenarios/assertions.mjs assert-image-generate /tmp/openclaw-release-media-memory-generate.json "$MOCK_REQUEST_LOG" + +cat >"$OPENCLAW_STATE_DIR/workspace/MEMORY.md" </tmp/openclaw-release-media-memory-index.log 2>&1 || true +openclaw memory search "$MEMORY_MARKER" --json >/tmp/openclaw-release-media-memory-search-before.json 2>&1 +node scripts/e2e/lib/release-scenarios/assertions.mjs assert-memory-search /tmp/openclaw-release-media-memory-search-before.json "$MEMORY_MARKER" + +start_gateway /tmp/openclaw-release-media-memory-gateway-1.log +stop_gateway +start_gateway /tmp/openclaw-release-media-memory-gateway-2.log +openclaw memory search "$MEMORY_MARKER" --json >/tmp/openclaw-release-media-memory-search-after.json 2>&1 +node scripts/e2e/lib/release-scenarios/assertions.mjs assert-memory-search /tmp/openclaw-release-media-memory-search-after.json "$MEMORY_MARKER" +stop_gateway + +echo "Release media memory scenario passed." diff --git a/scripts/e2e/lib/release-plugin-marketplace/scenario.sh b/scripts/e2e/lib/release-plugin-marketplace/scenario.sh new file mode 100755 index 00000000000..de398598341 --- /dev/null +++ b/scripts/e2e/lib/release-plugin-marketplace/scenario.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +set -euo pipefail +trap "" PIPE +export TERM=xterm-256color +export NO_COLOR=1 + +source scripts/lib/openclaw-e2e-instance.sh + +openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}" +openclaw_e2e_install_trash_shim + +export NPM_CONFIG_PREFIX="$HOME/.npm-global" +export PATH="$NPM_CONFIG_PREFIX/bin:$PATH" +export npm_config_loglevel=error +export npm_config_fund=false +export npm_config_audit=false + +dump_debug_logs() { + local status="$1" + echo "release plugin marketplace failed with exit code $status" >&2 + openclaw_e2e_dump_logs \ + /tmp/openclaw-release-plugin-marketplace-install.log \ + /tmp/openclaw-release-plugin-marketplace-onboard.log \ + /tmp/openclaw-release-plugin-marketplace-list.json \ + /tmp/openclaw-release-plugin-marketplace-install-plugin.log \ + /tmp/openclaw-release-plugin-marketplace-cli-v1.log \ + /tmp/openclaw-release-plugin-marketplace-update-dry-run.log \ + /tmp/openclaw-release-plugin-marketplace-update.log \ + /tmp/openclaw-release-plugin-marketplace-cli-v2.log \ + /tmp/openclaw-release-plugin-marketplace-uninstall.log \ + /tmp/openclaw-release-plugin-marketplace-cli-after-uninstall.log \ + "$HOME/.openclaw/plugins/installs.json" +} +trap 'status=$?; dump_debug_logs "$status"; exit "$status"' ERR + +openclaw_e2e_install_package /tmp/openclaw-release-plugin-marketplace-install.log +command -v openclaw >/dev/null + +openclaw onboard \ + --non-interactive \ + --accept-risk \ + --flow quickstart \ + --mode local \ + --auth-choice skip \ + --skip-daemon \ + --skip-ui \ + --skip-channels \ + --skip-skills \ + --skip-health >/tmp/openclaw-release-plugin-marketplace-onboard.log 2>&1 + +marketplace_root="$HOME/.claude/plugins/marketplaces/release-fixture-marketplace" +mkdir -p "$HOME/.claude/plugins" "$marketplace_root/.claude-plugin" +node scripts/e2e/lib/release-scenarios/write-cli-plugin.mjs \ + "$marketplace_root/plugins/release-marketplace-plugin" \ + release-marketplace-plugin \ + 0.0.1 \ + release.marketplace.v1 \ + "Release Marketplace Plugin" \ + release-market \ + "release-marketplace-plugin:v1" +node scripts/e2e/lib/release-scenarios/write-cli-plugin.mjs \ + "$marketplace_root/plugins/release-marketplace-other" \ + release-marketplace-other \ + 0.0.1 \ + release.marketplace.other \ + "Release Marketplace Other" \ + release-market-other \ + "release-marketplace-other:v1" +node scripts/e2e/lib/release-scenarios/write-marketplace.mjs \ + "$marketplace_root" \ + release-fixtures \ + release-marketplace-plugin \ + release-marketplace-other + +openclaw plugins marketplace list release-fixtures --json >/tmp/openclaw-release-plugin-marketplace-list.json +node scripts/e2e/lib/release-scenarios/assertions.mjs assert-file-contains /tmp/openclaw-release-plugin-marketplace-list.json release-marketplace-plugin + +openclaw plugins install release-marketplace-plugin@release-fixtures >/tmp/openclaw-release-plugin-marketplace-install-plugin.log 2>&1 +openclaw release-market ping >/tmp/openclaw-release-plugin-marketplace-cli-v1.log 2>&1 +node scripts/e2e/lib/release-scenarios/assertions.mjs assert-file-contains /tmp/openclaw-release-plugin-marketplace-cli-v1.log "release-marketplace-plugin:v1" + +node scripts/e2e/lib/release-scenarios/write-cli-plugin.mjs \ + "$marketplace_root/plugins/release-marketplace-plugin" \ + release-marketplace-plugin \ + 0.0.2 \ + release.marketplace.v2 \ + "Release Marketplace Plugin" \ + release-market \ + "release-marketplace-plugin:v2" +openclaw plugins update release-marketplace-plugin --dry-run >/tmp/openclaw-release-plugin-marketplace-update-dry-run.log 2>&1 +openclaw plugins update release-marketplace-plugin >/tmp/openclaw-release-plugin-marketplace-update.log 2>&1 +openclaw release-market ping >/tmp/openclaw-release-plugin-marketplace-cli-v2.log 2>&1 +node scripts/e2e/lib/release-scenarios/assertions.mjs assert-file-contains /tmp/openclaw-release-plugin-marketplace-cli-v2.log "release-marketplace-plugin:v2" + +openclaw plugins uninstall release-marketplace-plugin --force >/tmp/openclaw-release-plugin-marketplace-uninstall.log 2>&1 +if openclaw release-market ping >/tmp/openclaw-release-plugin-marketplace-cli-after-uninstall.log 2>&1; then + echo "release-market CLI should be gone after uninstall" >&2 + exit 1 +fi +node scripts/e2e/lib/release-scenarios/assertions.mjs assert-plugin-uninstalled release-marketplace-plugin release-market + +echo "Release plugin marketplace scenario passed." diff --git a/scripts/e2e/lib/release-scenarios/assertions.mjs b/scripts/e2e/lib/release-scenarios/assertions.mjs new file mode 100644 index 00000000000..2c85154d326 --- /dev/null +++ b/scripts/e2e/lib/release-scenarios/assertions.mjs @@ -0,0 +1,192 @@ +import fs from "node:fs"; +import path from "node:path"; + +const command = process.argv[2]; + +function assert(condition, message) { + if (!condition) { + throw new Error(message); + } +} + +function readJson(file) { + return JSON.parse(fs.readFileSync(file, "utf8")); +} + +function configPath() { + return ( + process.env.OPENCLAW_CONFIG_PATH ?? + path.join(process.env.HOME ?? "", ".openclaw", "openclaw.json") + ); +} + +function writeConfig(cfg) { + fs.writeFileSync(configPath(), `${JSON.stringify(cfg, null, 2)}\n`); +} + +function authProfilesPath() { + return path.join( + process.env.HOME ?? "", + ".openclaw", + "agents", + "main", + "agent", + "auth-profiles.json", + ); +} + +function readStateText() { + const paths = [configPath(), authProfilesPath()].filter((file) => fs.existsSync(file)); + return paths.map((file) => fs.readFileSync(file, "utf8")).join("\n"); +} + +function configureMockOpenAi() { + const mockPort = Number(process.argv[3]); + const cfg = readJson(configPath()); + const cost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }; + cfg.models = { + ...cfg.models, + mode: "merge", + providers: { + ...cfg.models?.providers, + openai: { + ...cfg.models?.providers?.openai, + baseUrl: `http://127.0.0.1:${mockPort}/v1`, + apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" }, + api: "openai-responses", + request: { ...cfg.models?.providers?.openai?.request, allowPrivateNetwork: true }, + models: [ + { + id: "gpt-5.5", + name: "gpt-5.5", + api: "openai-responses", + reasoning: false, + input: ["text", "image"], + cost, + contextWindow: 128000, + contextTokens: 96000, + maxTokens: 4096, + }, + ], + }, + }, + }; + cfg.agents = { + ...cfg.agents, + defaults: { + ...cfg.agents?.defaults, + model: { primary: "openai/gpt-5.5" }, + imageModel: { primary: "openai/gpt-5.5", timeoutMs: 30_000 }, + imageGenerationModel: { primary: "openai/gpt-image-1", timeoutMs: 30_000 }, + models: { + ...cfg.agents?.defaults?.models, + "openai/gpt-5.5": { params: { transport: "sse", openaiWsWarmup: false } }, + }, + }, + }; + cfg.plugins = { + ...cfg.plugins, + enabled: true, + }; + writeConfig(cfg); +} + +function assertOpenAiEnvRef() { + const rawKey = process.argv[3]; + const state = readStateText(); + assert(state.includes("OPENAI_API_KEY"), "OpenAI env ref was not persisted"); + assert(!state.includes(rawKey), "raw OpenAI key was persisted"); + assert(fs.existsSync(configPath()), "openclaw.json missing"); +} + +function assertAgentTurn() { + const marker = process.argv[3]; + const outputPath = process.argv[4]; + const requestLogPath = process.argv[5]; + const output = fs.readFileSync(outputPath, "utf8"); + assert(output.includes(marker), `agent output did not contain ${marker}. Output: ${output}`); + const requestLog = fs.existsSync(requestLogPath) ? fs.readFileSync(requestLogPath, "utf8") : ""; + assert(/\/v1\/(responses|chat\/completions)/u.test(requestLog), "mock OpenAI was not used"); +} + +function assertFileContains() { + const file = process.argv[3]; + const needle = process.argv[4]; + const raw = fs.readFileSync(file, "utf8"); + assert(raw.includes(needle), `${file} did not contain ${needle}. Output: ${raw}`); +} + +function assertImageDescribe() { + const outputPath = process.argv[3]; + const requestLogPath = process.argv[4]; + const payload = readJson(outputPath); + assert(payload.ok === true, `image describe failed: ${JSON.stringify(payload)}`); + assert(payload.capability === "image.describe", "wrong image describe capability"); + const output = payload.outputs?.[0]; + assert(output?.text?.includes("OPENCLAW_E2E_OK"), "image description marker missing"); + assert(output.provider === "openai", `unexpected image provider: ${output?.provider}`); + const requestLog = fs.existsSync(requestLogPath) ? fs.readFileSync(requestLogPath, "utf8") : ""; + assert(requestLog.includes("/v1/responses"), "image describe did not hit Responses API"); +} + +function assertImageGenerate() { + const outputPath = process.argv[3]; + const requestLogPath = process.argv[4]; + const payload = readJson(outputPath); + assert(payload.ok === true, `image generation failed: ${JSON.stringify(payload)}`); + assert(payload.capability === "image.generate", "wrong image generation capability"); + const output = payload.outputs?.[0]; + assert(output?.path && fs.existsSync(output.path), `generated image missing: ${output?.path}`); + assert(output.mimeType === "image/png", `unexpected generated mime type: ${output.mimeType}`); + assert(payload.provider === "openai", `unexpected generation provider: ${payload.provider}`); + const requestLog = fs.existsSync(requestLogPath) ? fs.readFileSync(requestLogPath, "utf8") : ""; + assert(requestLog.includes("/v1/images/generations"), "image generation endpoint was not used"); +} + +function assertMemorySearch() { + const outputPath = process.argv[3]; + const needle = process.argv[4]; + const payload = readJson(outputPath); + const haystack = JSON.stringify(payload); + assert(haystack.includes(needle), `memory search missed ${needle}: ${haystack}`); +} + +function assertPluginUninstalled() { + const pluginId = process.argv[3]; + const cliRoot = process.argv[4]; + const cfg = readJson(configPath()); + const recordsPath = path.join(process.env.HOME ?? "", ".openclaw", "plugins", "installs.json"); + const records = fs.existsSync(recordsPath) ? readJson(recordsPath) : {}; + const installRecords = records.installRecords ?? records.records ?? {}; + assert(!installRecords[pluginId], `install record still present for ${pluginId}`); + assert(!cfg.plugins?.entries?.[pluginId], `plugin config entry still present for ${pluginId}`); + const managedRoot = path.join( + process.env.HOME ?? "", + ".openclaw", + "plugins", + "installed", + pluginId, + ); + assert(!fs.existsSync(managedRoot), `managed plugin directory still present: ${managedRoot}`); + if (cliRoot) { + const list = JSON.stringify(records); + assert(!list.includes(cliRoot), `install records still mention CLI root ${cliRoot}`); + } +} + +const commands = { + "configure-mock-openai": configureMockOpenAi, + "assert-openai-env-ref": assertOpenAiEnvRef, + "assert-agent-turn": assertAgentTurn, + "assert-file-contains": assertFileContains, + "assert-image-describe": assertImageDescribe, + "assert-image-generate": assertImageGenerate, + "assert-memory-search": assertMemorySearch, + "assert-plugin-uninstalled": assertPluginUninstalled, +}; + +const fn = commands[command]; +if (!fn) { + throw new Error(`unknown release scenario assertion command: ${command ?? ""}`); +} +await fn(); diff --git a/scripts/e2e/lib/release-scenarios/write-cli-plugin.mjs b/scripts/e2e/lib/release-scenarios/write-cli-plugin.mjs new file mode 100644 index 00000000000..65096685447 --- /dev/null +++ b/scripts/e2e/lib/release-scenarios/write-cli-plugin.mjs @@ -0,0 +1,32 @@ +import fs from "node:fs"; +import path from "node:path"; + +const [dir, id, version, method, name, cliRoot, cliOutput] = process.argv.slice(2); + +if (!dir || !id || !version || !method || !name || !cliRoot || !cliOutput) { + throw new Error( + "usage: write-cli-plugin.mjs ", + ); +} + +fs.mkdirSync(dir, { recursive: true }); +fs.writeFileSync( + path.join(dir, "package.json"), + `${JSON.stringify( + { + name: `@openclaw/${id}`, + version, + openclaw: { extensions: ["./index.js"] }, + }, + null, + 2, + )}\n`, +); +fs.writeFileSync( + path.join(dir, "index.js"), + `module.exports = { id: ${JSON.stringify(id)}, name: ${JSON.stringify(name)}, register(api) { api.registerGatewayMethod(${JSON.stringify(method)}, async () => ({ ok: true, version: ${JSON.stringify(version)} })); api.registerCli(({ program }) => { const root = program.command(${JSON.stringify(cliRoot)}).description(${JSON.stringify(`${name} fixture command`)}); root.command("ping").description("Print fixture ping output").action(() => { console.log(${JSON.stringify(cliOutput)}); }); }, { descriptors: [{ name: ${JSON.stringify(cliRoot)}, description: ${JSON.stringify(`${name} fixture command`)}, hasSubcommands: true }] }); }, };\n`, +); +fs.writeFileSync( + path.join(dir, "openclaw.plugin.json"), + `${JSON.stringify({ id, configSchema: { type: "object", properties: {} } }, null, 2)}\n`, +); diff --git a/scripts/e2e/lib/release-scenarios/write-marketplace.mjs b/scripts/e2e/lib/release-scenarios/write-marketplace.mjs new file mode 100644 index 00000000000..bcbae469183 --- /dev/null +++ b/scripts/e2e/lib/release-scenarios/write-marketplace.mjs @@ -0,0 +1,41 @@ +import fs from "node:fs"; +import path from "node:path"; + +const [root, alias, ...plugins] = process.argv.slice(2); + +if (!root || !alias || plugins.length === 0) { + throw new Error("usage: write-marketplace.mjs ..."); +} + +fs.mkdirSync(path.join(root, ".claude-plugin"), { recursive: true }); +fs.mkdirSync(path.join(process.env.HOME, ".claude", "plugins"), { recursive: true }); +fs.writeFileSync( + path.join(root, ".claude-plugin", "marketplace.json"), + `${JSON.stringify( + { + name: "Release Fixture Marketplace", + version: "1.0.0", + plugins: plugins.map((pluginId) => ({ + name: pluginId, + version: "0.0.1", + description: `${pluginId} release fixture`, + source: { type: "path", path: `./plugins/${pluginId}` }, + })), + }, + null, + 2, + )}\n`, +); +fs.writeFileSync( + path.join(process.env.HOME, ".claude", "plugins", "known_marketplaces.json"), + `${JSON.stringify( + { + [alias]: { + installLocation: root, + source: { type: "github", repo: "openclaw/release-fixture-marketplace" }, + }, + }, + null, + 2, + )}\n`, +); diff --git a/scripts/e2e/lib/release-typed-onboarding/scenario.sh b/scripts/e2e/lib/release-typed-onboarding/scenario.sh new file mode 100755 index 00000000000..e7bae5a8472 --- /dev/null +++ b/scripts/e2e/lib/release-typed-onboarding/scenario.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +set -euo pipefail +trap "" PIPE +export TERM=xterm-256color +export NO_COLOR=1 + +source scripts/lib/openclaw-e2e-instance.sh + +openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}" +openclaw_e2e_install_trash_shim + +export NPM_CONFIG_PREFIX="$HOME/.npm-global" +export PATH="$NPM_CONFIG_PREFIX/bin:$PATH" +export npm_config_loglevel=error +export npm_config_fund=false +export npm_config_audit=false +export OPENAI_API_KEY="sk-openclaw-release-typed-onboarding" + +PORT="18789" +MOCK_PORT="44190" +SUCCESS_MARKER="OPENCLAW_E2E_OK_TYPED_ONBOARDING" +MOCK_REQUEST_LOG="/tmp/openclaw-release-typed-onboarding-openai.jsonl" +export SUCCESS_MARKER MOCK_REQUEST_LOG + +mock_pid="" +cleanup() { + openclaw_e2e_stop_process "${mock_pid:-}" +} +trap cleanup EXIT + +dump_debug_logs() { + local status="$1" + echo "release typed onboarding failed with exit code $status" >&2 + openclaw_e2e_dump_logs \ + /tmp/openclaw-release-typed-onboarding-install.log \ + /tmp/openclaw-release-typed-onboarding.log \ + /tmp/openclaw-release-typed-onboarding-openai.log \ + "$MOCK_REQUEST_LOG" \ + /tmp/openclaw-release-typed-onboarding-agent.log \ + "$OPENCLAW_CONFIG_PATH" \ + "$HOME/.openclaw/agents/main/agent/auth-profiles.json" +} +trap 'status=$?; dump_debug_logs "$status"; exit "$status"' ERR + +send() { + local payload="$1" + local delay="${2:-0.4}" + sleep "$delay" + printf "%b" "$payload" >&3 2>/dev/null || true +} + +wait_for_log() { + local needle="$1" + local timeout_s="${2:-60}" + local start_s + start_s="$(date +%s)" + while true; do + if [ -f /tmp/openclaw-release-typed-onboarding.log ]; then + if grep -a -F -q "$needle" /tmp/openclaw-release-typed-onboarding.log; then + return 0 + fi + if node scripts/e2e/lib/onboard/log-contains.mjs /tmp/openclaw-release-typed-onboarding.log "$needle"; then + return 0 + fi + fi + if [ $(($(date +%s) - start_s)) -ge "$timeout_s" ]; then + echo "Timeout waiting for log: $needle" >&2 + tail -n 120 /tmp/openclaw-release-typed-onboarding.log 2>/dev/null || true + return 1 + fi + sleep 0.2 + done +} + +openclaw_e2e_install_package /tmp/openclaw-release-typed-onboarding-install.log +command -v openclaw >/dev/null +package_root="$(openclaw_e2e_package_root)" +entry="$(openclaw_e2e_package_entrypoint "$package_root")" + +mock_pid="$(openclaw_e2e_start_mock_openai "$MOCK_PORT" /tmp/openclaw-release-typed-onboarding-openai.log)" +openclaw_e2e_wait_mock_openai "$MOCK_PORT" + +input_fifo="$(mktemp -u "/tmp/openclaw-release-typed-onboarding.XXXXXX")" +mkfifo "$input_fifo" +script -q -f -c "node \"$entry\" onboard --flow quickstart --mode local --auth-choice skip --gateway-port \"$PORT\" --gateway-bind loopback --skip-daemon --skip-ui --skip-channels --skip-skills --skip-health" /tmp/openclaw-release-typed-onboarding.log <"$input_fifo" >/dev/null 2>&1 & +wizard_pid="$!" +exec 3>"$input_fifo" + +wait_for_log "Continue?" 60 +send $'y\r' 0.4 +wait_for_log "to search" 60 +send $'ollama\r' 0.4 +wait_for_log "Enable hooks?" 60 +send $' \r' 0.4 +send $'\r' 0.4 + +wait "$wizard_pid" +exec 3>&- +rm -f "$input_fifo" + +openclaw onboard \ + --non-interactive \ + --accept-risk \ + --flow quickstart \ + --mode local \ + --auth-choice openai-api-key \ + --secret-input-mode ref \ + --gateway-port "$PORT" \ + --gateway-bind loopback \ + --skip-daemon \ + --skip-ui \ + --skip-channels \ + --skip-skills \ + --skip-health >>/tmp/openclaw-release-typed-onboarding.log 2>&1 + +node scripts/e2e/lib/release-scenarios/assertions.mjs assert-openai-env-ref "$OPENAI_API_KEY" +node scripts/e2e/lib/release-scenarios/assertions.mjs configure-mock-openai "$MOCK_PORT" + +openclaw agent --local \ + --agent main \ + --session-id release-typed-onboarding-agent \ + --message "Return marker $SUCCESS_MARKER" \ + --thinking off \ + --json >/tmp/openclaw-release-typed-onboarding-agent.log 2>&1 +node scripts/e2e/lib/release-scenarios/assertions.mjs assert-agent-turn "$SUCCESS_MARKER" /tmp/openclaw-release-typed-onboarding-agent.log "$MOCK_REQUEST_LOG" + +echo "Release typed onboarding scenario passed." diff --git a/scripts/e2e/lib/release-upgrade-user-journey/scenario.sh b/scripts/e2e/lib/release-upgrade-user-journey/scenario.sh new file mode 100755 index 00000000000..1b8462c086d --- /dev/null +++ b/scripts/e2e/lib/release-upgrade-user-journey/scenario.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +set -euo pipefail +trap "" PIPE +export TERM=xterm-256color +export NO_COLOR=1 + +source scripts/lib/openclaw-e2e-instance.sh + +openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}" +openclaw_e2e_install_trash_shim + +export NPM_CONFIG_PREFIX="$HOME/.npm-global" +export PATH="$NPM_CONFIG_PREFIX/bin:$PATH" +export npm_config_loglevel=error +export npm_config_fund=false +export npm_config_audit=false +export OPENAI_API_KEY="sk-openclaw-release-upgrade-user-journey" +export CLICKCLACK_BOT_TOKEN="clickclack-release-upgrade-token" + +PORT="18789" +MOCK_PORT="44210" +CLICKCLACK_PORT="44211" +SUCCESS_MARKER="OPENCLAW_E2E_OK_RELEASE_UPGRADE" +MOCK_REQUEST_LOG="/tmp/openclaw-release-upgrade-user-journey-openai.jsonl" +CLICKCLACK_STATE="/tmp/openclaw-release-upgrade-user-journey-clickclack.json" +BASELINE_SPEC="${OPENCLAW_RELEASE_UPGRADE_BASELINE_SPEC:-openclaw@latest}" +export SUCCESS_MARKER MOCK_REQUEST_LOG CLICKCLACK_STATE + +mock_pid="" +clickclack_pid="" +gateway_pid="" +cleanup() { + openclaw_e2e_terminate_gateways "${gateway_pid:-}" + openclaw_e2e_stop_process "${clickclack_pid:-}" + openclaw_e2e_stop_process "${mock_pid:-}" +} +trap cleanup EXIT + +dump_debug_logs() { + local status="$1" + echo "release upgrade user journey failed with exit code $status" >&2 + openclaw_e2e_dump_logs \ + /tmp/openclaw-release-upgrade-baseline-install.log \ + /tmp/openclaw-release-upgrade-candidate-install.log \ + /tmp/openclaw-release-upgrade-onboard.log \ + /tmp/openclaw-release-upgrade-openai.log \ + "$MOCK_REQUEST_LOG" \ + /tmp/openclaw-release-upgrade-plugin-install.log \ + /tmp/openclaw-release-upgrade-plugin-cli-before.log \ + /tmp/openclaw-release-upgrade-plugin-cli-after.log \ + /tmp/openclaw-release-upgrade-agent.log \ + /tmp/openclaw-release-upgrade-status.json \ + /tmp/openclaw-release-upgrade-clickclack-outbound.json \ + /tmp/openclaw-release-upgrade-clickclack-server.log \ + /tmp/openclaw-release-upgrade-gateway.log \ + "$CLICKCLACK_STATE" +} +trap 'status=$?; dump_debug_logs "$status"; exit "$status"' ERR + +start_gateway() { + local log_path="$1" + gateway_pid="$(openclaw_e2e_start_gateway "$entry" "$PORT" "$log_path")" + openclaw_e2e_wait_gateway_ready "$gateway_pid" "$log_path" +} + +echo "Installing published baseline $BASELINE_SPEC..." +npm install -g "$BASELINE_SPEC" --no-fund --no-audit >/tmp/openclaw-release-upgrade-baseline-install.log 2>&1 +command -v openclaw >/dev/null +baseline_root="$(openclaw_e2e_package_root)" +baseline_entry="$(openclaw_e2e_package_entrypoint "$baseline_root")" + +mock_pid="$(openclaw_e2e_start_mock_openai "$MOCK_PORT" /tmp/openclaw-release-upgrade-openai.log)" +openclaw_e2e_wait_mock_openai "$MOCK_PORT" + +CLICKCLACK_FIXTURE_PORT="$CLICKCLACK_PORT" \ +CLICKCLACK_FIXTURE_TOKEN="$CLICKCLACK_BOT_TOKEN" \ +CLICKCLACK_FIXTURE_STATE="$CLICKCLACK_STATE" \ + node scripts/e2e/lib/release-user-journey/clickclack-fixture.mjs >/tmp/openclaw-release-upgrade-clickclack-server.log 2>&1 & +clickclack_pid="$!" +for _ in $(seq 1 100); do + if openclaw_e2e_probe_http_status "http://127.0.0.1:$CLICKCLACK_PORT/health" 200 >/dev/null 2>&1; then + break + fi + sleep 0.1 +done +openclaw_e2e_probe_http_status "http://127.0.0.1:$CLICKCLACK_PORT/health" 200 + +node "$baseline_entry" onboard \ + --non-interactive \ + --accept-risk \ + --flow quickstart \ + --mode local \ + --auth-choice skip \ + --gateway-port "$PORT" \ + --gateway-bind loopback \ + --skip-daemon \ + --skip-ui \ + --skip-channels \ + --skip-skills \ + --skip-health >/tmp/openclaw-release-upgrade-onboard.log 2>&1 +node scripts/e2e/lib/release-scenarios/assertions.mjs configure-mock-openai "$MOCK_PORT" + +plugin_dir="$(mktemp -d "/tmp/openclaw-release-upgrade-plugin.XXXXXX")" +node scripts/e2e/lib/release-scenarios/write-cli-plugin.mjs \ + "$plugin_dir" \ + release-upgrade-plugin \ + 0.0.1 \ + release.upgrade.plugin \ + "Release Upgrade Plugin" \ + release-upgrade \ + "release-upgrade-plugin:pong" +openclaw plugins install "$plugin_dir" >/tmp/openclaw-release-upgrade-plugin-install.log 2>&1 +openclaw release-upgrade ping >/tmp/openclaw-release-upgrade-plugin-cli-before.log 2>&1 +node scripts/e2e/lib/release-scenarios/assertions.mjs assert-file-contains /tmp/openclaw-release-upgrade-plugin-cli-before.log "release-upgrade-plugin:pong" +node scripts/e2e/lib/release-user-journey/assertions.mjs configure-clickclack "http://127.0.0.1:$CLICKCLACK_PORT" + +openclaw_e2e_install_package /tmp/openclaw-release-upgrade-candidate-install.log "candidate OpenClaw package" +package_root="$(openclaw_e2e_package_root)" +entry="$(openclaw_e2e_package_entrypoint "$package_root")" + +openclaw agent --local \ + --agent main \ + --session-id release-upgrade-user-journey-agent \ + --message "Return marker $SUCCESS_MARKER" \ + --thinking off \ + --json >/tmp/openclaw-release-upgrade-agent.log 2>&1 +node scripts/e2e/lib/release-scenarios/assertions.mjs assert-agent-turn "$SUCCESS_MARKER" /tmp/openclaw-release-upgrade-agent.log "$MOCK_REQUEST_LOG" + +openclaw release-upgrade ping >/tmp/openclaw-release-upgrade-plugin-cli-after.log 2>&1 +node scripts/e2e/lib/release-scenarios/assertions.mjs assert-file-contains /tmp/openclaw-release-upgrade-plugin-cli-after.log "release-upgrade-plugin:pong" + +openclaw channels status --json >/tmp/openclaw-release-upgrade-status.json 2>/tmp/openclaw-release-upgrade-status.err +node scripts/e2e/lib/release-user-journey/assertions.mjs assert-channel-status clickclack /tmp/openclaw-release-upgrade-status.json +openclaw message send \ + --channel clickclack \ + --target channel:general \ + --message "release upgrade outbound" \ + --json >/tmp/openclaw-release-upgrade-clickclack-outbound.json 2>/tmp/openclaw-release-upgrade-clickclack-outbound.err +node scripts/e2e/lib/release-user-journey/assertions.mjs assert-clickclack-state outbound "$CLICKCLACK_STATE" "release upgrade outbound" + +start_gateway /tmp/openclaw-release-upgrade-gateway.log +node scripts/e2e/lib/release-user-journey/assertions.mjs wait-clickclack-socket "http://127.0.0.1:$CLICKCLACK_PORT" 45 +node scripts/e2e/lib/release-user-journey/assertions.mjs post-clickclack-inbound "http://127.0.0.1:$CLICKCLACK_PORT" "Return marker $SUCCESS_MARKER" +node scripts/e2e/lib/release-user-journey/assertions.mjs wait-clickclack-reply "$CLICKCLACK_STATE" "$SUCCESS_MARKER" 45 + +echo "Release upgrade user journey scenario passed." diff --git a/scripts/e2e/mock-openai-server.mjs b/scripts/e2e/mock-openai-server.mjs index 3185a72802b..962250abaf4 100644 --- a/scripts/e2e/mock-openai-server.mjs +++ b/scripts/e2e/mock-openai-server.mjs @@ -100,6 +100,20 @@ function writeChatCompletion(res, stream, text = successMarker) { }); } +function writeImageGeneration(res) { + writeJson(res, 200, { + created: Math.floor(Date.now() / 1000), + data: [ + { + b64_json: + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+yf7kAAAAASUVORK5CYII=", + mime_type: "image/png", + revised_prompt: "openclaw mock image", + }, + ], + }); +} + function resolveResponseText(bodyText) { const matches = Array.from(bodyText.matchAll(/\bOPENCLAW_E2E_OK(?:_\d+)?\b/gu)); return matches.at(-1)?.[0] ?? successMarker; @@ -163,6 +177,29 @@ const server = http.createServer(async (req, res) => { return; } + if (req.method === "POST" && url.pathname === "/v1/embeddings") { + const input = Array.isArray(body.input) ? body.input : [body.input ?? ""]; + writeJson(res, 200, { + object: "list", + data: input.map((_, index) => ({ + object: "embedding", + index, + embedding: [1, index / 100, 0, 0], + })), + model: body.model ?? "text-embedding-3-small", + usage: { prompt_tokens: input.length, total_tokens: input.length }, + }); + return; + } + + if ( + req.method === "POST" && + (url.pathname === "/v1/images/generations" || url.pathname === "/v1/images/edits") + ) { + writeImageGeneration(res); + return; + } + writeJson(res, 404, { error: { message: `unhandled mock route: ${req.method} ${url.pathname}` }, }); diff --git a/scripts/e2e/release-media-memory-docker.sh b/scripts/e2e/release-media-memory-docker.sh new file mode 100755 index 00000000000..23aec14fc89 --- /dev/null +++ b/scripts/e2e/release-media-memory-docker.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Package-installed media and memory release smoke. +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" +source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" + +IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-release-media-memory-e2e" OPENCLAW_RELEASE_MEDIA_MEMORY_E2E_IMAGE)" +SKIP_BUILD="${OPENCLAW_RELEASE_MEDIA_MEMORY_E2E_SKIP_BUILD:-0}" +PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz release-media-memory "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")" +docker_e2e_package_mount_args "$PACKAGE_TGZ" + +docker_e2e_build_or_reuse "$IMAGE_NAME" release-media-memory "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "bare" "$SKIP_BUILD" +OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 release-media-memory empty)" + +run_log="$(docker_e2e_run_log release-media-memory)" +echo "Running release media memory Docker E2E..." +if ! docker_e2e_run_with_harness \ + -e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ + -e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \ + "${DOCKER_E2E_PACKAGE_ARGS[@]}" \ + -i "$IMAGE_NAME" bash scripts/e2e/lib/release-media-memory/scenario.sh >"$run_log" 2>&1; then + docker_e2e_print_log "$run_log" + rm -f "$run_log" + exit 1 +fi + +rm -f "$run_log" +echo "Release media memory Docker E2E passed." diff --git a/scripts/e2e/release-plugin-marketplace-docker.sh b/scripts/e2e/release-plugin-marketplace-docker.sh new file mode 100755 index 00000000000..da6de364e76 --- /dev/null +++ b/scripts/e2e/release-plugin-marketplace-docker.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Package-installed local marketplace install/update/uninstall smoke. +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" +source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" + +IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-release-plugin-marketplace-e2e" OPENCLAW_RELEASE_PLUGIN_MARKETPLACE_E2E_IMAGE)" +SKIP_BUILD="${OPENCLAW_RELEASE_PLUGIN_MARKETPLACE_E2E_SKIP_BUILD:-0}" +PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz release-plugin-marketplace "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")" +docker_e2e_package_mount_args "$PACKAGE_TGZ" + +docker_e2e_build_or_reuse "$IMAGE_NAME" release-plugin-marketplace "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "bare" "$SKIP_BUILD" +OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 release-plugin-marketplace empty)" + +run_log="$(docker_e2e_run_log release-plugin-marketplace)" +echo "Running release plugin marketplace Docker E2E..." +if ! docker_e2e_run_with_harness \ + -e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ + -e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \ + "${DOCKER_E2E_PACKAGE_ARGS[@]}" \ + -i "$IMAGE_NAME" bash scripts/e2e/lib/release-plugin-marketplace/scenario.sh >"$run_log" 2>&1; then + docker_e2e_print_log "$run_log" + rm -f "$run_log" + exit 1 +fi + +rm -f "$run_log" +echo "Release plugin marketplace Docker E2E passed." diff --git a/scripts/e2e/release-typed-onboarding-docker.sh b/scripts/e2e/release-typed-onboarding-docker.sh new file mode 100755 index 00000000000..7085238a9f4 --- /dev/null +++ b/scripts/e2e/release-typed-onboarding-docker.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Package-installed release onboarding smoke with real TTY keypresses and env-ref provider auth. +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" +source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" + +IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-release-typed-onboarding-e2e" OPENCLAW_RELEASE_TYPED_ONBOARDING_E2E_IMAGE)" +SKIP_BUILD="${OPENCLAW_RELEASE_TYPED_ONBOARDING_E2E_SKIP_BUILD:-0}" +PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz release-typed-onboarding "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")" +docker_e2e_package_mount_args "$PACKAGE_TGZ" + +docker_e2e_build_or_reuse "$IMAGE_NAME" release-typed-onboarding "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "bare" "$SKIP_BUILD" +OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 release-typed-onboarding empty)" + +run_log="$(docker_e2e_run_log release-typed-onboarding)" +echo "Running release typed onboarding Docker E2E..." +if ! docker_e2e_run_with_harness \ + -e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ + -e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \ + "${DOCKER_E2E_PACKAGE_ARGS[@]}" \ + -i "$IMAGE_NAME" bash scripts/e2e/lib/release-typed-onboarding/scenario.sh >"$run_log" 2>&1; then + docker_e2e_print_log "$run_log" + rm -f "$run_log" + exit 1 +fi + +rm -f "$run_log" +echo "Release typed onboarding Docker E2E passed." diff --git a/scripts/e2e/release-upgrade-user-journey-docker.sh b/scripts/e2e/release-upgrade-user-journey-docker.sh new file mode 100755 index 00000000000..525709606d6 --- /dev/null +++ b/scripts/e2e/release-upgrade-user-journey-docker.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Published-baseline-to-candidate release user journey smoke. +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh" +source "$ROOT_DIR/scripts/lib/docker-e2e-package.sh" + +IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-release-upgrade-user-journey-e2e" OPENCLAW_RELEASE_UPGRADE_USER_JOURNEY_E2E_IMAGE)" +SKIP_BUILD="${OPENCLAW_RELEASE_UPGRADE_USER_JOURNEY_E2E_SKIP_BUILD:-0}" +PACKAGE_TGZ="$(docker_e2e_prepare_package_tgz release-upgrade-user-journey "${OPENCLAW_CURRENT_PACKAGE_TGZ:-}")" +docker_e2e_package_mount_args "$PACKAGE_TGZ" + +docker_e2e_build_or_reuse "$IMAGE_NAME" release-upgrade-user-journey "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR" "bare" "$SKIP_BUILD" +OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 release-upgrade-user-journey empty)" + +run_log="$(docker_e2e_run_log release-upgrade-user-journey)" +echo "Running release upgrade user journey Docker E2E..." +if ! docker_e2e_run_with_harness \ + -e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \ + -e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \ + -e "OPENCLAW_RELEASE_UPGRADE_BASELINE_SPEC=${OPENCLAW_RELEASE_UPGRADE_BASELINE_SPEC:-openclaw@latest}" \ + "${DOCKER_E2E_PACKAGE_ARGS[@]}" \ + -i "$IMAGE_NAME" bash scripts/e2e/lib/release-upgrade-user-journey/scenario.sh >"$run_log" 2>&1; then + docker_e2e_print_log "$run_log" + rm -f "$run_log" + exit 1 +fi + +rm -f "$run_log" +echo "Release upgrade user journey Docker E2E passed." diff --git a/scripts/lib/docker-e2e-scenarios.mjs b/scripts/lib/docker-e2e-scenarios.mjs index b31cd1deef2..921f09ef8e2 100644 --- a/scripts/lib/docker-e2e-scenarios.mjs +++ b/scripts/lib/docker-e2e-scenarios.mjs @@ -246,6 +246,46 @@ export const mainLanes = [ weight: 4, }, ), + npmLane( + "release-typed-onboarding", + "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:release-typed-onboarding", + { + resources: ["npm", "service"], + stateScenario: "empty", + timeoutMs: 20 * 60 * 1000, + weight: 3, + }, + ), + npmLane( + "release-media-memory", + "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:release-media-memory", + { + resources: ["npm", "service"], + stateScenario: "empty", + timeoutMs: 20 * 60 * 1000, + weight: 3, + }, + ), + npmLane( + "release-upgrade-user-journey", + "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:release-upgrade-user-journey", + { + resources: ["npm", "service"], + stateScenario: "empty", + timeoutMs: 30 * 60 * 1000, + weight: 5, + }, + ), + npmLane( + "release-plugin-marketplace", + "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:release-plugin-marketplace", + { + resources: ["npm"], + stateScenario: "empty", + timeoutMs: 20 * 60 * 1000, + weight: 3, + }, + ), serviceLane("gateway-network", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:gateway-network"), serviceLane( "agents-delete-shared-workspace",