diff --git a/docs/help/testing.md b/docs/help/testing.md index 6703a568975..0ec006fc6f4 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -537,7 +537,7 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or - Plugins (install smoke + `/plugin` alias + Claude-bundle restart semantics): `pnpm test:docker:plugins` (script: `scripts/e2e/plugins-docker.sh`) - Plugin update unchanged smoke: `pnpm test:docker:plugin-update` (script: `scripts/e2e/plugin-update-unchanged-docker.sh`) - Config reload metadata smoke: `pnpm test:docker:config-reload` (script: `scripts/e2e/config-reload-source-docker.sh`) -- Bundled plugin runtime deps: `pnpm test:docker:bundled-channel-deps` builds a small Docker runner image by default, builds and packs OpenClaw once on the host, then mounts that tarball into each Linux install scenario. Reuse the image with `OPENCLAW_SKIP_DOCKER_BUILD=1`, skip the host rebuild after a fresh local build with `OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD=0`, or point at an existing tarball with `OPENCLAW_BUNDLED_CHANNEL_PACKAGE_TGZ=/path/to/openclaw-*.tgz`. +- Bundled plugin runtime deps: `pnpm test:docker:bundled-channel-deps` builds a small Docker runner image by default, builds and packs OpenClaw once on the host, then mounts that tarball into each Linux install scenario. Reuse the image with `OPENCLAW_SKIP_DOCKER_BUILD=1`, skip the host rebuild after a fresh local build with `OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD=0`, or point at an existing tarball with `OPENCLAW_BUNDLED_CHANNEL_PACKAGE_TGZ=/path/to/openclaw-*.tgz`. The full Docker aggregate pre-packs this tarball once, then shards bundled channel checks into independent lanes; use `OPENCLAW_BUNDLED_CHANNELS=telegram,slack` to narrow the channel matrix when running the bundled lane directly. - Narrow bundled plugin runtime deps while iterating by disabling unrelated scenarios, for example: `OPENCLAW_BUNDLED_CHANNEL_SCENARIOS=0 OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO=0 pnpm test:docker:bundled-channel-deps`. diff --git a/scripts/e2e/bundled-channel-runtime-deps-docker.sh b/scripts/e2e/bundled-channel-runtime-deps-docker.sh index ee116614c41..0ef65f5141e 100644 --- a/scripts/e2e/bundled-channel-runtime-deps-docker.sh +++ b/scripts/e2e/bundled-channel-runtime-deps-docker.sh @@ -1366,11 +1366,22 @@ EOF } if [ "$RUN_CHANNEL_SCENARIOS" != "0" ]; then - run_channel_scenario telegram grammy - run_channel_scenario discord discord-api-types - run_channel_scenario slack @slack/web-api - run_channel_scenario feishu @larksuiteoapi/node-sdk - run_channel_scenario memory-lancedb @lancedb/lancedb + IFS=',' read -r -a CHANNEL_SCENARIOS <<<"${OPENCLAW_BUNDLED_CHANNELS:-telegram,discord,slack,feishu,memory-lancedb}" + for channel_scenario in "${CHANNEL_SCENARIOS[@]}"; do + channel_scenario="${channel_scenario//[[:space:]]/}" + [ -n "$channel_scenario" ] || continue + case "$channel_scenario" in + telegram) run_channel_scenario telegram grammy ;; + discord) run_channel_scenario discord discord-api-types ;; + slack) run_channel_scenario slack @slack/web-api ;; + feishu) run_channel_scenario feishu @larksuiteoapi/node-sdk ;; + memory-lancedb) run_channel_scenario memory-lancedb @lancedb/lancedb ;; + *) + echo "Unsupported OPENCLAW_BUNDLED_CHANNELS entry: $channel_scenario" >&2 + exit 1 + ;; + esac + done fi if [ "$RUN_UPDATE_SCENARIO" != "0" ]; then run_update_scenario diff --git a/scripts/test-docker-all.mjs b/scripts/test-docker-all.mjs index 391f5cbc75f..e2ae94f1ce1 100644 --- a/scripts/test-docker-all.mjs +++ b/scripts/test-docker-all.mjs @@ -6,12 +6,42 @@ import { fileURLToPath } from "node:url"; const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const DEFAULT_E2E_IMAGE = "openclaw-docker-e2e:local"; -const DEFAULT_PARALLELISM = 8; -const DEFAULT_TAIL_PARALLELISM = 8; +const DEFAULT_PARALLELISM = 10; +const DEFAULT_TAIL_PARALLELISM = 10; const DEFAULT_FAILURE_TAIL_LINES = 80; const DEFAULT_LANE_TIMEOUT_MS = 120 * 60 * 1000; const DEFAULT_LANE_START_STAGGER_MS = 2_000; +const bundledChannelLaneCommand = + "OPENCLAW_SKIP_DOCKER_BUILD=1 OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_LOAD_FAILURE_SCENARIO=0 pnpm test:docker:bundled-channel-deps"; + +const bundledScenarioLanes = [ + ["bundled-channel-telegram", `OPENCLAW_BUNDLED_CHANNELS=telegram ${bundledChannelLaneCommand}`], + ["bundled-channel-discord", `OPENCLAW_BUNDLED_CHANNELS=discord ${bundledChannelLaneCommand}`], + ["bundled-channel-slack", `OPENCLAW_BUNDLED_CHANNELS=slack ${bundledChannelLaneCommand}`], + ["bundled-channel-feishu", `OPENCLAW_BUNDLED_CHANNELS=feishu ${bundledChannelLaneCommand}`], + [ + "bundled-channel-memory-lancedb", + `OPENCLAW_BUNDLED_CHANNELS=memory-lancedb ${bundledChannelLaneCommand}`, + ], + [ + "bundled-channel-update", + "OPENCLAW_SKIP_DOCKER_BUILD=1 OPENCLAW_BUNDLED_CHANNEL_SCENARIOS=0 OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO=1 OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_LOAD_FAILURE_SCENARIO=0 pnpm test:docker:bundled-channel-deps", + ], + [ + "bundled-channel-root-owned", + "OPENCLAW_SKIP_DOCKER_BUILD=1 OPENCLAW_BUNDLED_CHANNEL_SCENARIOS=0 OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO=1 OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_LOAD_FAILURE_SCENARIO=0 pnpm test:docker:bundled-channel-deps", + ], + [ + "bundled-channel-setup-entry", + "OPENCLAW_SKIP_DOCKER_BUILD=1 OPENCLAW_BUNDLED_CHANNEL_SCENARIOS=0 OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO=1 OPENCLAW_BUNDLED_CHANNEL_LOAD_FAILURE_SCENARIO=0 pnpm test:docker:bundled-channel-deps", + ], + [ + "bundled-channel-load-failure", + "OPENCLAW_SKIP_DOCKER_BUILD=1 OPENCLAW_BUNDLED_CHANNEL_SCENARIOS=0 OPENCLAW_BUNDLED_CHANNEL_UPDATE_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_ROOT_OWNED_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_SETUP_ENTRY_SCENARIO=0 OPENCLAW_BUNDLED_CHANNEL_LOAD_FAILURE_SCENARIO=1 pnpm test:docker:bundled-channel-deps", + ], +]; + const lanes = [ ["live-models", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-models"], ["live-gateway", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-gateway"], @@ -37,7 +67,7 @@ const lanes = [ ["plugins", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:plugins"], ["plugin-update", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:plugin-update"], ["config-reload", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:config-reload"], - ["bundled-channel-deps", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:bundled-channel-deps"], + ...bundledScenarioLanes, ["openai-image-auth", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:openai-image-auth"], ["qr", "pnpm test:docker:qr"], ]; @@ -53,7 +83,9 @@ const exclusiveLanes = [ "live-cli-backend-codex", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-cli-backend:codex", ], - ["live-acp-bind", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-acp-bind"], + ["live-acp-bind-claude", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-acp-bind:claude"], + ["live-acp-bind-codex", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-acp-bind:codex"], + ["live-acp-bind-gemini", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:live-acp-bind:gemini"], ]; const tailLanes = exclusiveLanes; @@ -117,6 +149,10 @@ function commandEnv(extra = {}) { }; } +function shellQuote(value) { + return `'${String(value).replaceAll("'", "'\\''")}'`; +} + function runShellCommand({ command, env, label, logFile, timeoutMs }) { return new Promise((resolve) => { const child = spawn("bash", ["-lc", command], { @@ -176,6 +212,45 @@ async function runForeground(label, command, env) { } } +async function prepareBundledChannelPackage(baseEnv, logDir) { + if (baseEnv.OPENCLAW_BUNDLED_CHANNEL_PACKAGE_TGZ) { + console.log(`==> Bundled channel package: ${baseEnv.OPENCLAW_BUNDLED_CHANNEL_PACKAGE_TGZ}`); + return; + } + + const packDir = path.join(logDir, "bundled-channel-package"); + await mkdir(packDir, { recursive: true }); + const packScript = [ + "set -euo pipefail", + "node --import tsx --input-type=module -e \"const { writePackageDistInventory } = await import('./src/infra/package-dist-inventory.ts'); await writePackageDistInventory(process.cwd());\"", + "npm pack --silent --ignore-scripts --pack-destination /tmp/openclaw-pack >/tmp/openclaw-pack.out", + "cat /tmp/openclaw-pack.out", + ].join("\n"); + await runForeground( + "Pack bundled channel package once from Docker E2E image", + [ + "docker run --rm", + "-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0", + `-v ${shellQuote(packDir)}:/tmp/openclaw-pack`, + shellQuote(baseEnv.OPENCLAW_DOCKER_E2E_IMAGE), + "bash -lc", + shellQuote(packScript), + ].join(" "), + baseEnv, + ); + + const packed = (await fs.promises.readdir(packDir)) + .filter((entry) => /^openclaw-.*\.tgz$/.test(entry)) + .toSorted() + .at(-1); + if (!packed) { + throw new Error(`missing packed OpenClaw tarball in ${packDir}`); + } + baseEnv.OPENCLAW_BUNDLED_CHANNEL_PACKAGE_TGZ = path.join(packDir, packed); + baseEnv.OPENCLAW_BUNDLED_CHANNEL_HOST_BUILD = "0"; + console.log(`==> Bundled channel package: ${baseEnv.OPENCLAW_BUNDLED_CHANNEL_PACKAGE_TGZ}`); +} + function laneEnv(name, baseEnv, logDir) { const env = { ...baseEnv, @@ -358,6 +433,7 @@ async function main() { "pnpm test:docker:e2e-build", baseEnv, ); + await prepareBundledChannelPackage(baseEnv, logDir); const options = { failFast, startStaggerMs: laneStartStaggerMs, timeoutMs: laneTimeoutMs }; const failures = await runLanePool(lanes, baseEnv, logDir, parallelism, options);