mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-23 14:45:46 +00:00
fix: stabilize docker e2e lanes
This commit is contained in:
@@ -80,6 +80,21 @@ describe("acpx plugin config parsing", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves workspace plugin root from dist-runtime shared chunks", () => {
|
||||
const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), "acpx-root-shared-dist-runtime-"));
|
||||
const workspacePluginRoot = bundledPluginRootAt(repoRoot, "acpx");
|
||||
try {
|
||||
fs.mkdirSync(workspacePluginRoot, { recursive: true });
|
||||
fs.mkdirSync(path.join(repoRoot, "dist-runtime"), { recursive: true });
|
||||
fs.writeFileSync(path.join(workspacePluginRoot, "package.json"), "{}\n", "utf8");
|
||||
fs.writeFileSync(path.join(workspacePluginRoot, "openclaw.plugin.json"), "{}\n", "utf8");
|
||||
|
||||
const moduleUrl = pathToFileURL(path.join(repoRoot, "dist-runtime", "register.runtime.js")).href;
|
||||
expect(resolveAcpxPluginRoot(moduleUrl)).toBe(workspacePluginRoot);
|
||||
} finally {
|
||||
fs.rmSync(repoRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
it("resolves bundled acpx with pinned version by default", () => {
|
||||
const resolved = resolveAcpxPluginConfig({
|
||||
rawConfig: {
|
||||
|
||||
@@ -67,6 +67,27 @@ function resolveRepoAcpxPluginRoot(currentRoot: string): string | null {
|
||||
return isAcpxPluginRoot(workspaceRoot) ? workspaceRoot : null;
|
||||
}
|
||||
|
||||
function resolveAcpxPluginRootFromOpenClawLayout(moduleUrl: string): string | null {
|
||||
let cursor = path.dirname(fileURLToPath(moduleUrl));
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
const candidates = [
|
||||
path.join(cursor, "extensions", "acpx"),
|
||||
path.join(cursor, "dist", "extensions", "acpx"),
|
||||
path.join(cursor, "dist-runtime", "extensions", "acpx"),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (isAcpxPluginRoot(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
const parent = path.dirname(cursor);
|
||||
if (parent === cursor) {
|
||||
break;
|
||||
}
|
||||
cursor = parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
export function resolveAcpxPluginRoot(moduleUrl: string = import.meta.url): string {
|
||||
const resolvedRoot = resolveNearestAcpxPluginRoot(moduleUrl);
|
||||
// In a live repo checkout, dist/ can be rebuilt out from under the running gateway.
|
||||
@@ -74,6 +95,9 @@ export function resolveAcpxPluginRoot(moduleUrl: string = import.meta.url): stri
|
||||
return (
|
||||
resolveWorkspaceAcpxPluginRoot(resolvedRoot) ??
|
||||
resolveRepoAcpxPluginRoot(resolvedRoot) ??
|
||||
// Shared dist/dist-runtime chunks can load this module outside the plugin tree.
|
||||
// Scan common OpenClaw layouts before falling back to the nearest path guess.
|
||||
resolveAcpxPluginRootFromOpenClawLayout(moduleUrl) ??
|
||||
resolvedRoot
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ RUN --mount=type=cache,id=openclaw-install-sh-e2e-apt-cache,target=/var/cache/ap
|
||||
git
|
||||
|
||||
COPY install-sh-common/version-parse.sh /usr/local/install-sh-common/version-parse.sh
|
||||
COPY --chmod=755 run.sh /usr/local/bin/openclaw-install-e2e
|
||||
COPY --chmod=755 install-sh-e2e/run.sh /usr/local/bin/openclaw-install-e2e
|
||||
|
||||
RUN useradd --create-home --shell /bin/bash appuser
|
||||
USER appuser
|
||||
|
||||
@@ -18,6 +18,12 @@ OPENAI_API_KEY="${OPENAI_API_KEY:-}"
|
||||
ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY:-}"
|
||||
ANTHROPIC_API_TOKEN="${ANTHROPIC_API_TOKEN:-}"
|
||||
|
||||
# This image runs as a non-root user, so seed a user-local npm prefix before we
|
||||
# preinstall an older global version to exercise the upgrade path.
|
||||
export NPM_CONFIG_PREFIX="${NPM_CONFIG_PREFIX:-$HOME/.npm-global}"
|
||||
mkdir -p "$NPM_CONFIG_PREFIX"
|
||||
export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"
|
||||
|
||||
if [[ "$MODELS_MODE" != "both" && "$MODELS_MODE" != "openai" && "$MODELS_MODE" != "anthropic" ]]; then
|
||||
echo "ERROR: OPENCLAW_E2E_MODELS must be one of: both|openai|anthropic" >&2
|
||||
exit 2
|
||||
@@ -186,11 +192,46 @@ run_agent_turn() {
|
||||
local session_id="$2"
|
||||
local prompt="$3"
|
||||
local out_json="$4"
|
||||
# Installer E2E validates install + onboard + embedded agent tooling. It does
|
||||
# not need a paired Gateway control-plane hop, which is flaky/non-deterministic
|
||||
# in the isolated container and already covered by gateway-specific lanes.
|
||||
openclaw --profile "$profile" agent \
|
||||
--local \
|
||||
--session-id "$session_id" \
|
||||
--message "$prompt" \
|
||||
--thinking off \
|
||||
--json >"$out_json"
|
||||
--json >"$out_json" 2>&1
|
||||
node - <<'NODE' "$out_json"
|
||||
const fs = require("node:fs");
|
||||
|
||||
const path = process.argv[2];
|
||||
const raw = fs.readFileSync(path, "utf8");
|
||||
|
||||
function extractTrailingJsonObject(input) {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error("agent output was empty");
|
||||
}
|
||||
try {
|
||||
return JSON.parse(trimmed);
|
||||
} catch {
|
||||
// Some local runs emit stderr diagnostics before the final JSON payload.
|
||||
// Walk backward and keep the last parseable top-level object.
|
||||
for (let index = trimmed.lastIndexOf("{"); index >= 0; index = trimmed.lastIndexOf("{", index - 1)) {
|
||||
const candidate = trimmed.slice(index);
|
||||
try {
|
||||
return JSON.parse(candidate);
|
||||
} catch {
|
||||
// keep scanning
|
||||
}
|
||||
}
|
||||
throw new Error(`could not extract JSON payload from agent output:\n${trimmed}`);
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = extractTrailingJsonObject(raw);
|
||||
fs.writeFileSync(path, `${JSON.stringify(parsed, null, 2)}\n`, "utf8");
|
||||
NODE
|
||||
}
|
||||
|
||||
assert_agent_json_has_text() {
|
||||
@@ -358,6 +399,18 @@ run_profile() {
|
||||
--gateway-bind loopback \
|
||||
--gateway-auth token \
|
||||
--workspace "$workspace" \
|
||||
--skip-health
|
||||
elif [[ -n "$ANTHROPIC_API_KEY" ]]; then
|
||||
openclaw --profile "$profile" onboard \
|
||||
--non-interactive \
|
||||
--accept-risk \
|
||||
--flow quickstart \
|
||||
--auth-choice apiKey \
|
||||
--anthropic-api-key "$ANTHROPIC_API_KEY" \
|
||||
--gateway-port "$port" \
|
||||
--gateway-bind loopback \
|
||||
--gateway-auth token \
|
||||
--workspace "$workspace" \
|
||||
--skip-health
|
||||
elif [[ -n "$ANTHROPIC_API_TOKEN" ]]; then
|
||||
openclaw --profile "$profile" onboard \
|
||||
@@ -425,7 +478,7 @@ run_profile() {
|
||||
IMAGE_PNG="$workspace/proof.png"
|
||||
IMAGE_TXT="$workspace/image.txt"
|
||||
SESSION_ID="e2e-tools-${profile}"
|
||||
SESSION_JSONL="/root/.openclaw-${profile}/agents/main/sessions/${SESSION_ID}.jsonl"
|
||||
SESSION_JSONL="$HOME/.openclaw-${profile}/agents/main/sessions/${SESSION_ID}.jsonl"
|
||||
|
||||
PROOF_VALUE="$(node -e 'console.log(require("node:crypto").randomBytes(16).toString("hex"))')"
|
||||
echo -n "$PROOF_VALUE" >"$PROOF_TXT"
|
||||
@@ -456,11 +509,13 @@ run_profile() {
|
||||
echo "==> Agent turns ($profile)"
|
||||
TURN1_JSON="/tmp/agent-${profile}-1.json"
|
||||
TURN2_JSON="/tmp/agent-${profile}-2.json"
|
||||
TURN2B_JSON="/tmp/agent-${profile}-2b.json"
|
||||
TURN3_JSON="/tmp/agent-${profile}-3.json"
|
||||
TURN3B_JSON="/tmp/agent-${profile}-3b.json"
|
||||
TURN4_JSON="/tmp/agent-${profile}-4.json"
|
||||
|
||||
run_agent_turn "$profile" "$SESSION_ID" \
|
||||
"Use the read tool (not exec) to read proof.txt. Reply with the exact contents only (no extra whitespace)." \
|
||||
"Use the read tool (not exec) to read ${PROOF_TXT}. Reply with the exact contents only (no extra whitespace)." \
|
||||
"$TURN1_JSON"
|
||||
assert_agent_json_has_text "$TURN1_JSON"
|
||||
assert_agent_json_ok "$TURN1_JSON" "$agent_model_provider"
|
||||
@@ -472,7 +527,7 @@ run_profile() {
|
||||
fi
|
||||
|
||||
local prompt2
|
||||
prompt2=$'Use the write tool (not exec) to write exactly this string into copy.txt:\n'"${reply1}"$'\nThen use the read tool (not exec) to read copy.txt and reply with the exact contents only (no extra whitespace).'
|
||||
prompt2=$'Use the write tool (not exec) to write exactly this string into '"${PROOF_COPY}"$':\n'"${reply1}"$'\nReply with exactly: WROTE'
|
||||
run_agent_turn "$profile" "$SESSION_ID" "$prompt2" "$TURN2_JSON"
|
||||
assert_agent_json_has_text "$TURN2_JSON"
|
||||
assert_agent_json_ok "$TURN2_JSON" "$agent_model_provider"
|
||||
@@ -482,25 +537,41 @@ run_profile() {
|
||||
echo "ERROR: copy.txt did not match proof.txt ($profile)" >&2
|
||||
exit 1
|
||||
fi
|
||||
run_agent_turn "$profile" "$SESSION_ID" \
|
||||
"Use the read tool (not exec) to read ${PROOF_COPY}. Reply with the exact contents only (no extra whitespace)." \
|
||||
"$TURN2B_JSON"
|
||||
assert_agent_json_has_text "$TURN2B_JSON"
|
||||
assert_agent_json_ok "$TURN2B_JSON" "$agent_model_provider"
|
||||
local reply2
|
||||
reply2="$(extract_matching_text "$TURN2_JSON" "$PROOF_VALUE" | tr -d '\r\n')"
|
||||
reply2="$(extract_matching_text "$TURN2B_JSON" "$PROOF_VALUE" | tr -d '\r\n')"
|
||||
if [[ "$reply2" != "$PROOF_VALUE" ]]; then
|
||||
echo "ERROR: agent did not read copy.txt correctly ($profile): $reply2" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local prompt3
|
||||
prompt3=$'Use the exec tool to run: cat /etc/hostname\nThen use the write tool to write the exact stdout (trim trailing newline) into hostname.txt. Reply with the hostname only.'
|
||||
run_agent_turn "$profile" "$SESSION_ID" "$prompt3" "$TURN3_JSON"
|
||||
run_agent_turn "$profile" "$SESSION_ID" \
|
||||
"Use the exec tool to run: cat /etc/hostname. Reply with the exact stdout only (trim trailing newline)." \
|
||||
"$TURN3_JSON"
|
||||
assert_agent_json_has_text "$TURN3_JSON"
|
||||
assert_agent_json_ok "$TURN3_JSON" "$agent_model_provider"
|
||||
local reply3
|
||||
reply3="$(extract_matching_text "$TURN3_JSON" "$EXPECTED_HOSTNAME" | tr -d '\r\n')"
|
||||
if [[ "$reply3" != "$EXPECTED_HOSTNAME" ]]; then
|
||||
echo "ERROR: agent did not read /etc/hostname correctly ($profile): $reply3" >&2
|
||||
exit 1
|
||||
fi
|
||||
local prompt3b
|
||||
prompt3b=$'Use the write tool to write exactly this string into '"${HOSTNAME_TXT}"$':\n'"${reply3}"$'\nReply with exactly: WROTE'
|
||||
run_agent_turn "$profile" "$SESSION_ID" "$prompt3b" "$TURN3B_JSON"
|
||||
assert_agent_json_has_text "$TURN3B_JSON"
|
||||
assert_agent_json_ok "$TURN3B_JSON" "$agent_model_provider"
|
||||
if [[ "$(cat "$HOSTNAME_TXT" 2>/dev/null | tr -d '\r\n' || true)" != "$EXPECTED_HOSTNAME" ]]; then
|
||||
echo "ERROR: hostname.txt did not match /etc/hostname ($profile)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
run_agent_turn "$profile" "$SESSION_ID" \
|
||||
"Use the image tool on proof.png. Determine which color is on the left half and which is on the right half. Then use the write tool to write exactly: LEFT=RED RIGHT=GREEN into image.txt. Reply with exactly: LEFT=RED RIGHT=GREEN" \
|
||||
"Use the image tool on ${IMAGE_PNG}. Determine which color is on the left half and which is on the right half. Then use the write tool to write exactly: LEFT=RED RIGHT=GREEN into ${IMAGE_TXT}. Reply with exactly: LEFT=RED RIGHT=GREEN" \
|
||||
"$TURN4_JSON"
|
||||
assert_agent_json_has_text "$TURN4_JSON"
|
||||
assert_agent_json_ok "$TURN4_JSON" "$agent_model_provider"
|
||||
@@ -520,7 +591,7 @@ run_profile() {
|
||||
sleep 1
|
||||
if [[ ! -f "$SESSION_JSONL" ]]; then
|
||||
echo "ERROR: missing session transcript ($profile): $SESSION_JSONL" >&2
|
||||
ls -la "/root/.openclaw-${profile}/agents/main/sessions" >&2 || true
|
||||
ls -la "$HOME/.openclaw-${profile}/agents/main/sessions" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
assert_session_used_tools "$SESSION_JSONL" read write exec image
|
||||
|
||||
@@ -27,7 +27,7 @@ COPY --chown=appuser:appuser scripts/postinstall-bundled-plugins.mjs scripts/npm
|
||||
RUN --mount=type=cache,id=openclaw-pnpm-store,target=/home/appuser/.local/share/pnpm/store,sharing=locked \
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
COPY --chown=appuser:appuser tsconfig.json tsconfig.plugin-sdk.dts.json tsdown.config.ts vitest.config.ts vitest.e2e.config.ts vitest.performance-config.ts vitest.shared.config.ts vitest.bundled-plugin-paths.ts openclaw.mjs ./
|
||||
COPY --chown=appuser:appuser tsconfig.json tsconfig.plugin-sdk.dts.json tsdown.config.ts vitest.config.ts vitest.e2e.config.ts vitest.performance-config.ts vitest.shared.config.ts vitest.system-load.ts vitest.bundled-plugin-paths.ts openclaw.mjs ./
|
||||
COPY --chown=appuser:appuser src ./src
|
||||
COPY --chown=appuser:appuser test ./test
|
||||
COPY --chown=appuser:appuser scripts ./scripts
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
source "$ROOT_DIR/scripts/lib/live-docker-auth.sh"
|
||||
|
||||
IMAGE_NAME="openclaw-openwebui-e2e"
|
||||
OPENWEBUI_IMAGE="${OPENWEBUI_IMAGE:-ghcr.io/open-webui/open-webui:v0.8.10}"
|
||||
PROFILE_FILE="${OPENCLAW_PROFILE_FILE:-$HOME/.profile}"
|
||||
MODEL="${OPENCLAW_OPENWEBUI_MODEL:-openai/gpt-5.4}"
|
||||
# Keep the default on a broadly available non-reasoning OpenAI model for
|
||||
# Open WebUI compatibility smoke. Callers can still override this explicitly.
|
||||
MODEL="${OPENCLAW_OPENWEBUI_MODEL:-openai/gpt-4.1-mini}"
|
||||
PROMPT_NONCE="OPENWEBUI_DOCKER_E2E_$(date +%s)_$$"
|
||||
PROMPT="${OPENCLAW_OPENWEBUI_PROMPT:-Reply with exactly this token and nothing else: ${PROMPT_NONCE}}"
|
||||
PORT="${OPENCLAW_OPENWEBUI_GATEWAY_PORT:-18789}"
|
||||
@@ -19,31 +19,17 @@ NET_NAME="openclaw-openwebui-e2e-$$"
|
||||
GW_NAME="openclaw-openwebui-gateway-$$"
|
||||
OW_NAME="openclaw-openwebui-$$"
|
||||
|
||||
PROFILE_MOUNT=()
|
||||
if [[ -f "$PROFILE_FILE" ]]; then
|
||||
PROFILE_MOUNT=(-v "$PROFILE_FILE":/home/appuser/.profile:ro)
|
||||
OPENAI_API_KEY_VALUE="${OPENAI_API_KEY:-}"
|
||||
if [[ "$OPENAI_API_KEY_VALUE" == "undefined" || "$OPENAI_API_KEY_VALUE" == "null" ]]; then
|
||||
OPENAI_API_KEY_VALUE=""
|
||||
fi
|
||||
|
||||
AUTH_DIRS=()
|
||||
if [[ -n "${OPENCLAW_DOCKER_AUTH_DIRS:-}" ]]; then
|
||||
while IFS= read -r auth_dir; do
|
||||
[[ -n "$auth_dir" ]] || continue
|
||||
AUTH_DIRS+=("$auth_dir")
|
||||
done < <(openclaw_live_collect_auth_dirs)
|
||||
OPENAI_BASE_URL_VALUE="${OPENAI_BASE_URL:-}"
|
||||
if [[ "$OPENAI_BASE_URL_VALUE" == "undefined" || "$OPENAI_BASE_URL_VALUE" == "null" ]]; then
|
||||
OPENAI_BASE_URL_VALUE=""
|
||||
fi
|
||||
AUTH_DIRS_CSV=""
|
||||
if ((${#AUTH_DIRS[@]} > 0)); then
|
||||
AUTH_DIRS_CSV="$(openclaw_live_join_csv "${AUTH_DIRS[@]}")"
|
||||
fi
|
||||
|
||||
EXTERNAL_AUTH_MOUNTS=()
|
||||
if ((${#AUTH_DIRS[@]} > 0)); then
|
||||
for auth_dir in "${AUTH_DIRS[@]}"; do
|
||||
host_path="$HOME/$auth_dir"
|
||||
if [[ -d "$host_path" ]]; then
|
||||
EXTERNAL_AUTH_MOUNTS+=(-v "$host_path":/host-auth/"$auth_dir":ro)
|
||||
fi
|
||||
done
|
||||
if [[ -z "$OPENAI_API_KEY_VALUE" ]]; then
|
||||
echo "OPENAI_API_KEY is required for the Open WebUI Docker smoke." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
cleanup() {
|
||||
@@ -66,34 +52,45 @@ echo "Starting gateway container..."
|
||||
docker run -d \
|
||||
--name "$GW_NAME" \
|
||||
--network "$NET_NAME" \
|
||||
-e "OPENCLAW_DOCKER_AUTH_DIRS_RESOLVED=$AUTH_DIRS_CSV" \
|
||||
-e "OPENCLAW_GATEWAY_TOKEN=$TOKEN" \
|
||||
-e "OPENCLAW_OPENWEBUI_MODEL=$MODEL" \
|
||||
-e "OPENCLAW_SKIP_CHANNELS=1" \
|
||||
-e "OPENCLAW_SKIP_GMAIL_WATCHER=1" \
|
||||
-e "OPENCLAW_SKIP_CRON=1" \
|
||||
-e "OPENCLAW_SKIP_CANVAS_HOST=1" \
|
||||
"${EXTERNAL_AUTH_MOUNTS[@]}" \
|
||||
"${PROFILE_MOUNT[@]}" \
|
||||
-e OPENAI_API_KEY \
|
||||
${OPENAI_BASE_URL_VALUE:+-e OPENAI_BASE_URL} \
|
||||
"$IMAGE_NAME" \
|
||||
bash -lc '
|
||||
set -euo pipefail
|
||||
[ -f "$HOME/.profile" ] && source "$HOME/.profile" || true
|
||||
IFS="," read -r -a auth_dirs <<<"${OPENCLAW_DOCKER_AUTH_DIRS_RESOLVED:-}"
|
||||
if ((${#auth_dirs[@]} > 0)); then
|
||||
for auth_dir in "${auth_dirs[@]}"; do
|
||||
[ -n "$auth_dir" ] || continue
|
||||
if [ -d "/host-auth/$auth_dir" ]; then
|
||||
mkdir -p "$HOME/$auth_dir"
|
||||
cp -R "/host-auth/$auth_dir/." "$HOME/$auth_dir"
|
||||
chmod -R u+rwX "$HOME/$auth_dir" || true
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
entry=dist/index.mjs
|
||||
[ -f "$entry" ] || entry=dist/index.js
|
||||
|
||||
openai_api_key="${OPENAI_API_KEY:?OPENAI_API_KEY required}"
|
||||
node - <<'"'"'NODE'"'"' "$openai_api_key"
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const openaiApiKey = process.argv[2];
|
||||
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
||||
const config = fs.existsSync(configPath)
|
||||
? JSON.parse(fs.readFileSync(configPath, "utf8"))
|
||||
: {};
|
||||
const existingOpenAI = config.models?.providers?.openai ?? {};
|
||||
config.models = {
|
||||
...(config.models || {}),
|
||||
providers: {
|
||||
...(config.models?.providers || {}),
|
||||
openai: {
|
||||
...existingOpenAI,
|
||||
baseUrl:
|
||||
typeof existingOpenAI.baseUrl === "string" && existingOpenAI.baseUrl.trim()
|
||||
? existingOpenAI.baseUrl
|
||||
: process.env.OPENAI_BASE_URL || "https://api.openai.com/v1",
|
||||
apiKey: openaiApiKey,
|
||||
models: Array.isArray(existingOpenAI.models) ? existingOpenAI.models : [],
|
||||
},
|
||||
},
|
||||
};
|
||||
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
|
||||
@@ -176,14 +173,20 @@ if [ "$ow_ready" -ne 1 ]; then
|
||||
fi
|
||||
|
||||
echo "Running Open WebUI -> OpenClaw smoke..."
|
||||
docker exec \
|
||||
if ! docker exec \
|
||||
-e "OPENWEBUI_BASE_URL=http://$OW_NAME:$WEBUI_PORT" \
|
||||
-e "OPENWEBUI_ADMIN_EMAIL=$ADMIN_EMAIL" \
|
||||
-e "OPENWEBUI_ADMIN_PASSWORD=$ADMIN_PASSWORD" \
|
||||
-e "OPENWEBUI_EXPECTED_NONCE=$PROMPT_NONCE" \
|
||||
-e "OPENWEBUI_PROMPT=$PROMPT" \
|
||||
"$GW_NAME" \
|
||||
node /app/scripts/e2e/openwebui-probe.mjs
|
||||
node /app/scripts/e2e/openwebui-probe.mjs; then
|
||||
echo "Open WebUI probe failed; gateway log tail:"
|
||||
docker exec "$GW_NAME" bash -lc 'tail -n 200 /tmp/openwebui-gateway.log' || true
|
||||
echo "Open WebUI container logs:"
|
||||
docker logs "$OW_NAME" 2>&1 | tail -n 200 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Open WebUI container logs:"
|
||||
docker logs "$OW_NAME" 2>&1 | tail -n 80 || true
|
||||
|
||||
@@ -7,8 +7,16 @@ IMAGE_NAME="openclaw-plugins-e2e"
|
||||
echo "Building Docker image..."
|
||||
docker build -t "$IMAGE_NAME" -f "$ROOT_DIR/scripts/e2e/Dockerfile" "$ROOT_DIR"
|
||||
|
||||
DOCKER_ENV_ARGS=(-e COREPACK_ENABLE_DOWNLOAD_PROMPT=0)
|
||||
if [[ -n "${OPENAI_API_KEY:-}" && "${OPENAI_API_KEY:-}" != "undefined" && "${OPENAI_API_KEY:-}" != "null" ]]; then
|
||||
DOCKER_ENV_ARGS+=(-e OPENAI_API_KEY)
|
||||
fi
|
||||
if [[ -n "${OPENAI_BASE_URL:-}" && "${OPENAI_BASE_URL:-}" != "undefined" && "${OPENAI_BASE_URL:-}" != "null" ]]; then
|
||||
DOCKER_ENV_ARGS+=(-e OPENAI_BASE_URL)
|
||||
fi
|
||||
|
||||
echo "Running plugins Docker E2E..."
|
||||
docker run --rm -e COREPACK_ENABLE_DOWNLOAD_PROMPT=0 -e OPENAI_API_KEY -i "$IMAGE_NAME" bash -s <<'EOF'
|
||||
docker run --rm "${DOCKER_ENV_ARGS[@]}" -i "$IMAGE_NAME" bash -s <<'EOF'
|
||||
set -euo pipefail
|
||||
|
||||
if [ -f dist/index.mjs ]; then
|
||||
@@ -22,6 +30,24 @@ else
|
||||
fi
|
||||
export OPENCLAW_ENTRY
|
||||
|
||||
sanitize_env_string() {
|
||||
local value="${1:-}"
|
||||
if [[ "$value" == "undefined" || "$value" == "null" ]]; then
|
||||
printf ''
|
||||
return
|
||||
fi
|
||||
printf '%s' "$value"
|
||||
}
|
||||
|
||||
export OPENAI_API_KEY="$(sanitize_env_string "${OPENAI_API_KEY:-}")"
|
||||
export OPENAI_BASE_URL="$(sanitize_env_string "${OPENAI_BASE_URL:-}")"
|
||||
if [[ -z "$OPENAI_API_KEY" ]]; then
|
||||
unset OPENAI_API_KEY || true
|
||||
fi
|
||||
if [[ -z "$OPENAI_BASE_URL" ]]; then
|
||||
unset OPENAI_BASE_URL || true
|
||||
fi
|
||||
|
||||
home_dir=$(mktemp -d "/tmp/openclaw-plugins-e2e.XXXXXX")
|
||||
export HOME="$home_dir"
|
||||
BUNDLED_PLUGIN_ROOT_DIR="extensions"
|
||||
@@ -29,6 +55,40 @@ OPENCLAW_PLUGIN_HOME="$HOME/.openclaw/$BUNDLED_PLUGIN_ROOT_DIR"
|
||||
|
||||
gateway_pid=""
|
||||
|
||||
seed_openai_provider_config() {
|
||||
local openai_api_key="$1"
|
||||
local openai_base_url="${2:-}"
|
||||
node - <<'NODE' "$openai_api_key" "$openai_base_url"
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const openaiApiKey = process.argv[2];
|
||||
const openaiBaseUrl = process.argv[3];
|
||||
const configPath = path.join(process.env.HOME, ".openclaw", "openclaw.json");
|
||||
const config = fs.existsSync(configPath)
|
||||
? JSON.parse(fs.readFileSync(configPath, "utf8"))
|
||||
: {};
|
||||
const existingOpenAI = config.models?.providers?.openai ?? {};
|
||||
config.models = {
|
||||
...(config.models || {}),
|
||||
providers: {
|
||||
...(config.models?.providers || {}),
|
||||
openai: {
|
||||
...existingOpenAI,
|
||||
baseUrl:
|
||||
typeof existingOpenAI.baseUrl === "string" && existingOpenAI.baseUrl.trim()
|
||||
? existingOpenAI.baseUrl
|
||||
: openaiBaseUrl || "https://api.openai.com/v1",
|
||||
apiKey: openaiApiKey,
|
||||
models: Array.isArray(existingOpenAI.models) ? existingOpenAI.models : [],
|
||||
},
|
||||
},
|
||||
};
|
||||
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
||||
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
||||
NODE
|
||||
}
|
||||
|
||||
stop_gateway() {
|
||||
if [ -n "${gateway_pid:-}" ] && kill -0 "$gateway_pid" 2>/dev/null; then
|
||||
kill "$gateway_pid" 2>/dev/null || true
|
||||
@@ -162,11 +222,12 @@ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
async function main() {
|
||||
const runId = `plugin-e2e-${randomUUID()}`;
|
||||
const sendResult = callGateway("chat.send", {
|
||||
const sendParams = {
|
||||
sessionKey,
|
||||
message,
|
||||
idempotencyKey: runId,
|
||||
});
|
||||
};
|
||||
const sendResult = callGateway("chat.send", sendParams);
|
||||
if (!sendResult.ok) {
|
||||
throw sendResult.error;
|
||||
}
|
||||
@@ -198,9 +259,39 @@ async function main() {
|
||||
);
|
||||
return;
|
||||
}
|
||||
const statusResult = callGateway("chat.send", sendParams);
|
||||
if (statusResult.ok) {
|
||||
const status = statusResult.value;
|
||||
if (status?.status === "error") {
|
||||
const summary =
|
||||
typeof status.summary === "string" && status.summary.trim()
|
||||
? status.summary.trim()
|
||||
: JSON.stringify(status);
|
||||
throw new Error(`gateway run failed for ${sessionKey}: ${summary}`);
|
||||
}
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
|
||||
const finalHistory = callGateway("chat.history", { sessionKey });
|
||||
const finalStatus = callGateway("chat.send", sendParams);
|
||||
fs.writeFileSync(
|
||||
outputFile,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
sessionKey,
|
||||
runId,
|
||||
error: "timeout",
|
||||
history: finalHistory.ok ? finalHistory.value : null,
|
||||
historyError: finalHistory.ok ? null : String(finalHistory.error),
|
||||
status: finalStatus.ok ? finalStatus.value : null,
|
||||
statusError: finalStatus.ok ? null : String(finalStatus.error),
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
throw new Error(`timed out waiting for assistant reply for ${sessionKey}`);
|
||||
}
|
||||
|
||||
@@ -504,7 +595,9 @@ if (process.env.OPENAI_API_KEY) {
|
||||
...(config.agents || {}),
|
||||
defaults: {
|
||||
...(config.agents?.defaults || {}),
|
||||
model: { primary: "openai/gpt-5.4" },
|
||||
// Use the same stable OpenAI family as the installer E2E to avoid
|
||||
// long or reasoning-heavy live turns in this bundle-command smoke.
|
||||
model: { primary: "openai/gpt-4.1-mini" },
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -517,6 +610,10 @@ fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
||||
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
||||
NODE
|
||||
|
||||
if [ -n "${OPENAI_API_KEY:-}" ]; then
|
||||
seed_openai_provider_config "$OPENAI_API_KEY" "${OPENAI_BASE_URL:-}"
|
||||
fi
|
||||
|
||||
gateway_log="/tmp/openclaw-plugin-command-e2e.log"
|
||||
start_gateway "$gateway_log"
|
||||
wait_for_gateway_health
|
||||
@@ -630,11 +727,17 @@ NODE
|
||||
|
||||
if [ -n "${OPENAI_API_KEY:-}" ]; then
|
||||
echo "Testing Claude bundle command invocation..."
|
||||
run_gateway_chat_json \
|
||||
if ! run_gateway_chat_json \
|
||||
"plugin-e2e-live" \
|
||||
"/office_hours Reply with exactly BUNDLE_OK and nothing else." \
|
||||
/tmp/plugin-command-live.json \
|
||||
60000
|
||||
120000; then
|
||||
echo "Claude bundle command invocation failed; payload dump:"
|
||||
cat /tmp/plugin-command-live.json 2>/dev/null || true
|
||||
echo "Gateway log tail:"
|
||||
tail -n 200 "$gateway_log" || true
|
||||
exit 1
|
||||
fi
|
||||
node - <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const payload = JSON.parse(fs.readFileSync("/tmp/plugin-command-live.json", "utf8"));
|
||||
|
||||
@@ -14,7 +14,7 @@ echo "==> Build image: $IMAGE_NAME"
|
||||
docker build \
|
||||
-t "$IMAGE_NAME" \
|
||||
-f "$ROOT_DIR/scripts/docker/install-sh-e2e/Dockerfile" \
|
||||
"$ROOT_DIR/scripts/docker/install-sh-e2e"
|
||||
"$ROOT_DIR/scripts/docker"
|
||||
|
||||
echo "==> Run E2E installer test"
|
||||
docker run --rm \
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { captureEnv } from "../test-utils/env.js";
|
||||
import {
|
||||
@@ -9,7 +9,18 @@ import {
|
||||
writeClaudeBundle,
|
||||
writeFakeClaudeCli,
|
||||
} from "./bundle-mcp.test-harness.js";
|
||||
import { runCliAgent } from "./cli-runner.js";
|
||||
|
||||
vi.mock("./cli-runner/helpers.js", async () => {
|
||||
const original =
|
||||
await vi.importActual<typeof import("./cli-runner/helpers.js")>("./cli-runner/helpers.js");
|
||||
return {
|
||||
...original,
|
||||
// This e2e only validates bundle MCP wiring into the spawned CLI backend.
|
||||
// Stub the large prompt-construction path so cold Vitest workers do not
|
||||
// time out before the actual MCP roundtrip runs.
|
||||
buildSystemPrompt: () => "Bundle MCP e2e test prompt.",
|
||||
};
|
||||
});
|
||||
|
||||
// This e2e spins a real stdio MCP server plus a spawned CLI process, which is
|
||||
// notably slower under Docker and cold Vitest imports.
|
||||
@@ -20,6 +31,7 @@ describe("runCliAgent bundle MCP e2e", () => {
|
||||
"routes enabled bundle MCP config into the claude-cli backend and executes the tool",
|
||||
{ timeout: E2E_TIMEOUT_MS },
|
||||
async () => {
|
||||
const { runCliAgent } = await import("./cli-runner.js");
|
||||
const envSnapshot = captureEnv(["HOME"]);
|
||||
const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-cli-bundle-mcp-"));
|
||||
process.env.HOME = tempHome;
|
||||
|
||||
Reference in New Issue
Block a user