diff --git a/docs/help/testing.md b/docs/help/testing.md index 58541cbc7ce..7c143d1dc8f 100644 --- a/docs/help/testing.md +++ b/docs/help/testing.md @@ -442,6 +442,10 @@ The live-model Docker runners also bind-mount only the needed CLI auth homes (or The live-model Docker runners also bind-mount the current checkout read-only and stage it into a temporary workdir inside the container. This keeps the runtime image slim while still running Vitest against your exact local source/config. +The staging step skips large local-only caches and app build outputs such as +`.pnpm-store`, `.worktrees`, `__openclaw_vitest__`, and app-local `.build` or +Gradle output directories so Docker live runs do not spend minutes copying +machine-specific artifacts. They also set `OPENCLAW_SKIP_CHANNELS=1` so gateway live probes do not start real Telegram/Discord/etc. channel workers inside the container. `test:docker:live-models` still runs `pnpm test:live`, so pass through @@ -479,8 +483,8 @@ Useful env vars: - `OPENCLAW_PROFILE_FILE=...` (default: `~/.profile`) mounted to `/home/node/.profile` and sourced before running tests - `OPENCLAW_DOCKER_CLI_TOOLS_DIR=...` (default: `~/.cache/openclaw/docker-cli-tools`) mounted to `/home/node/.npm-global` for cached CLI installs inside Docker - External CLI auth dirs/files under `$HOME` are mounted read-only under `/host-auth...`, then copied into `/home/node/...` before tests start - - Default dirs: `.codex`, `.minimax` - - Default files: `.claude.json`, `~/.claude/.credentials.json`, `~/.claude/settings.json`, `~/.claude/settings.local.json` + - Default dirs: `.minimax` + - Default files: `~/.codex/auth.json`, `~/.codex/config.toml`, `.claude.json`, `~/.claude/.credentials.json`, `~/.claude/settings.json`, `~/.claude/settings.local.json` - Narrowed provider runs mount only the needed dirs/files inferred from `OPENCLAW_LIVE_PROVIDERS` / `OPENCLAW_LIVE_GATEWAY_PROVIDERS` - Override manually with `OPENCLAW_DOCKER_AUTH_DIRS=all`, `OPENCLAW_DOCKER_AUTH_DIRS=none`, or a comma list like `OPENCLAW_DOCKER_AUTH_DIRS=.claude,.codex` - `OPENCLAW_LIVE_GATEWAY_MODELS=...` / `OPENCLAW_LIVE_MODELS=...` to narrow the run diff --git a/scripts/e2e/openwebui-docker.sh b/scripts/e2e/openwebui-docker.sh index 2144faf3ee9..71f662a5fd3 100755 --- a/scripts/e2e/openwebui-docker.sh +++ b/scripts/e2e/openwebui-docker.sh @@ -88,16 +88,43 @@ config.models = { }, }, }; +config.gateway = { + ...(config.gateway || {}), + controlUi: { + ...(config.gateway?.controlUi || {}), + enabled: false, + }, + mode: "local", + bind: "lan", + auth: { + ...(config.gateway?.auth || {}), + mode: "token", + token: process.env.OPENCLAW_GATEWAY_TOKEN, + }, + http: { + ...(config.gateway?.http || {}), + endpoints: { + ...(config.gateway?.http?.endpoints || {}), + chatCompletions: { + ...(config.gateway?.http?.endpoints?.chatCompletions || {}), + enabled: true, + }, + }, + }, +}; +config.agents = { + ...(config.agents || {}), + defaults: { + ...(config.agents?.defaults || {}), + model: { + ...(config.agents?.defaults?.model || {}), + primary: process.env.OPENCLAW_OPENWEBUI_MODEL, + }, + }, +}; fs.mkdirSync(path.dirname(configPath), { recursive: true }); fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8"); NODE - node "$entry" config set gateway.controlUi.enabled false >/dev/null - node "$entry" config set gateway.mode local >/dev/null - node "$entry" config set gateway.bind lan >/dev/null - node "$entry" config set gateway.auth.mode token >/dev/null - node "$entry" config set gateway.auth.token "$OPENCLAW_GATEWAY_TOKEN" >/dev/null - node "$entry" config set gateway.http.endpoints.chatCompletions.enabled true --strict-json >/dev/null - node "$entry" config set agents.defaults.model.primary "$OPENCLAW_OPENWEBUI_MODEL" >/dev/null exec node "$entry" gateway --port '"$PORT"' --bind lan --allow-unconfigured > /tmp/openwebui-gateway.log 2>&1 ' diff --git a/scripts/lib/live-docker-auth.sh b/scripts/lib/live-docker-auth.sh index 5eec7a5e0b7..2dbacee81df 100644 --- a/scripts/lib/live-docker-auth.sh +++ b/scripts/lib/live-docker-auth.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash -OPENCLAW_DOCKER_LIVE_AUTH_ALL=(.codex .minimax) +OPENCLAW_DOCKER_LIVE_AUTH_ALL=(.minimax) OPENCLAW_DOCKER_LIVE_AUTH_FILES_ALL=( + .codex/auth.json + .codex/config.toml .claude.json .claude/.credentials.json .claude/settings.json @@ -27,9 +29,6 @@ openclaw_live_should_include_auth_dir_for_provider() { local provider provider="$(openclaw_live_trim "${1:-}")" case "$provider" in - openai-codex) - printf '%s\n' ".codex" - ;; minimax | minimax-portal) printf '%s\n' ".minimax" ;; @@ -40,6 +39,10 @@ openclaw_live_should_include_auth_file_for_provider() { local provider provider="$(openclaw_live_trim "${1:-}")" case "$provider" in + openai-codex) + printf '%s\n' ".codex/auth.json" + printf '%s\n' ".codex/config.toml" + ;; anthropic) printf '%s\n' ".claude.json" printf '%s\n' ".claude/.credentials.json" diff --git a/scripts/lib/live-docker-stage.sh b/scripts/lib/live-docker-stage.sh new file mode 100644 index 00000000000..dba5a75f653 --- /dev/null +++ b/scripts/lib/live-docker-stage.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +openclaw_live_stage_source_tree() { + local dest_dir="${1:?destination directory required}" + + tar -C /src \ + --exclude=.git \ + --exclude=node_modules \ + --exclude=dist \ + --exclude=ui/dist \ + --exclude=ui/node_modules \ + --exclude=.pnpm-store \ + --exclude=.tmp \ + --exclude=.tmp-precommit-venv \ + --exclude=.worktrees \ + --exclude=__openclaw_vitest__ \ + --exclude='apps/*/.build' \ + --exclude='apps/*/*.bun-build' \ + --exclude='apps/*/.gradle' \ + --exclude='apps/*/.kotlin' \ + --exclude='apps/*/build' \ + -cf - . | tar -C "$dest_dir" -xf - +} + +openclaw_live_link_runtime_tree() { + local dest_dir="${1:?destination directory required}" + + ln -s /app/node_modules "$dest_dir/node_modules" + ln -s /app/dist "$dest_dir/dist" + if [ -d /app/dist-runtime/extensions ]; then + export OPENCLAW_BUNDLED_PLUGINS_DIR=/app/dist-runtime/extensions + elif [ -d /app/dist/extensions ]; then + export OPENCLAW_BUNDLED_PLUGINS_DIR=/app/dist/extensions + fi +} diff --git a/scripts/test-live-acp-bind-docker.sh b/scripts/test-live-acp-bind-docker.sh index b9151d2f9de..c28149a3f38 100644 --- a/scripts/test-live-acp-bind-docker.sh +++ b/scripts/test-live-acp-bind-docker.sh @@ -150,20 +150,9 @@ cleanup() { rm -rf "$tmp_dir" } trap cleanup EXIT -tar -C /src \ - --exclude=.git \ - --exclude=node_modules \ - --exclude=dist \ - --exclude=ui/dist \ - --exclude=ui/node_modules \ - -cf - . | tar -C "$tmp_dir" -xf - -ln -s /app/node_modules "$tmp_dir/node_modules" -ln -s /app/dist "$tmp_dir/dist" -if [ -d /app/dist-runtime/extensions ]; then - export OPENCLAW_BUNDLED_PLUGINS_DIR=/app/dist-runtime/extensions -elif [ -d /app/dist/extensions ]; then - export OPENCLAW_BUNDLED_PLUGINS_DIR=/app/dist/extensions -fi +source /app/scripts/lib/live-docker-stage.sh +openclaw_live_stage_source_tree "$tmp_dir" +openclaw_live_link_runtime_tree "$tmp_dir" cd "$tmp_dir" export OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND="${OPENCLAW_LIVE_ACP_BIND_AGENT_COMMAND:-}" pnpm test:live src/gateway/gateway-acp-bind.live.test.ts diff --git a/scripts/test-live-gateway-models-docker.sh b/scripts/test-live-gateway-models-docker.sh index 1e33aee03cc..c4119a1c7ee 100755 --- a/scripts/test-live-gateway-models-docker.sh +++ b/scripts/test-live-gateway-models-docker.sh @@ -101,20 +101,9 @@ cleanup() { rm -rf "$tmp_dir" } trap cleanup EXIT -tar -C /src \ - --exclude=.git \ - --exclude=node_modules \ - --exclude=dist \ - --exclude=ui/dist \ - --exclude=ui/node_modules \ - -cf - . | tar -C "$tmp_dir" -xf - -ln -s /app/node_modules "$tmp_dir/node_modules" -ln -s /app/dist "$tmp_dir/dist" -if [ -d /app/dist-runtime/extensions ]; then - export OPENCLAW_BUNDLED_PLUGINS_DIR=/app/dist-runtime/extensions -elif [ -d /app/dist/extensions ]; then - export OPENCLAW_BUNDLED_PLUGINS_DIR=/app/dist/extensions -fi +source /app/scripts/lib/live-docker-stage.sh +openclaw_live_stage_source_tree "$tmp_dir" +openclaw_live_link_runtime_tree "$tmp_dir" cd "$tmp_dir" pnpm test:live:gateway-profiles EOF diff --git a/scripts/test-live-models-docker.sh b/scripts/test-live-models-docker.sh index 176c21ede5c..9b59bf4f8cc 100755 --- a/scripts/test-live-models-docker.sh +++ b/scripts/test-live-models-docker.sh @@ -111,20 +111,9 @@ cleanup() { rm -rf "$tmp_dir" } trap cleanup EXIT -tar -C /src \ - --exclude=.git \ - --exclude=node_modules \ - --exclude=dist \ - --exclude=ui/dist \ - --exclude=ui/node_modules \ - -cf - . | tar -C "$tmp_dir" -xf - -ln -s /app/node_modules "$tmp_dir/node_modules" -ln -s /app/dist "$tmp_dir/dist" -if [ -d /app/dist-runtime/extensions ]; then - export OPENCLAW_BUNDLED_PLUGINS_DIR=/app/dist-runtime/extensions -elif [ -d /app/dist/extensions ]; then - export OPENCLAW_BUNDLED_PLUGINS_DIR=/app/dist/extensions -fi +source /app/scripts/lib/live-docker-stage.sh +openclaw_live_stage_source_tree "$tmp_dir" +openclaw_live_link_runtime_tree "$tmp_dir" cd "$tmp_dir" pnpm test:live:models-profiles EOF diff --git a/src/gateway/gateway-models.profiles.live.test.ts b/src/gateway/gateway-models.profiles.live.test.ts index 88083899f5d..91f54ee557b 100644 --- a/src/gateway/gateway-models.profiles.live.test.ts +++ b/src/gateway/gateway-models.profiles.live.test.ts @@ -66,6 +66,7 @@ const GATEWAY_LIVE_HEARTBEAT_MS = Math.max( toInt(process.env.OPENCLAW_LIVE_GATEWAY_HEARTBEAT_MS, 30_000), ); const GATEWAY_LIVE_STRIP_SCAFFOLDING_MODEL_KEYS = new Set([ + "google/gemini-2.5-flash", "google/gemini-3-flash-preview", "google/gemini-3-pro-preview", "google/gemini-3.1-flash-lite-preview", @@ -419,6 +420,12 @@ function shouldSkipEmptyResponseForLiveModel(params: { describe("maybeStripAssistantScaffoldingForLiveModel", () => { it("strips scaffolding for Gemini preview models with known transcript wrappers", () => { + expect( + maybeStripAssistantScaffoldingForLiveModel( + "Visible", + "google/gemini-2.5-flash", + ), + ).toBe("Visible"); expect( maybeStripAssistantScaffoldingForLiveModel( "hiddenVisible", @@ -448,7 +455,7 @@ describe("maybeStripAssistantScaffoldingForLiveModel", () => { "hiddenVisible", "google/gemini-2.5-flash", ), - ).toBe("hiddenVisible"); + ).toBe("Visible"); }); it("strips scaffolding for known OpenAI transcript wrappers", () => { @@ -1816,12 +1823,14 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) { client.stop(); await server.close({ reason: "live test complete" }); await fs.rm(toolProbePath, { force: true }); - await fs.rm(tempDir, { recursive: true, force: true }); + // Give the filesystem a short retry window while agent/runtime teardown + // releases handles inside these temporary live-test directories. + await fs.rm(tempDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); if (tempAgentDir) { - await fs.rm(tempAgentDir, { recursive: true, force: true }); + await fs.rm(tempAgentDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); } if (tempStateDir) { - await fs.rm(tempStateDir, { recursive: true, force: true }); + await fs.rm(tempStateDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); } process.env.OPENCLAW_CONFIG_PATH = previous.configPath; diff --git a/src/gateway/server.impl.ts b/src/gateway/server.impl.ts index ec70dee1fcf..6e1f3fac718 100644 --- a/src/gateway/server.impl.ts +++ b/src/gateway/server.impl.ts @@ -32,7 +32,7 @@ import { resolveControlUiRootSync, } from "../infra/control-ui-assets.js"; import { isDiagnosticsEnabled } from "../infra/diagnostic-events.js"; -import { logAcceptedEnvOption } from "../infra/env.js"; +import { isTruthyEnvValue, logAcceptedEnvOption } from "../infra/env.js"; import { createExecApprovalForwarder } from "../infra/exec-approval-forwarder.js"; import { onHeartbeatEvent } from "../infra/heartbeat-events.js"; import { startHeartbeatRunner, type HeartbeatRunner } from "../infra/heartbeat-runner.js"; @@ -179,6 +179,20 @@ function getChannelRuntime() { cachedChannelRuntime ??= createPluginRuntime().channel; return cachedChannelRuntime; } + +function pruneSkippedStartupSecretSurfaces(config: OpenClawConfig): OpenClawConfig { + const skipChannels = + isTruthyEnvValue(process.env.OPENCLAW_SKIP_CHANNELS) || + isTruthyEnvValue(process.env.OPENCLAW_SKIP_PROVIDERS); + if (!skipChannels || !config.channels) { + return config; + } + return { + ...config, + channels: undefined, + }; +} + const logHealth = log.child("health"); const logCron = log.child("cron"); const logReload = log.child("reload"); @@ -464,7 +478,9 @@ export async function startGatewayServer( ) => await runWithSecretsActivationLock(async () => { try { - const prepared = await prepareSecretsRuntimeSnapshot({ config }); + const prepared = await prepareSecretsRuntimeSnapshot({ + config: pruneSkippedStartupSecretSurfaces(config), + }); if (params.activate) { activateSecretsRuntimeSnapshot(prepared); logGatewayAuthSurfaceDiagnostics(prepared); diff --git a/src/gateway/server.reload.test.ts b/src/gateway/server.reload.test.ts index 007949a49e6..a6cfd58b2a8 100644 --- a/src/gateway/server.reload.test.ts +++ b/src/gateway/server.reload.test.ts @@ -229,6 +229,16 @@ describe("gateway hot reload", () => { }); } + async function writeChannelEnvRefConfig() { + await writeConfigFile({ + channels: { + telegram: { + botToken: { source: "env", provider: "default", id: "TELEGRAM_BOT_TOKEN" }, + }, + }, + }); + } + async function writeConfigFile(config: unknown) { const configPath = process.env.OPENCLAW_CONFIG_PATH; if (!configPath) { @@ -566,6 +576,13 @@ describe("gateway hot reload", () => { ); }); + it("allows startup when unresolved channel refs exist but channels are skipped", async () => { + await writeChannelEnvRefConfig(); + delete process.env.TELEGRAM_BOT_TOKEN; + process.env.OPENCLAW_SKIP_CHANNELS = "1"; + await expect(withGatewayServer(async () => {})).resolves.toBeUndefined(); + }); + it("fails startup when an active exec ref id contains traversal segments", async () => { await writeGatewayTraversalExecRefConfig(); const previousGatewayAuth = testState.gatewayAuth;