mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 01:31:08 +00:00
fix: stabilize docker live and docker e2e harnesses
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
'
|
||||
|
||||
@@ -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"
|
||||
|
||||
35
scripts/lib/live-docker-stage.sh
Normal file
35
scripts/lib/live-docker-stage.sh
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
"<final>Visible</final>",
|
||||
"google/gemini-2.5-flash",
|
||||
),
|
||||
).toBe("Visible");
|
||||
expect(
|
||||
maybeStripAssistantScaffoldingForLiveModel(
|
||||
"<think>hidden</think>Visible",
|
||||
@@ -448,7 +455,7 @@ describe("maybeStripAssistantScaffoldingForLiveModel", () => {
|
||||
"<think>hidden</think>Visible",
|
||||
"google/gemini-2.5-flash",
|
||||
),
|
||||
).toBe("<think>hidden</think>Visible");
|
||||
).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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user