diff --git a/.agents/skills/nemoclaw-user-reference/references/commands.md b/.agents/skills/nemoclaw-user-reference/references/commands.md index 9abf55335c..7be3cff477 100644 --- a/.agents/skills/nemoclaw-user-reference/references/commands.md +++ b/.agents/skills/nemoclaw-user-reference/references/commands.md @@ -376,7 +376,7 @@ Existing sandboxes do not auto-upgrade when a newer NemoClaw release ships a new ```console $ nemoclaw my-assistant status ... - Agent: OpenClaw v2026.5.18 + Agent: OpenClaw v2026.4.24 ... ``` diff --git a/.github/workflows/regression-e2e.yaml b/.github/workflows/regression-e2e.yaml index b3d4bf9e4a..d6340d124e 100644 --- a/.github/workflows/regression-e2e.yaml +++ b/.github/workflows/regression-e2e.yaml @@ -175,8 +175,8 @@ jobs: # ── OpenShell version-pin E2E ────────────────────────────── - # Coverage guard for #3474. If a host has sticky OpenShell 0.0.45 on PATH - # but this NemoClaw release supports only <=0.0.44, install-openshell.sh + # Coverage guard for #3474. If a host has sticky OpenShell 0.0.40 on PATH + # but this NemoClaw release supports only <=0.0.39, install-openshell.sh # must replace it with the pinned compatible release instead of hard-failing. openshell-version-pin-e2e: needs: select_regression_jobs diff --git a/Dockerfile b/Dockerfile index 9ea4337eca..cba29fb48d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -204,12 +204,12 @@ RUN set -eu; \ sed -i 's/const baseLstat = await fs\.lstat(params\.installBaseDir)/const baseLstat = await fs.stat(params.installBaseDir)/' "$ipd_file"; \ sed -i 's/baseLstat\.isSymbolicLink()/false \/* nemoclaw: symlink check disabled, realpath guards containment *\//' "$ipd_file"; \ if grep -q 'fs\.lstat(params\.installBaseDir)' "$ipd_file"; then echo "ERROR: Patch 3b (install-package-dir) left lstat in assertInstallBaseStable" >&2; exit 1; fi; \ - # --- Patch 5: bump default WS handshake timeout to 60s (#2484) --- \ - # OpenClaw's WS connect handshake has a hard-coded short timeout on both \ + # --- Patch 5: bump default WS handshake timeout 10s -> 60s (#2484) --- \ + # OpenClaw's WS connect handshake has a hard-coded 10s timeout on both \ # client and server. Server-side connect-handler processing can exceed \ - # that limit under load (multiple concurrent connects on slow CI infra), \ + # 10s under load (multiple concurrent connects on slow CI infra), \ # causing `openclaw agent --json` to fail with "gateway timeout after \ - # ms" and TC-SBX-02 to hit its 90s SSH timeout. \ + # 10000ms" and TC-SBX-02 to hit its 90s SSH timeout. \ # \ # Both env vars (OPENCLAW_HANDSHAKE_TIMEOUT_MS, \ # OPENCLAW_CONNECT_CHALLENGE_TIMEOUT_MS) are clamped at the same \ @@ -219,12 +219,12 @@ RUN set -eu; \ # \ # Removal criteria: drop when openclaw fixes the underlying connect \ # latency, or exposes the timeout as an unbounded env override. \ - hto_files="$(grep -RIlE --include='*.js' 'DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS = (1e4|15e3)' "$OC_DIST")"; \ + hto_files="$(grep -RIlE --include='*.js' 'DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS = 1e4' "$OC_DIST")"; \ test -n "$hto_files" || { echo "ERROR: handshake-timeout constant not found" >&2; exit 1; }; \ - printf '%s\n' "$hto_files" | xargs sed -i -E 's#DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS = (1e4|15e3)#DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS = 6e4#g'; \ - if grep -REq --include='*.js' 'DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS = (1e4|15e3)' "$OC_DIST"; then echo "ERROR: Patch 5 left a short timeout constant" >&2; exit 1; fi + printf '%s\n' "$hto_files" | xargs sed -i -E 's|DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS = 1e4|DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS = 6e4|g'; \ + if grep -REq --include='*.js' 'DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS = 1e4' "$OC_DIST"; then echo "ERROR: Patch 5 left a 1e4 constant" >&2; exit 1; fi -# Patch OpenClaw's pinned 2026.5.18 compiled selection runtime to expose a +# Patch OpenClaw's pinned 2026.4.24 compiled selection runtime to expose a # compact searchable tool catalog to the model while preserving the full # effective tool set behind tool_call. NEMOCLAW_TOOL_CATALOG=0 disables this # wrapper if an emergency rollback is needed. The script fails closed if the @@ -358,7 +358,6 @@ ENV NEMOCLAW_MODEL=${NEMOCLAW_MODEL} \ NEMOCLAW_DISCORD_GUILDS_B64=${NEMOCLAW_DISCORD_GUILDS_B64} \ NEMOCLAW_TELEGRAM_CONFIG_B64=${NEMOCLAW_TELEGRAM_CONFIG_B64} \ NEMOCLAW_WECHAT_CONFIG_B64=${NEMOCLAW_WECHAT_CONFIG_B64} \ - NEMOCLAW_OPENCLAW_WECHAT_PLUGIN_PREINSTALLED=1 \ NEMOCLAW_DISABLE_DEVICE_AUTH=${NEMOCLAW_DISABLE_DEVICE_AUTH} \ NEMOCLAW_PROXY_HOST=${NEMOCLAW_PROXY_HOST} \ NEMOCLAW_PROXY_PORT=${NEMOCLAW_PROXY_PORT} \ diff --git a/Dockerfile.base b/Dockerfile.base index 70ca4c6c07..4d920cb614 100644 --- a/Dockerfile.base +++ b/Dockerfile.base @@ -177,7 +177,7 @@ RUN printf '%s\n' \ # OpenClaw version: change the OPENCLAW_VERSION ARG default so CI rebuilds # the base image on push to main, or use workflow_dispatch on base-image.yaml # with the openclaw_version input for a one-off build without editing this file. -ARG OPENCLAW_VERSION=2026.5.18 +ARG OPENCLAW_VERSION=2026.4.24 SHELL ["/bin/bash", "-o", "pipefail", "-c"] @@ -204,7 +204,7 @@ RUN --mount=type=bind,source=nemoclaw-blueprint/blueprint.yaml,target=/tmp/bluep USER sandbox WORKDIR /sandbox # hadolint ignore=DL3059,DL4006 -RUN openclaw plugins install '@tencent-weixin/openclaw-weixin@2.4.3' --pin \ +RUN openclaw plugins install '@tencent-weixin/openclaw-weixin@2.4.2' --pin \ && openclaw config set plugins.entries.openclaw-weixin.enabled true # hadolint ignore=DL3002 USER root diff --git a/agents/hermes/Dockerfile.base b/agents/hermes/Dockerfile.base index 604dc37401..575f6b3b8f 100644 --- a/agents/hermes/Dockerfile.base +++ b/agents/hermes/Dockerfile.base @@ -23,9 +23,9 @@ FROM node:22-trixie-slim@sha256:2d9f5c76c8f4dd36e8f253bee5d828a83a6c09f36188f0b0 ENV DEBIAN_FRONTEND=noninteractive # Hermes version pinned for reproducibility. -# Calver tag v2026.5.16 = Hermes Agent v0.14.0. -ARG HERMES_VERSION=v2026.5.16 -ARG HERMES_TARBALL_SHA256=c0a554050a50ee9a62f3fa5cd288a167ba5640c42d647d100cdea084b7294143 +# Calver tag v2026.4.23 = semver 0.11.0. +ARG HERMES_VERSION=v2026.4.23 +ARG HERMES_TARBALL_SHA256=1ee1be80a2112b7edc581770cee8858e725ba110cc423979cd7102492504bc6b ARG HERMES_UV_EXTRAS="messaging web" ARG UV_VERSION=0.11.8 diff --git a/agents/hermes/config/hermes-config.ts b/agents/hermes/config/hermes-config.ts index 5e634bbf15..fc1f71c2a1 100644 --- a/agents/hermes/config/hermes-config.ts +++ b/agents/hermes/config/hermes-config.ts @@ -62,7 +62,7 @@ export function buildHermesConfig(settings: HermesBuildSettings): Record binary_path: /usr/local/bin/openclaw version_command: "openclaw --version" -expected_version: "2026.5.18" +expected_version: "2026.4.24" gateway_command: "openclaw gateway run" # ── Health probe ──────────────────────────────────────────────── diff --git a/docs/reference/commands.mdx b/docs/reference/commands.mdx index e3c85fb8c7..8c1028330c 100644 --- a/docs/reference/commands.mdx +++ b/docs/reference/commands.mdx @@ -383,7 +383,7 @@ Existing sandboxes do not auto-upgrade when a newer NemoClaw release ships a new ```console $ nemoclaw my-assistant status ... - Agent: OpenClaw v2026.5.18 + Agent: OpenClaw v2026.4.24 ... ``` diff --git a/nemoclaw-blueprint/blueprint.yaml b/nemoclaw-blueprint/blueprint.yaml index 9faae92a98..2f184c8516 100644 --- a/nemoclaw-blueprint/blueprint.yaml +++ b/nemoclaw-blueprint/blueprint.yaml @@ -2,9 +2,9 @@ # SPDX-License-Identifier: Apache-2.0 version: "0.1.0" -min_openshell_version: "0.0.44" -max_openshell_version: "0.0.44" -min_openclaw_version: "2026.5.18" +min_openshell_version: "0.0.39" +max_openshell_version: "0.0.39" +min_openclaw_version: "2026.4.24" # Mirrors the components.sandbox.image manifest digest below. Lets a # downstream consumer (or release tooling) verify the blueprint declares # a specific sandbox image without parsing the components tree, and diff --git a/nemoclaw-blueprint/openclaw-plugins/kimi-inference-compat/index.js b/nemoclaw-blueprint/openclaw-plugins/kimi-inference-compat/index.js index 5e45d3c093..b61bae3df5 100644 --- a/nemoclaw-blueprint/openclaw-plugins/kimi-inference-compat/index.js +++ b/nemoclaw-blueprint/openclaw-plugins/kimi-inference-compat/index.js @@ -136,11 +136,6 @@ function decodeToolCallArguments(value) { return null; } -function encodeToolCallArgumentsLike(original, command) { - if (typeof original === "string") return JSON.stringify({ command }); - return { command }; -} - function splitSafeExecCommand(command) { if (typeof command !== "string") return null; if (!command.includes(";")) return null; @@ -156,7 +151,12 @@ function buildSplitToolCallId(originalId, index, command) { return `${baseId}_split_${index + 1}_${command}`; } -function getSafeCombinedExecToolCallFromBlock(toolCall) { +function getSafeCombinedExecToolCall(message) { + if (!message || typeof message !== "object") return null; + const content = message.content; + if (!Array.isArray(content) || content.length !== 1) return null; + + const toolCall = content[0]; if (!toolCall || typeof toolCall !== "object" || Array.isArray(toolCall)) return null; if (toolCall.type !== "toolCall" || toolCall.name !== "exec") return null; @@ -173,103 +173,27 @@ function getSafeCombinedExecToolCallFromBlock(toolCall) { return { commands, toolCall }; } -function getExecToolCallCommand(toolCall) { - if (!toolCall || typeof toolCall !== "object" || Array.isArray(toolCall)) return null; - if (toolCall.type !== "toolCall" || toolCall.name !== "exec") return null; - const args = decodeToolCallArguments(toolCall.arguments); - if (!args) return null; - const argKeys = Object.keys(args); - if (argKeys.length !== 1 || argKeys[0] !== "command" || typeof args.command !== "string") { - return null; - } - return args.command; -} +function applySafeExecSplitToMessage(message, split) { + if (!message || typeof message !== "object" || !split) return false; + const { commands, toolCall } = split; -function buildSplitToolCalls(toolCall, commands) { - return commands.map((command, index) => ({ + message.content = commands.map((command, index) => ({ type: "toolCall", id: buildSplitToolCallId(toolCall.id, index, command), name: "exec", arguments: { command }, })); -} - -function dedupeSafeExecToolCalls(content) { - const seenSafeExecCommands = new Set(); - const deduped = []; - for (const block of content) { - const command = getExecToolCallCommand(block); - if (SAFE_SPLIT_EXEC_COMMANDS.has(command)) { - if (seenSafeExecCommands.has(command)) continue; - seenSafeExecCommands.add(command); - } - deduped.push(block); - } - return deduped; -} - -function rewriteSafeCombinedExecToolCallsInContent(content) { - if (!Array.isArray(content)) return { changed: false, content }; - - let changed = false; - const expanded = []; - for (const block of content) { - const split = getSafeCombinedExecToolCallFromBlock(block); - if (split) { - expanded.push(...buildSplitToolCalls(split.toolCall, split.commands)); - changed = true; - } else { - expanded.push(block); - } - } - if (!changed) return { changed: false, content }; - - return { changed: true, content: dedupeSafeExecToolCalls(expanded) }; -} - -function applySafeExecSplitToMessage(message) { - if (!message || typeof message !== "object") return false; - const rewritten = rewriteSafeCombinedExecToolCallsInContent(message.content); - if (!rewritten.changed) return false; - message.content = rewritten.content; - if (message.stopReason === "stop") message.stopReason = "toolUse"; - return true; -} - -function applySafeExecSplitAtContentIndex(message, split) { - if (!message || typeof message !== "object" || !Array.isArray(message.content) || !split) { - return false; - } - const index = Number.isInteger(split.contentIndex) ? split.contentIndex : 0; - if (index < 0 || index >= message.content.length) return false; - const replacement = buildSplitToolCalls(split.toolCall, split.commands); - message.content = dedupeSafeExecToolCalls([ - ...message.content.slice(0, index), - ...replacement, - ...message.content.slice(index + 1), - ]); if (message.stopReason === "stop") message.stopReason = "toolUse"; return true; } -function targetSplitCommandIndex(event, split) { - const rawIndex = Number.isInteger(event && event.contentIndex) ? event.contentIndex : 0; - const fallbackIndex = Math.min(Math.max(rawIndex, 0), split.commands.length - 1); - const content = event && event.partial && Array.isArray(event.partial.content) - ? event.partial.content - : []; - const commandAtContentIndex = getExecToolCallCommand(content[rawIndex]); - const commandIndex = split.commands.findIndex((command) => command === commandAtContentIndex); - return commandIndex >= 0 ? commandIndex : fallbackIndex; -} - function rewriteSafeCombinedExecToolCallInMessage(message) { - return applySafeExecSplitToMessage(message); + return applySafeExecSplitToMessage(message, getSafeCombinedExecToolCall(message)); } function getSafeCombinedExecToolCallFromEventDelta(event) { if (!event || typeof event !== "object") return null; - if (event.type !== "toolcall_delta") return null; + if (event.type !== "toolcall_delta" || typeof event.delta !== "string") return null; const partial = event.partial; if (!partial || typeof partial !== "object" || !Array.isArray(partial.content)) return null; const index = Number.isInteger(event.contentIndex) ? event.contentIndex : 0; @@ -286,31 +210,33 @@ function getSafeCombinedExecToolCallFromEventDelta(event) { const commands = splitSafeExecCommand(args.command); if (!commands) return null; - return { commands, toolCall, contentIndex: index }; + return { commands, toolCall }; } function rewriteSafeCombinedExecToolCallInEvent(event) { if (!event || typeof event !== "object") return false; - const deltaSplit = getSafeCombinedExecToolCallFromEventDelta(event); - let changed = false; + const split = + getSafeCombinedExecToolCall(event.partial) || + getSafeCombinedExecToolCall(event.message) || + getSafeCombinedExecToolCallFromEventDelta(event); + if (!split) return false; - const partialChanged = applySafeExecSplitToMessage(event.partial); - const messageChanged = applySafeExecSplitToMessage(event.message); - changed = partialChanged || messageChanged; - - if (deltaSplit) { - if (!partialChanged) changed = applySafeExecSplitAtContentIndex(event.partial, deltaSplit) || changed; - if (!messageChanged) changed = applySafeExecSplitAtContentIndex(event.message, deltaSplit) || changed; - changed = true; - const targetIndex = targetSplitCommandIndex(event, deltaSplit); - const targetCommand = deltaSplit.commands[targetIndex]; - event.delta = encodeToolCallArgumentsLike(event.delta, targetCommand); - if (event.toolCall && typeof event.toolCall === "object" && !Array.isArray(event.toolCall)) { - event.toolCall = buildSplitToolCalls(deltaSplit.toolCall, deltaSplit.commands)[targetIndex]; - } + applySafeExecSplitToMessage(event.partial, split); + applySafeExecSplitToMessage(event.message, split); + + if (event.type === "toolcall_delta" && typeof event.delta === "string") { + event.delta = JSON.stringify({ command: split.commands[0] }); + } + if (event.toolCall && typeof event.toolCall === "object" && !Array.isArray(event.toolCall)) { + event.toolCall = { + type: "toolCall", + id: buildSplitToolCallId(split.toolCall.id, 0, split.commands[0]), + name: "exec", + arguments: { command: split.commands[0] }, + }; } - return changed; + return true; } function wrapStreamFinalMessages(stream) { diff --git a/nemoclaw/package.json b/nemoclaw/package.json index f00d35efba..9f47b6b06a 100644 --- a/nemoclaw/package.json +++ b/nemoclaw/package.json @@ -11,11 +11,11 @@ "./dist/index.js" ], "compat": { - "pluginApi": ">=2026.5.18", - "minGatewayVersion": "2026.5.18" + "pluginApi": ">=2026.4.24", + "minGatewayVersion": "2026.4.24" }, "build": { - "openclawVersion": "2026.5.18" + "openclawVersion": "2026.4.24" } }, "scripts": { diff --git a/nemoclaw/src/package-metadata.test.ts b/nemoclaw/src/package-metadata.test.ts index 5485993de8..6c3d27fcdd 100644 --- a/nemoclaw/src/package-metadata.test.ts +++ b/nemoclaw/src/package-metadata.test.ts @@ -20,8 +20,8 @@ const packageJson = JSON.parse( describe("OpenClaw package metadata", () => { it("declares the required external plugin compatibility fields", () => { - expect(packageJson.openclaw?.compat?.pluginApi).toBe(">=2026.5.18"); - expect(packageJson.openclaw?.compat?.minGatewayVersion).toBe("2026.5.18"); - expect(packageJson.openclaw?.build?.openclawVersion).toBe("2026.5.18"); + expect(packageJson.openclaw?.compat?.pluginApi).toBe(">=2026.4.24"); + expect(packageJson.openclaw?.compat?.minGatewayVersion).toBe("2026.4.24"); + expect(packageJson.openclaw?.build?.openclawVersion).toBe("2026.4.24"); }); }); diff --git a/scripts/brev-launchable-ci-cpu.sh b/scripts/brev-launchable-ci-cpu.sh index cbd305b354..f28bb352b1 100755 --- a/scripts/brev-launchable-ci-cpu.sh +++ b/scripts/brev-launchable-ci-cpu.sh @@ -28,7 +28,7 @@ # curl -fsSL https://raw.githubusercontent.com/NVIDIA/NemoClaw//scripts/brev-launchable-ci-cpu.sh | bash # # Environment overrides: -# OPENSHELL_VERSION — OpenShell CLI release tag (default: v0.0.44) +# OPENSHELL_VERSION — OpenShell CLI release tag (default: v0.0.39) # NEMOCLAW_REF — NemoClaw git ref to clone (default: main) # NEMOCLAW_CLONE_DIR — Where to clone NemoClaw (default: ~/NemoClaw) # SKIP_DOCKER_PULL — Set to 1 to skip Docker image pre-pulls @@ -40,7 +40,7 @@ set -euo pipefail # ── Configuration ──────────────────────────────────────────────────── -OPENSHELL_VERSION="${OPENSHELL_VERSION:-v0.0.44}" +OPENSHELL_VERSION="${OPENSHELL_VERSION:-v0.0.39}" NEMOCLAW_REF="${NEMOCLAW_REF:-main}" TARGET_USER="${SUDO_USER:-$(id -un)}" TARGET_HOME="$(getent passwd "$TARGET_USER" | cut -d: -f6)" @@ -250,7 +250,7 @@ DOCKER_PULL_PID="" if [[ "${SKIP_DOCKER_PULL:-0}" != "1" ]]; then info "Pre-pulling Docker images in background..." ( - SUPERVISOR_TAG="${OPENSHELL_VERSION#v}" # v0.0.44 -> 0.0.44 + SUPERVISOR_TAG="${OPENSHELL_VERSION#v}" # v0.0.39 -> 0.0.39 SUPERVISOR_IMAGE="ghcr.io/nvidia/openshell/supervisor:${SUPERVISOR_TAG}" # Pull all images in parallel diff --git a/scripts/generate-openclaw-config.py b/scripts/generate-openclaw-config.py index 4686f6d793..0e3e3456d6 100755 --- a/scripts/generate-openclaw-config.py +++ b/scripts/generate-openclaw-config.py @@ -465,8 +465,8 @@ def build_config(env: dict | None = None) -> dict: ) # NEMOCLAW_WECHAT_CONFIG_B64 is intentionally not decoded here. The # WeChat plugin's per-account state (accountId/baseUrl/userId) is read by - # seed-wechat-accounts.py, which runs after the base image has installed - # the WeChat plugin and registered its metadata/channel id. + # seed-wechat-accounts.py, which the Dockerfile invokes separately after + # `openclaw plugins install` registers the openclaw-weixin channel id. # Decoding it here too would create a misleading second consumer that # nothing acts on. @@ -532,12 +532,12 @@ def _placeholder(channel: str, env_key: str) -> str: } _ch_cfg[ch] = {"accounts": {"default": account}} - # WeChat (openclaw-weixin) is NOT added to channels.* here in build - # contexts where the plugin has not been installed yet — writing it upfront - # makes `openclaw plugins install` fail with "unknown channel id: - # openclaw-weixin" because the plugin registry hasn't seen the channel yet - # (chicken-and-egg). When the base image has already installed the plugin, - # scripts/seed-wechat-accounts.py adds: + # WeChat (openclaw-weixin) is NOT added to channels.* here — writing + # channels.openclaw-weixin upfront makes `openclaw plugins install` fail + # with "unknown channel id: openclaw-weixin" because the plugin registry + # hasn't seen the channel yet (chicken-and-egg). The block is written + # AFTER `openclaw plugins install` runs, by scripts/seed-wechat-accounts.py, + # which adds: # channels.openclaw-weixin.channelConfigUpdatedAt = # channels.openclaw-weixin.accounts..enabled = true # The upstream plugin's auth/accounts.ts reads that block at boot to @@ -674,7 +674,7 @@ def _placeholder(channel: str, env_key: str) -> str: ), # NemoClaw sandboxes are provisioned non-interactively and the # E2E CLI contract expects the first agent turn to answer the - # caller's prompt. OpenClaw 2026.4.24+ seeds BOOTSTRAP.md by + # caller's prompt. OpenClaw 2026.4.24 seeds BOOTSTRAP.md by # default, which redirects a fresh workspace into an identity # setup conversation before normal replies. "skipBootstrap": True, @@ -701,7 +701,7 @@ def _placeholder(channel: str, env_key: str) -> str: # load. The sandbox L7 proxy denies the registry URL, the # install retries for ~6 minutes, and while it's stuck the # gateway can't service openclaw-agent requests — that's the - # TC-SBX-02 hang observed in 2026.4.24. + # TC-SBX-02 hang in 2026.4.24. # # acpx is disabled by default because its runtime dependency staging # also reaches npm during gateway startup. NemoClaw's primary CLI path @@ -770,52 +770,8 @@ def _has_plugin_install(config: dict, plugin_id: str) -> bool: return isinstance(installs, dict) and plugin_id in installs -def _has_installed_wechat_plugin_metadata() -> bool: - extensions_dir = Path(os.path.expanduser("~/.openclaw/extensions")) - if not extensions_dir.exists(): - return False - - for root, dirs, files in os.walk(extensions_dir): - dirs[:] = [ - item - for item in dirs - if item not in {"node_modules", "plugin-runtime-deps", ".git"} - ] - root_path = Path(root) - for filename in files: - if filename not in {"openclaw.plugin.json", "package.json"}: - continue - path = root_path / filename - try: - metadata = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - continue - if not isinstance(metadata, dict): - continue - if ( - metadata.get("id") == "openclaw-weixin" - or metadata.get("name") == "@tencent-weixin/openclaw-weixin" - or "openclaw-weixin" in str(path).lower() - ): - return True - return False - - -def _has_preinstalled_wechat_plugin_signal() -> bool: - return os.environ.get("NEMOCLAW_OPENCLAW_WECHAT_PLUGIN_PREINSTALLED", "").strip().lower() in { - "1", - "true", - "yes", - "on", - } - - -def _seed_wechat_accounts_if_available(config: dict) -> None: - if ( - not _has_plugin_install(config, "openclaw-weixin") - and not _has_installed_wechat_plugin_metadata() - and not _has_preinstalled_wechat_plugin_signal() - ): +def _seed_wechat_accounts_if_installed(config: dict) -> None: + if not _has_plugin_install(config, "openclaw-weixin"): return seed_script = Path(__file__).resolve().with_name("seed-wechat-accounts.py") @@ -837,7 +793,7 @@ def main() -> None: with open(path, "w") as f: json.dump(config, f, indent=2) os.chmod(path, 0o600) - _seed_wechat_accounts_if_available(config) + _seed_wechat_accounts_if_installed(config) if __name__ == "__main__": diff --git a/scripts/install-openshell.sh b/scripts/install-openshell.sh index 61f19b0cfe..cfea450ea8 100755 --- a/scripts/install-openshell.sh +++ b/scripts/install-openshell.sh @@ -35,16 +35,16 @@ info "Detected $OS_LABEL ($ARCH_LABEL)" # Minimum version required for native messaging credential rewrite: # WebSocket text frames plus provider-shaped aliases and REST request bodies. -MIN_VERSION="0.0.44" +MIN_VERSION="0.0.39" # Maximum version validated for this NemoClaw release. Newer OpenShell builds # may change sandbox semantics; upgrade NemoClaw before upgrading past this. -MAX_VERSION="0.0.44" +MAX_VERSION="0.0.39" # Pin fresh installs to this version. The TS installer normally overrides this # via NEMOCLAW_OPENSHELL_PIN_VERSION after resolving the highest published # OpenShell release that satisfies the blueprint's max_openshell_version # (see #3404). The hardcoded value is the fallback for offline runs. PIN_VERSION="$MAX_VERSION" -DEV_MIN_VERSION="0.0.44" +DEV_MIN_VERSION="0.0.39" CHANNEL="${NEMOCLAW_OPENSHELL_CHANNEL:-auto}" case "$CHANNEL" in diff --git a/scripts/patch-openclaw-tool-catalog.js b/scripts/patch-openclaw-tool-catalog.js index 09b02b34d2..0d9f2c01ab 100755 --- a/scripts/patch-openclaw-tool-catalog.js +++ b/scripts/patch-openclaw-tool-catalog.js @@ -31,13 +31,6 @@ const ALREADY_PATCHED_REQUIRED_PATTERNS = [ "\t\t\tconst nemoClawCatalogSourceTools = [...customTools, ...clientToolDefs];", "\t\t\tconst allCustomTools = nemoClawCreateToolCatalog(nemoClawCatalogSourceTools);", ]; -const NATIVE_TOOL_SEARCH_PATTERNS = [ - "const uncompactedEffectiveTools = [...tools, ...filteredBundledTools];", - "applyToolSearchCatalog({", - "buildToolSearchRunPlan({", - "const allowedToolNames = toolSearchRunPlan.visibleAllowedToolNames;", - "const replayAllowedToolNames = toolSearchRunPlan.replayAllowedToolNames;", -]; const EFFECTIVE_TOOLS_REPLACEMENT = [ EFFECTIVE_TOOLS_PATTERN, @@ -233,10 +226,6 @@ function patchSelectionText(source, filePath) { return { patched: false, text: source }; } - if (NATIVE_TOOL_SEARCH_PATTERNS.every((pattern) => source.includes(pattern))) { - return { patched: false, text: source, status: "native-tool-search" }; - } - const requiredPatterns = [ EFFECTIVE_TOOLS_PATTERN, ALLOWED_TOOL_NAMES_PATTERN, @@ -272,11 +261,7 @@ function patchOpenClawToolCatalog(distDir) { const targetFiles = selectionFiles.filter((file) => { const text = fs.readFileSync(file, "utf-8"); - return ( - text.includes(ALL_CUSTOM_TOOLS_PATTERN) || - text.includes(MARKER) || - NATIVE_TOOL_SEARCH_PATTERNS.every((pattern) => text.includes(pattern)) - ); + return text.includes(ALL_CUSTOM_TOOLS_PATTERN) || text.includes(MARKER); }); if (targetFiles.length !== 1) { throw new Error(`Expected exactly one selection-*.js target, found ${targetFiles.length}`); @@ -284,13 +269,12 @@ function patchOpenClawToolCatalog(distDir) { const target = targetFiles[0]; const source = fs.readFileSync(target, "utf-8"); - const result = patchSelectionText(source, target); - const { patched, text } = result; + const { patched, text } = patchSelectionText(source, target); if (patched) { fs.writeFileSync(target, text); return { status: "patched", file: target, version }; } - return { status: result.status ?? "already-patched", file: target, version }; + return { status: "already-patched", file: target, version }; } function main(argv) { diff --git a/scripts/seed-wechat-accounts.py b/scripts/seed-wechat-accounts.py index cddd264ec9..5c4935c2e4 100755 --- a/scripts/seed-wechat-accounts.py +++ b/scripts/seed-wechat-accounts.py @@ -10,7 +10,7 @@ # would otherwise drive an in-sandbox QR scan that has no terminal and no # paired phone access. # -# Files written (matching auth/accounts.ts in @tencent-weixin/openclaw-weixin@2.4.3): +# Files written (matching auth/accounts.ts in @tencent-weixin/openclaw-weixin@2.4.2): # /openclaw-weixin/accounts.json — JSON array of accountIds # /openclaw-weixin/accounts/.json — { token, savedAt, baseUrl, userId } # /openclaw.json (channels.openclaw-weixin) — registered channel + accounts..enabled @@ -21,9 +21,7 @@ # disabled and the bridge won't start, even if the per-account state files # above exist. The patch also restores the openclaw-weixin plugin registry # entry because later OpenClaw config rewrites can drop it while leaving the -# pre-installed extension files in place. generate-openclaw-config.py invokes -# this only after the base image's installed plugin metadata, install registry, -# or preinstalled-plugin signal proves OpenClaw knows the WeChat channel id. +# pre-installed extension files in place. # # State dir resolution mirrors the upstream's resolveStateDir(): # $OPENCLAW_STATE_DIR || $CLAWDBOT_STATE_DIR || ~/.openclaw @@ -54,18 +52,15 @@ import os import pathlib import sys -from collections.abc import Iterable -WECHAT_TOKEN_PLACEHOLDER = "openshell:resolve:env:WECHAT_BOT_TOKEN" WECHAT_PLUGIN_ID = "openclaw-weixin" -WECHAT_PLUGIN_PACKAGE = "@tencent-weixin/openclaw-weixin" -WECHAT_PLUGIN_SPEC = f"{WECHAT_PLUGIN_PACKAGE}@2.4.3" +WECHAT_PLUGIN_SPEC = "@tencent-weixin/openclaw-weixin@2.4.2" WECHAT_PLUGIN_INSTALL = { "source": "npm", "spec": WECHAT_PLUGIN_SPEC, } -LEGACY_WECHAT_CHANNEL_IDS = (WECHAT_PLUGIN_ID,) +WECHAT_TOKEN_PLACEHOLDER = "openshell:resolve:env:WECHAT_BOT_TOKEN" def _wechat_enabled() -> bool: @@ -123,98 +118,10 @@ def _js_iso_utc() -> str: return f"{now.strftime('%Y-%m-%dT%H:%M:%S')}.{now.microsecond // 1000:03d}Z" -def _dedupe(values: Iterable[str]) -> list[str]: - seen: set[str] = set() - result: list[str] = [] - for value in values: - item = str(value or "").strip() - if not item or item in seen: - continue - seen.add(item) - result.append(item) - return result - - -def _read_json_file(path: pathlib.Path) -> dict: - try: - parsed = json.loads(path.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError): - return {} - return parsed if isinstance(parsed, dict) else {} - - -def _metadata_files() -> Iterable[pathlib.Path]: - extensions_dir = _state_dir() / "extensions" - if not extensions_dir.exists(): - return [] - - matches: list[pathlib.Path] = [] - for root, dirs, files in os.walk(extensions_dir): - dirs[:] = [ - item - for item in dirs - if item not in {"node_modules", "plugin-runtime-deps", ".git"} - ] - root_path = pathlib.Path(root) - for filename in files: - if filename in {"openclaw.plugin.json", "package.json"}: - matches.append(root_path / filename) - return matches - - -def _declared_channel_ids_from_metadata() -> list[str]: - ids: list[str] = [] - for path in _metadata_files(): - metadata = _read_json_file(path) - if not metadata: - continue - - path_hint = str(path).lower() - package_name = str(metadata.get("name") or "") - plugin_id = str(metadata.get("id") or "") - openclaw = metadata.get("openclaw") - is_wechat_plugin = ( - plugin_id == WECHAT_PLUGIN_ID - or package_name == WECHAT_PLUGIN_PACKAGE - or WECHAT_PLUGIN_ID in path_hint - ) - if not is_wechat_plugin: - continue - - channels = metadata.get("channels") - if isinstance(channels, list): - ids.extend(item for item in channels if isinstance(item, str)) - - channel_configs = metadata.get("channelConfigs") - if isinstance(channel_configs, dict): - ids.extend(str(key) for key in channel_configs.keys()) - - if isinstance(openclaw, dict): - channel = openclaw.get("channel") - if isinstance(channel, dict) and isinstance(channel.get("id"), str): - ids.append(channel["id"]) - elif isinstance(channel, str): - ids.append(channel) - - channels = openclaw.get("channels") - if isinstance(channels, list): - ids.extend(item for item in channels if isinstance(item, str)) - - channel_configs = openclaw.get("channelConfigs") - if isinstance(channel_configs, dict): - ids.extend(str(key) for key in channel_configs.keys()) - - return _dedupe(ids) - - -def _wechat_channel_ids() -> list[str]: - return _dedupe([*_declared_channel_ids_from_metadata(), *LEGACY_WECHAT_CHANNEL_IDS]) - - def _patch_openclaw_config(account_id: str) -> None: - """Register enabled WeChat account blocks under the plugin channel ids - OpenClaw can load. The upstream plugin's auth/accounts.ts reads these blocks - to decide which accounts to start at boot.""" + """Register channels.openclaw-weixin.accounts..enabled=true in + openclaw.json. The upstream plugin's auth/accounts.ts reads this block to + decide which accounts to start at boot.""" cfg_path = _state_dir() / "openclaw.json" if not cfg_path.exists(): # generate-openclaw-config.py runs before us and is responsible for @@ -267,28 +174,14 @@ def _patch_openclaw_config(account_id: str) -> None: wechat_entry["enabled"] = True channels = cfg.setdefault("channels", {}) - if not isinstance(channels, dict): - channels = {} - cfg["channels"] = channels - - channel_ids = _wechat_channel_ids() - for channel_id in channel_ids: - channel_cfg = channels.setdefault(channel_id, {}) - if not isinstance(channel_cfg, dict): - channel_cfg = {} - channels[channel_id] = channel_cfg - channel_cfg["channelConfigUpdatedAt"] = _js_iso_utc() - accounts = channel_cfg.setdefault("accounts", {}) - if not isinstance(accounts, dict): - accounts = {} - channel_cfg["accounts"] = accounts - accounts[account_id] = {"enabled": True} + weixin = channels.setdefault("openclaw-weixin", {}) + weixin["channelConfigUpdatedAt"] = _js_iso_utc() + accounts = weixin.setdefault("accounts", {}) + accounts[account_id] = {"enabled": True} _atomic_write(cfg_path, json.dumps(cfg, indent=2) + "\n", 0o600) print( - "[seed-wechat-accounts] registered " - f"{', '.join(f'channels.{channel_id}.accounts.{account_id}' for channel_id in channel_ids)} " - f"in {cfg_path}" + f"[seed-wechat-accounts] registered channels.openclaw-weixin.accounts.{account_id} in {cfg_path}" ) diff --git a/src/ext/wechat/qr.ts b/src/ext/wechat/qr.ts index ce80732642..3320676773 100644 --- a/src/ext/wechat/qr.ts +++ b/src/ext/wechat/qr.ts @@ -38,7 +38,7 @@ export const WECHAT_ILINK_APP_ID = "bot"; * installed in the sandbox image, so the iLink gateway sees the same * client version from both the host login and the in-sandbox plugin. * Bump together with the version pinned in the Dockerfile. */ -export const WECHAT_ILINK_CLIENT_VERSION = encodeIlinkClientVersion("2.4.3"); +export const WECHAT_ILINK_CLIENT_VERSION = encodeIlinkClientVersion("2.4.2"); /** Client-side ceiling for a single status long-poll. 35s keeps us within * typical 60s gateway/proxy idle windows. */ diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index c3d86eb4cd..bc231df3a5 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -2921,7 +2921,7 @@ function getOpenShellDockerSupervisorImage(versionOutput: string | null = null): if (shouldUseOpenshellDevChannel() || isOpenshellDevVersion(versionOutput)) { return "ghcr.io/nvidia/openshell/supervisor:dev"; } - const supportedVersion = installedVersion ?? getBlueprintMaxOpenshellVersion() ?? "0.0.44"; + const supportedVersion = installedVersion ?? getBlueprintMaxOpenshellVersion() ?? "0.0.39"; return `ghcr.io/nvidia/openshell/supervisor:${supportedVersion}`; } @@ -4171,7 +4171,7 @@ async function startDockerDriverGateway({ exitOnFailure = true, skipSandboxBridg } if (!gatewayBin) { console.error(" OpenShell Docker-driver gateway binary not found."); - console.error(" Install OpenShell v0.0.44, or set NEMOCLAW_OPENSHELL_GATEWAY_BIN."); + console.error(" Install OpenShell v0.0.39, or set NEMOCLAW_OPENSHELL_GATEWAY_BIN."); if (exitOnFailure) process.exit(1); throw new Error("OpenShell gateway binary not found"); } diff --git a/src/lib/onboard/docker-driver-gateway-runtime-marker.test.ts b/src/lib/onboard/docker-driver-gateway-runtime-marker.test.ts index a99a2b2b47..ca9ca31aab 100644 --- a/src/lib/onboard/docker-driver-gateway-runtime-marker.test.ts +++ b/src/lib/onboard/docker-driver-gateway-runtime-marker.test.ts @@ -22,7 +22,7 @@ const expected = { }, endpoint: "http://127.0.0.1:8080", gatewayBin: "/usr/local/bin/openshell-gateway", - openshellVersion: "0.0.44", + openshellVersion: "0.0.39", dockerHost: "unix:///Users/me/.colima/default/docker.sock", platform: "darwin" as NodeJS.Platform, arch: "arm64" as NodeJS.Architecture, diff --git a/src/lib/onboard/openshell-install.ts b/src/lib/onboard/openshell-install.ts index 7ff90f5af2..f11531251f 100644 --- a/src/lib/onboard/openshell-install.ts +++ b/src/lib/onboard/openshell-install.ts @@ -159,7 +159,7 @@ export function ensureOpenshellForOnboard(deps: OpenShellInstallDeps): OpenShell deps.exit(1); } } else { - const minOpenshellVersion = deps.getBlueprintMinOpenshellVersion() ?? "0.0.44"; + const minOpenshellVersion = deps.getBlueprintMinOpenshellVersion() ?? "0.0.39"; const currentVersionOutput = deps.runCaptureOpenshell(["--version"], { ignoreError: true }); const needsDevChannel = deps.isLinuxDockerDriverGatewayEnabled(platform, arch) && diff --git a/src/lib/policy/index.ts b/src/lib/policy/index.ts index b2363465ea..9b6653ebd1 100644 --- a/src/lib/policy/index.ts +++ b/src/lib/policy/index.ts @@ -91,7 +91,7 @@ function loadPreset(name: string): string | null { */ function getPresetEndpoints(content: string): string[] { const hosts: string[] = []; - const regex = /^[ \t]*(?:-[ \t]*)?host:[ \t]*([^#\s,}]+)/gm; + const regex = /host:\s*([^\s,}]+)/g; let match; while ((match = regex.exec(content)) !== null) { hosts.push(match[1].replace(/^["']|["']$/g, "")); diff --git a/src/lib/sandbox/version.test.ts b/src/lib/sandbox/version.test.ts index 385c77a120..8597ae35d8 100644 --- a/src/lib/sandbox/version.test.ts +++ b/src/lib/sandbox/version.test.ts @@ -36,7 +36,7 @@ vi.mock("../agent/defs.js", () => ({ name, displayName: name === "openclaw" ? "OpenClaw" : "Hermes Agent", versionCommand: name === "openclaw" ? "openclaw --version" : "hermes --version", - expectedVersion: name === "openclaw" ? "2026.5.18" : "2026.5.16", + expectedVersion: name === "openclaw" ? "2026.4.24" : "2026.4.24", stateDirs: [], configPaths: { dir: "/sandbox/.openclaw" }, })), @@ -77,12 +77,12 @@ describe("checkAgentVersion", () => { registry.registerSandbox({ name: "test-sb", agent: null, - agentVersion: "2026.5.18", + agentVersion: "2026.4.24", }); const result = checkAgentVersion("test-sb"); expect(result.detectionMethod).toBe("registry"); - expect(result.sandboxVersion).toBe("2026.5.18"); + expect(result.sandboxVersion).toBe("2026.4.24"); expect(result.isStale).toBe(false); }); @@ -103,7 +103,7 @@ describe("checkAgentVersion", () => { registry.registerSandbox({ name: "test-sb", agent: null, - agentVersion: "2026.5.18", + agentVersion: "2026.4.24", }); const result = checkAgentVersion("test-sb"); @@ -120,7 +120,7 @@ describe("checkAgentVersion", () => { vi.mocked(spawnSync).mockReturnValue({ status: 0, - stdout: "OpenClaw 2026.5.18 (abc123)\n", + stdout: "OpenClaw 2026.4.24 (abc123)\n", stderr: "", pid: 1234, output: [], @@ -129,7 +129,7 @@ describe("checkAgentVersion", () => { const result = checkAgentVersion("test-sb"); expect(result.detectionMethod).toBe("ssh-exec"); - expect(result.sandboxVersion).toBe("2026.5.18"); + expect(result.sandboxVersion).toBe("2026.4.24"); expect(result.isStale).toBe(false); expect(captureOpenshellCommand).toHaveBeenCalledWith( "/usr/local/bin/openshell", @@ -139,7 +139,7 @@ describe("checkAgentVersion", () => { // Should have cached the version in registry const updated = registry.getSandbox("test-sb"); - expect(updated?.agentVersion).toBe("2026.5.18"); + expect(updated?.agentVersion).toBe("2026.4.24"); }); it("returns unavailable when SSH config fails", () => { @@ -183,7 +183,7 @@ describe("checkAgentVersion", () => { vi.mocked(spawnSync).mockReturnValue({ status: 0, - stdout: "OpenClaw 2026.5.18 (abc123)\n", + stdout: "OpenClaw 2026.4.24 (abc123)\n", stderr: "", pid: 1234, output: [], @@ -192,7 +192,7 @@ describe("checkAgentVersion", () => { const result = checkAgentVersion("test-sb", { forceProbe: true }); expect(result.detectionMethod).toBe("ssh-exec"); - expect(result.sandboxVersion).toBe("2026.5.18"); + expect(result.sandboxVersion).toBe("2026.4.24"); }); }); @@ -219,14 +219,14 @@ describe("formatStalenessWarning", () => { it("includes sandbox name, versions, and rebuild hint", () => { const lines = formatStalenessWarning("my-sb", { sandboxVersion: "2026.3.11", - expectedVersion: "2026.5.18", + expectedVersion: "2026.4.24", isStale: true, detectionMethod: "registry", }); const joined = lines.join("\n"); expect(joined).toContain("my-sb"); expect(joined).toContain("2026.3.11"); - expect(joined).toContain("2026.5.18"); + expect(joined).toContain("2026.4.24"); expect(joined).toContain("rebuild"); }); }); diff --git a/src/lib/verify-deployment.test.ts b/src/lib/verify-deployment.test.ts index 6eeaa4cabe..18592a9adb 100644 --- a/src/lib/verify-deployment.test.ts +++ b/src/lib/verify-deployment.test.ts @@ -124,13 +124,13 @@ describe("verifyDeployment", () => { const deps = makeDeps({ executeSandboxCommand: (_name: string, script: string) => { if (script.includes("openclaw --version")) { - return { status: 0, stdout: "2026.5.18", stderr: "" }; + return { status: 0, stdout: "2026.4.24", stderr: "" }; } return { status: 0, stdout: "200", stderr: "" }; }, }); const result = await verifyDeployment("my-sandbox", chain, deps, NO_RETRY); - expect(result.verification.gatewayVersion).toBe("2026.5.18"); + expect(result.verification.gatewayVersion).toBe("2026.4.24"); }); it("reports null version when gateway is down (skips version probe)", async () => { @@ -179,7 +179,7 @@ describe("verifyDeployment", () => { const deps = makeDeps({ executeSandboxCommand: (_name: string, script: string) => { if (script.includes("openclaw --version")) { - return { status: 0, stdout: "2026.5.18", stderr: "" }; + return { status: 0, stdout: "2026.4.24", stderr: "" }; } if (script.includes("inference.local")) { return { status: 0, stdout: "200", stderr: "" }; @@ -240,14 +240,14 @@ describe("formatVerificationDiagnostics", () => { const result = await verifyDeployment("my-sandbox", chain, makeDeps({ executeSandboxCommand: (_name: string, script: string) => { if (script.includes("openclaw --version")) { - return { status: 0, stdout: "2026.5.18", stderr: "" }; + return { status: 0, stdout: "2026.4.24", stderr: "" }; } return { status: 0, stdout: "200", stderr: "" }; }, }), NO_RETRY); const lines = formatVerificationDiagnostics(result); expect(lines.some((l) => l.includes("verified"))).toBe(true); - expect(lines.some((l) => l.includes("2026.5.18"))).toBe(true); + expect(lines.some((l) => l.includes("2026.4.24"))).toBe(true); }); it("prints failure diagnostics with hints when unhealthy", async () => { diff --git a/test/e2e-test.sh b/test/e2e-test.sh index 155d256f9f..6aa3c8d80b 100755 --- a/test/e2e-test.sh +++ b/test/e2e-test.sh @@ -246,8 +246,7 @@ if node --input-type=module -e " // Simulate corruption: modify the host config const configPath = path.join(os.homedir(), '.openclaw', 'openclaw.json'); - const originalRaw = fs.readFileSync(configPath, 'utf-8'); - JSON.parse(originalRaw); + const original = JSON.parse(fs.readFileSync(configPath, 'utf-8')); fs.writeFileSync(configPath, JSON.stringify({ corrupted: true })); // Rollback @@ -255,10 +254,10 @@ if node --input-type=module -e " if (!success) throw new Error('Rollback returned false'); // Verify restoration - const restoredRaw = fs.readFileSync(configPath, 'utf-8'); - const restored = JSON.parse(restoredRaw); + const restored = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + const version = (restored.meta || {}).lastTouchedVersion; + if (version !== '2026.3.11') throw new Error('Restored config wrong: ' + JSON.stringify(restored)); if ('corrupted' in restored) throw new Error('Config still corrupted after rollback'); - if (restoredRaw !== originalRaw) throw new Error('Restored config differs from pre-corruption content: ' + JSON.stringify(restored)); console.log('Restored config: ' + JSON.stringify(restored)); "; then pass "Snapshot rollback restores original config" diff --git a/test/e2e/docs/parity-inventory.generated.json b/test/e2e/docs/parity-inventory.generated.json index f42dff2ee9..62f9c3e441 100644 --- a/test/e2e/docs/parity-inventory.generated.json +++ b/test/e2e/docs/parity-inventory.generated.json @@ -12140,9 +12140,9 @@ { "script": "test/e2e/test-openshell-version-pin.sh", "line": 215, - "text": "Installer hard-failed on sticky OpenShell 0.0.45 instead of reinstalling pinned 0.0.44 (#3474)", + "text": "Installer hard-failed on sticky OpenShell 0.0.40 instead of reinstalling pinned 0.0.39 (#3474)", "polarity": "fail", - "normalized_id": "installer.hard.failed.on.sticky.openshell.0.0.45.instead.of.reinstalling.pinned.0.0.44.3474", + "normalized_id": "installer.hard.failed.on.sticky.openshell.0.0.40.instead.of.reinstalling.pinned.0.0.39.3474", "mapping_status": "retired" }, { @@ -12164,49 +12164,49 @@ { "script": "test/e2e/test-openshell-version-pin.sh", "line": 222, - "text": "Expected installer to download pinned OpenShell v0.0.44", + "text": "Expected installer to download pinned OpenShell v0.0.39", "polarity": "fail", - "normalized_id": "expected.installer.to.download.pinned.openshell.v0.0.44", + "normalized_id": "expected.installer.to.download.pinned.openshell.v0.0.39", "mapping_status": "retired" }, { "script": "test/e2e/test-openshell-version-pin.sh", "line": 224, - "text": "Installer downloaded pinned OpenShell v0.0.44", + "text": "Installer downloaded pinned OpenShell v0.0.39", "polarity": "pass", - "normalized_id": "installer.downloaded.pinned.openshell.v0.0.44", + "normalized_id": "installer.downloaded.pinned.openshell.v0.0.39", "mapping_status": "mapped" }, { "script": "test/e2e/test-openshell-version-pin.sh", "line": 227, - "text": "Installer downloaded OpenShell v0.0.45 despite NemoClaw max 0.0.44", + "text": "Installer downloaded OpenShell v0.0.40 despite NemoClaw max 0.0.39", "polarity": "fail", - "normalized_id": "installer.downloaded.openshell.v0.0.45.despite.nemoclaw.max.0.0.44", + "normalized_id": "installer.downloaded.openshell.v0.0.40.despite.nemoclaw.max.0.0.39", "mapping_status": "retired" }, { "script": "test/e2e/test-openshell-version-pin.sh", "line": 229, - "text": "Installer did not download too-new OpenShell v0.0.45", + "text": "Installer did not download too-new OpenShell v0.0.40", "polarity": "pass", - "normalized_id": "installer.did.not.download.too.new.openshell.v0.0.45", + "normalized_id": "installer.did.not.download.too.new.openshell.v0.0.40", "mapping_status": "mapped" }, { "script": "test/e2e/test-openshell-version-pin.sh", "line": 232, - "text": "openshell binary was not replaced with pinned 0.0.44", + "text": "openshell binary was not replaced with pinned 0.0.39", "polarity": "fail", - "normalized_id": "openshell.binary.was.not.replaced.with.pinned.0.0.44", + "normalized_id": "openshell.binary.was.not.replaced.with.pinned.0.0.39", "mapping_status": "retired" }, { "script": "test/e2e/test-openshell-version-pin.sh", "line": 234, - "text": "Sticky openshell 0.0.45 was replaced with pinned 0.0.44", + "text": "Sticky openshell 0.0.40 was replaced with pinned 0.0.39", "polarity": "pass", - "normalized_id": "sticky.openshell.0.0.45.was.replaced.with.pinned.0.0.44", + "normalized_id": "sticky.openshell.0.0.40.was.replaced.with.pinned.0.0.39", "mapping_status": "mapped" } ] diff --git a/test/e2e/docs/parity-map.yaml b/test/e2e/docs/parity-map.yaml index d6694d4115..639dc3c539 100644 --- a/test/e2e/docs/parity-map.yaml +++ b/test/e2e/docs/parity-map.yaml @@ -11696,7 +11696,7 @@ scripts: status: migrated bucket: install-upgrade assertions: - - legacy: Installer hard-failed on sticky OpenShell 0.0.45 instead of reinstalling pinned 0.0.44 (#3474) + - legacy: Installer hard-failed on sticky OpenShell 0.0.40 instead of reinstalling pinned 0.0.39 (#3474) status: retired reason: legacy negative/failure assertion retained by script but not represented as scenario success criterion reviewer: e2e-maintainers @@ -11709,27 +11709,27 @@ scripts: - legacy: install-openshell.sh completed status: mapped id: legacy.openshell.version.pin.install.openshell.sh.completed - - legacy: Expected installer to download pinned OpenShell v0.0.44 + - legacy: Expected installer to download pinned OpenShell v0.0.39 status: retired reason: legacy negative/failure assertion retained by script but not represented as scenario success criterion reviewer: e2e-maintainers approved_at: '2026-05-13' - - legacy: Installer downloaded pinned OpenShell v0.0.44 + - legacy: Installer downloaded pinned OpenShell v0.0.39 status: mapped - id: legacy.openshell.version.pin.installer.downloaded.pinned.openshell.vv44 - - legacy: Installer downloaded OpenShell v0.0.45 despite NemoClaw max 0.0.44 + id: legacy.openshell.version.pin.installer.downloaded.pinned.openshell.vv39 + - legacy: Installer downloaded OpenShell v0.0.40 despite NemoClaw max 0.0.39 status: retired reason: legacy negative/failure assertion retained by script but not represented as scenario success criterion reviewer: e2e-maintainers approved_at: '2026-05-13' - - legacy: Installer did not download too-new OpenShell v0.0.45 + - legacy: Installer did not download too-new OpenShell v0.0.40 status: mapped - id: legacy.openshell.version.pin.installer.did.not.download.too.new.openshell.vv45 - - legacy: openshell binary was not replaced with pinned 0.0.44 + id: legacy.openshell.version.pin.installer.did.not.download.too.new.openshell.vv40 + - legacy: openshell binary was not replaced with pinned 0.0.39 status: retired reason: legacy negative/failure assertion retained by script but not represented as scenario success criterion reviewer: e2e-maintainers approved_at: '2026-05-13' - - legacy: Sticky openshell 0.0.45 was replaced with pinned 0.0.44 + - legacy: Sticky openshell 0.0.40 was replaced with pinned 0.0.39 status: mapped - id: legacy.openshell.version.pin.sticky.openshell.v45.was.replaced.with.pinned.v44 + id: legacy.openshell.version.pin.sticky.openshell.v40.was.replaced.with.pinned.v39 diff --git a/test/e2e/lib/openclaw-agent-json.py b/test/e2e/lib/openclaw-agent-json.py deleted file mode 100755 index c99e9f5a16..0000000000 --- a/test/e2e/lib/openclaw-agent-json.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/usr/bin/env python3 -# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Extract text payloads from `openclaw agent --json` output. - -OpenClaw has emitted both of these envelopes across recent versions: - - {"result": {"payloads": [{"text": "..."}]}} - {"payloads": [{"text": "..."}]} - -The E2E smoke checks only need the joined assistant text. Invalid JSON is a -real harness failure and exits nonzero; valid JSON with no text prints nothing. -""" - -from __future__ import annotations - -import json -import sys -from typing import Any - - -def _payloads(doc: Any) -> list[Any]: - if not isinstance(doc, dict): - return [] - top_level = doc.get("payloads") - if isinstance(top_level, list): - return top_level - result = doc.get("result") - if isinstance(result, dict) and isinstance(result.get("payloads"), list): - return result["payloads"] - return [] - - -def _load_agent_json_docs(text: str) -> list[Any]: - try: - doc = json.loads(text) - except json.JSONDecodeError: - pass - else: - return doc if isinstance(doc, list) else [doc] - - decoder = json.JSONDecoder() - docs: list[Any] = [] - index = 0 - while index < len(text): - start = text.find("{", index) - if start < 0: - break - try: - doc, end = decoder.raw_decode(text[start:]) - except json.JSONDecodeError: - index = start + 1 - continue - docs.append(doc) - index = start + end - if docs: - return docs - raise json.JSONDecodeError("no JSON object found", text, 0) - - -def main() -> int: - raw = sys.stdin.read() - try: - docs = _load_agent_json_docs(raw) - except json.JSONDecodeError as err: - print(f"invalid JSON: {err}", file=sys.stderr) - return 1 - - parts = [ - payload["text"] - for doc in docs - for payload in _payloads(doc) - if isinstance(payload, dict) and isinstance(payload.get("text"), str) - ] - print("\n".join(parts)) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/test/e2e/lib/slack-api-proof.sh b/test/e2e/lib/slack-api-proof.sh index 47cf0fbca0..3e15ac42b0 100755 --- a/test/e2e/lib/slack-api-proof.sh +++ b/test/e2e/lib/slack-api-proof.sh @@ -163,7 +163,6 @@ run_fake_slack_channel_mention_proof() { import { execFileSync } from "node:child_process"; import fs from "node:fs"; import http from "node:http"; -import { createRequire } from "node:module"; import path from "node:path"; import { pathToFileURL } from "node:url"; @@ -175,12 +174,6 @@ function fail(message) { function resolveOpenClawRoot() { const candidates = []; if (process.env.OPENCLAW_PACKAGE_ROOT) candidates.push(process.env.OPENCLAW_PACKAGE_ROOT); - const require = createRequire(import.meta.url); - for (const base of [process.cwd(), "/sandbox", "/usr/local/lib/node_modules"]) { - try { - candidates.push(path.dirname(require.resolve("openclaw/package.json", { paths: [base] }))); - } catch {} - } try { const globalRoot = execFileSync("npm", ["root", "-g"], { encoding: "utf8" }).trim(); if (globalRoot) candidates.push(path.join(globalRoot, "openclaw")); @@ -191,7 +184,7 @@ function resolveOpenClawRoot() { return candidate; } } - return null; + fail(`OpenClaw Slack test API not found in: ${candidates.join(", ")}`); } function createOpenClawSlackProofRoot(openclawRoot) { @@ -279,7 +272,7 @@ function resolveSlackTestApiImport(testApiSource, exportName) { new RegExp(`import\\s+\\{[^}]*\\b${escapedExportName}\\b[^}]*\\}\\s+from\\s+["']([^"']+)["']`), ]; const match = patterns.map((pattern) => testApiSource.match(pattern)).find(Boolean); - if (!match) throw new Error(`OpenClaw Slack test API does not expose ${exportName}`); + if (!match) fail(`OpenClaw Slack test API does not expose ${exportName}`); return match[1]; } @@ -357,181 +350,133 @@ const deniedUser = process.env.SLACK_DENIED_USER || "U999DENIED"; if (!Array.isArray(wildcard.users) || !wildcard.users.includes(allowedUser)) { fail(`wildcard Slack channel users do not include ${allowedUser}: ${JSON.stringify(wildcard.users)}`); } -if (wildcard.users.includes(deniedUser)) { - fail(`wildcard Slack channel users unexpectedly include denied user ${deniedUser}`); -} + +const openclawRoot = resolveOpenClawRoot(); +const slackApi = await importOpenClawSlackProofApi(openclawRoot); +const { createInboundSlackTestContext, prepareSlackMessage, sendMessageSlack } = slackApi; const channelId = "C0E2ESLACK"; +const appClient = { + assistant: { + threads: { + setStatus: async () => ({ ok: true }), + }, + }, + conversations: { + info: async () => ({ + ok: true, + channel: { + id: channelId, + name: "nemoclaw-test", + is_channel: true, + }, + }), + open: async ({ users }) => ({ + ok: true, + channel: { id: `D${users}` }, + }), + }, + reactions: { + add: async () => ({ ok: true }), + remove: async () => ({ ok: true }), + }, + users: { + info: async ({ user }) => ({ + ok: true, + user: { + id: user, + name: user, + profile: { display_name: user, real_name: user }, + }, + }), + }, +}; + +const ctx = createInboundSlackTestContext({ + cfg, + appClient, + channelsConfig: slackAccount.channels, + defaultRequireMention: slackAccount.requireMention ?? true, +}); +ctx.botToken = slackAccount.botToken; +ctx.botUserId = "B1"; +ctx.botId = "B1"; +ctx.teamId = "T1"; +ctx.apiAppId = "A1"; + +const account = { + accountId: "default", + botToken: slackAccount.botToken, + appToken: slackAccount.appToken, + config: slackAccount, +}; const baseMessage = { channel: channelId, channel_type: "channel", team: "T1", text: "<@B1> channel mention proof", }; -const proofText = "NemoClaw Slack channel mention proof"; -const token = slackAccount.botToken; -async function postChannelProofMessage() { - const response = await postForm( - "/api/chat.postMessage", - { - token, - channel: channelId, - text: proofText, - thread_ts: "1710000000.000100", - }, - `Bearer ${token}`, - ); - if (response.statusCode !== 200 || response.body?.ok !== true) { - throw new Error(`fake Slack chat.postMessage failed: ${response.statusCode} ${JSON.stringify(response.body)}`); - } - return response.body; +const allowedPrepared = await prepareSlackMessage({ + ctx, + account, + message: { ...baseMessage, user: allowedUser, ts: "1710000000.000100" }, + opts: { source: "app_mention", wasMentioned: true }, +}); +if (!allowedPrepared) fail("allowed Slack app_mention did not prepare"); +if (allowedPrepared.replyTarget !== `channel:${channelId}`) { + fail(`unexpected allowed replyTarget: ${allowedPrepared.replyTarget}`); } -async function runOpenClawPrivateProof(openclawRoot) { - const slackApi = await importOpenClawSlackProofApi(openclawRoot); - const { createInboundSlackTestContext, prepareSlackMessage, sendMessageSlack } = slackApi; - const appClient = { - assistant: { - threads: { - setStatus: async () => ({ ok: true }), - }, - }, - conversations: { - info: async () => ({ - ok: true, - channel: { - id: channelId, - name: "nemoclaw-test", - is_channel: true, - }, - }), - open: async ({ users }) => ({ - ok: true, - channel: { id: `D${users}` }, - }), - }, - reactions: { - add: async () => ({ ok: true }), - remove: async () => ({ ok: true }), - }, - users: { - info: async ({ user }) => ({ - ok: true, - user: { - id: user, - name: user, - profile: { display_name: user, real_name: user }, - }, - }), - }, - }; - - const ctx = createInboundSlackTestContext({ - cfg, - appClient, - channelsConfig: slackAccount.channels, - defaultRequireMention: slackAccount.requireMention ?? true, - }); - ctx.botToken = slackAccount.botToken; - ctx.botUserId = "B1"; - ctx.botId = "B1"; - ctx.teamId = "T1"; - ctx.apiAppId = "A1"; - - const account = { - accountId: "default", - botToken: slackAccount.botToken, - appToken: slackAccount.appToken, - config: slackAccount, - }; - const allowedPrepared = await prepareSlackMessage({ - ctx, - account, - message: { ...baseMessage, user: allowedUser, ts: "1710000000.000100" }, - opts: { source: "app_mention", wasMentioned: true }, - }); - if (!allowedPrepared) fail("allowed Slack app_mention did not prepare"); - if (allowedPrepared.replyTarget !== `channel:${channelId}`) { - fail(`unexpected allowed replyTarget: ${allowedPrepared.replyTarget}`); - } +const deniedPrepared = await prepareSlackMessage({ + ctx, + account, + message: { ...baseMessage, user: deniedUser, ts: "1710000000.000101" }, + opts: { source: "app_mention", wasMentioned: true }, +}); +if (deniedPrepared !== null) fail("denied Slack app_mention unexpectedly prepared"); - const deniedPrepared = await prepareSlackMessage({ - ctx, - account, - message: { ...baseMessage, user: deniedUser, ts: "1710000000.000101" }, - opts: { source: "app_mention", wasMentioned: true }, - }); - if (deniedPrepared !== null) fail("denied Slack app_mention unexpectedly prepared"); - - const fakeClient = { - chat: { - postMessage: async (payload) => { - const response = await postForm( - "/api/chat.postMessage", - { - token, - channel: payload.channel || "", - text: payload.text || "", - ...(payload.thread_ts ? { thread_ts: payload.thread_ts } : {}), - ...(payload.blocks ? { blocks: JSON.stringify(payload.blocks) } : {}), - }, - `Bearer ${token}`, - ); - if (response.statusCode !== 200 || response.body?.ok !== true) { - throw new Error(`fake Slack chat.postMessage failed: ${response.statusCode} ${JSON.stringify(response.body)}`); - } - return response.body; - }, +const proofText = "NemoClaw Slack channel mention proof"; +const token = slackAccount.botToken; +const fakeClient = { + chat: { + postMessage: async (payload) => { + const response = await postForm( + "/api/chat.postMessage", + { + token, + channel: payload.channel || "", + text: payload.text || "", + ...(payload.thread_ts ? { thread_ts: payload.thread_ts } : {}), + ...(payload.blocks ? { blocks: JSON.stringify(payload.blocks) } : {}), + }, + `Bearer ${token}`, + ); + if (response.statusCode !== 200 || response.body?.ok !== true) { + throw new Error(`fake Slack chat.postMessage failed: ${response.statusCode} ${JSON.stringify(response.body)}`); + } + return response.body; }, - }; - - const sendResult = await sendMessageSlack(allowedPrepared.replyTarget, proofText, { - cfg, - token, - client: fakeClient, - accountId: "default", - }); - if (sendResult.channelId !== channelId) { - fail(`sendMessageSlack returned unexpected channelId: ${sendResult.channelId}`); - } - return { - proof: "openclaw-private-helper", - allowedReplyTarget: allowedPrepared.replyTarget, - deniedPrepared: deniedPrepared === null, - messageId: sendResult.messageId, - channelId: sendResult.channelId, - }; -} - -async function runHermeticSlackProof() { - const response = await postChannelProofMessage(); - return { - proof: "nemoclaw-hermetic", - allowedReplyTarget: `channel:${channelId}`, - deniedPrepared: true, - messageId: response.ts || response.message?.ts || null, - channelId, - }; -} + }, +}; -const openclawRoot = resolveOpenClawRoot(); -let result; -if (openclawRoot) { - try { - result = await runOpenClawPrivateProof(openclawRoot); - } catch (error) { - console.error(`[slack-proof] OpenClaw Slack helper unavailable (${error.message}); using NemoClaw hermetic proof`); - result = await runHermeticSlackProof(); - } -} else { - result = await runHermeticSlackProof(); +const sendResult = await sendMessageSlack(allowedPrepared.replyTarget, proofText, { + cfg, + token, + client: fakeClient, + accountId: "default", +}); +if (sendResult.channelId !== channelId) { + fail(`sendMessageSlack returned unexpected channelId: ${sendResult.channelId}`); } console.log( JSON.stringify({ ok: true, - ...result, + allowedReplyTarget: allowedPrepared.replyTarget, + deniedPrepared: deniedPrepared === null, + messageId: sendResult.messageId, + channelId: sendResult.channelId, }), ); NODE diff --git a/test/e2e/test-bedrock-runtime-compatible-anthropic.sh b/test/e2e/test-bedrock-runtime-compatible-anthropic.sh index 0b0f3deff5..76e36354ae 100755 --- a/test/e2e/test-bedrock-runtime-compatible-anthropic.sh +++ b/test/e2e/test-bedrock-runtime-compatible-anthropic.sh @@ -698,7 +698,23 @@ check_openclaw_agent_turn() { return fi - reply=$(printf '%s' "$raw" | python3 "${SCRIPT_DIR_TIMEOUT}/lib/openclaw-agent-json.py" 2>/dev/null) || true + reply=$(printf '%s' "$raw" | python3 -c ' +import json +import sys + +text = sys.stdin.read() +for idx, char in enumerate(text): + if char != "{": + continue + try: + doc = json.loads(text[idx:]) + except Exception: + continue + payloads = ((doc.get("result") or {}).get("payloads") or []) + parts = [p.get("text") for p in payloads if isinstance(p, dict) and isinstance(p.get("text"), str)] + print("\n".join(parts)) + break +' 2>/dev/null) || true if [ "$rc" -eq 0 ] && grep -qi "PONG" <<<"$reply"; then pass "B8: OpenClaw agent completed a Bedrock-backed turn through inference.local" diff --git a/test/e2e/test-brave-search-e2e.sh b/test/e2e/test-brave-search-e2e.sh index f7e1a34d57..e6f3f92d49 100755 --- a/test/e2e/test-brave-search-e2e.sh +++ b/test/e2e/test-brave-search-e2e.sh @@ -306,7 +306,19 @@ check_real_brave_search_via_agent() { return fi - reply=$(printf '%s' "$raw" | python3 "${SCRIPT_DIR_TIMEOUT}/lib/openclaw-agent-json.py" 2>/dev/null) || true + reply=$(printf '%s' "$raw" | python3 -c " +import json, sys +try: + doc = json.load(sys.stdin) +except Exception: + sys.exit(0) +result = doc.get('result') or {} +parts = [] +for p in result.get('payloads') or []: + if isinstance(p, dict) and isinstance(p.get('text'), str): + parts.append(p['text']) +print('\n'.join(parts)) +" 2>/dev/null) || true # NVIDIA-related phrasing (nvidia, gpu, cuda, geforce) is overwhelmingly # likely in any legitimate top-1 web result for the query "NVIDIA". diff --git a/test/e2e/test-full-e2e.sh b/test/e2e/test-full-e2e.sh index e705238ed8..cb902662f9 100755 --- a/test/e2e/test-full-e2e.sh +++ b/test/e2e/test-full-e2e.sh @@ -371,8 +371,8 @@ fi # routeLogsToStderr() (openclaw/src/commands/agent-via-gateway.ts:57), # so stdout is a clean JSON envelope; prompt-echo on stderr cannot # pollute the assertion. -# * Asserts on the model's reply payload text from OpenClaw JSON, not on -# the merged stdout/stderr. +# * Asserts on the model's reply text inside `result.payloads[].text`, +# not on the merged stdout/stderr. # * The expected token (the integer 42) is not a literal substring of the # prompt, so an error path that quoted the prompt back cannot satisfy # the grep. @@ -394,7 +394,19 @@ if openshell sandbox ssh-config "$SANDBOX_NAME" >"$ssh_config" 2>/dev/null; then fi rm -f "$ssh_config" -agent_reply=$(printf '%s' "$agent_response" | python3 "$REPO/test/e2e/lib/openclaw-agent-json.py" 2>/dev/null) || true +agent_reply=$(echo "$agent_response" | python3 -c " +import json, sys +try: + doc = json.load(sys.stdin) +except Exception: + sys.exit(0) +result = doc.get('result') or {} +parts = [] +for p in result.get('payloads') or []: + if isinstance(p, dict) and isinstance(p.get('text'), str): + parts.append(p['text']) +print('\n'.join(parts)) +" 2>/dev/null) || true if grep -qE "(^|[^0-9])42([^0-9]|$)" <<<"$agent_reply"; then pass "[LIVE] openclaw agent: model answered 6×7=42 through openclaw → inference.local" diff --git a/test/e2e/test-issue-2478-crash-loop-recovery.sh b/test/e2e/test-issue-2478-crash-loop-recovery.sh index 59bb09f46f..966df30bf7 100755 --- a/test/e2e/test-issue-2478-crash-loop-recovery.sh +++ b/test/e2e/test-issue-2478-crash-loop-recovery.sh @@ -92,7 +92,6 @@ info() { printf '\033[1;34m [info]\033[0m %s\n' "$1"; } SANDBOX_NAME="${NEMOCLAW_SANDBOX_NAME:-e2e-2478}" CRASH_CYCLES="${NEMOCLAW_E2E_CRASH_CYCLES:-5}" SOAK_SECONDS="${NEMOCLAW_E2E_SOAK_SECONDS:-300}" -DASHBOARD_PORT="${NEMOCLAW_DASHBOARD_PORT:-18789}" REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/../.." && pwd)" # ── Helpers ────────────────────────────────────────────────────── @@ -103,15 +102,15 @@ sandbox_exec() { openshell sandbox exec --name "$SANDBOX_NAME" -- "$@" 2>&1 } -# Get the current OpenClaw gateway PID inside the sandbox, or empty string. -# OpenClaw v0.0.44/2026.5.18 can show the long-running process as plain -# `openclaw` rather than the older `openclaw-gateway` argv. Match the process -# table directly so readiness does not depend on the legacy rename. +# Get the current openclaw gateway PID inside the sandbox, or empty string. +# The gateway re-execs to argv `openclaw-gateway` after startup (it spawns +# from the launcher whose argv is `openclaw gateway run`). Match either form +# via `[o]penclaw[ -]gateway` — bracket trick prevents pgrep self-match, +# `[ -]` accepts both the launcher (space) and the post-rename (dash). `-o` +# returns the OLDEST match (the long-lived launcher 262 in the typical +# parent/child tree); env is inherited so NODE_OPTIONS reads the same. gateway_pid() { - local out - # shellcheck disable=SC2016 # Single-quoted body runs inside the sandbox shell. - out="$(sandbox_exec sh -c 'pid="$(ps -eo pid=,comm=,args= 2>/dev/null | awk '\''($2 == "openclaw" && $0 ~ /gateway/) || $0 ~ /openclaw[ -]gateway/ { print $1 }'\'' | sort -n | head -n 1)"; if [ -z "$pid" ]; then pid="$(ps -eo pid=,comm=,args= 2>/dev/null | awk '\''$2 == "openclaw" { print $1 }'\'' | sort -n | head -n 1)"; fi; printf "%s\n" "$pid"')" - printf '%s\n' "$out" | awk '/^[0-9]+$/ { print; exit }' + sandbox_exec sh -c "pgrep -fo '[o]penclaw[ -]gateway'" | tr -d '[:space:]' } # Read /tmp/nemoclaw-proxy-env.sh — the single source of truth for the @@ -233,8 +232,8 @@ gateway_diagnostics() { sandbox_exec sh -c "tail -n 60 /tmp/gateway.log 2>&1 || echo '(no gateway.log)'" | sed 's/^/ /' echo " [nemoclaw status]" nemoclaw "$SANDBOX_NAME" status 2>&1 | head -30 | sed 's/^/ /' - echo " [openshell sandbox list]" - openshell sandbox list 2>&1 | head -20 | sed 's/^/ /' || true + echo " [openshell sandbox containers / pod]" + openshell sandbox info --name "$SANDBOX_NAME" 2>&1 | head -20 | sed 's/^/ /' || true if [ -n "$pid" ]; then echo " [reported pid: $pid]" echo " [/proc/${pid} listing]" @@ -261,39 +260,13 @@ run_probe_only_or_fail() { rm -f "$probe_out" } -# Returns 0 when the current OpenClaw runtime has crossed the same readiness -# surface used by newer gateway E2Es: ready log, local /health, or healthy host -# status. This avoids failing on the old PID-name-only probe when OpenClaw is -# already serving. -gateway_runtime_ready() { - if sandbox_exec sh -c "grep -Fq '[gateway] ready' /tmp/gateway.log 2>/dev/null"; then - return 0 - fi - local health_code - health_code="$(sandbox_exec sh -c "curl -so /dev/null -w '%{http_code}' --max-time 3 http://localhost:${DASHBOARD_PORT}/health 2>/dev/null" | tr -d '[:space:]')" || true - if [ "$health_code" = "200" ]; then - return 0 - fi - local status_output - status_output="$(timeout 20 nemoclaw "$SANDBOX_NAME" status 2>&1)" || true - if echo "$status_output" | grep -Eiq '\b(healthy|ready)\b'; then - return 0 - fi - if echo "$status_output" | grep -Eiq '\brunning\b' \ - && ! echo "$status_output" | grep -Eiq '\bnot[[:space:]]+running\b'; then - return 0 - fi - return 1 -} - -# Wait until gateway PID is non-empty and runtime-ready (or timeout). Echoes -# pid, returns 0/1. +# Wait until gateway PID is non-empty (or timeout). Echoes pid, returns 0/1. wait_for_gateway_up() { local timeout="${1:-30}" local elapsed=0 pid="" while [ "$elapsed" -lt "$timeout" ]; do pid="$(gateway_pid)" - if [ -n "$pid" ] && gateway_runtime_ready; then + if [ -n "$pid" ]; then echo "$pid" return 0 fi @@ -411,7 +384,7 @@ section "Phase 3: Crash-recovery loop ($CRASH_CYCLES cycles)" prev_pid="$INIT_PID" for cycle in $(seq 1 "$CRASH_CYCLES"); do info "Cycle $cycle/$CRASH_CYCLES — killing gateway pid=$prev_pid" - sandbox_exec sh -c "kill -9 $prev_pid 2>/dev/null; sleep 1" >/dev/null + sandbox_exec sh -c "kill -9 $prev_pid 2>/dev/null; sleep 1; pgrep -fo '[o]penclaw[ -]gateway' || echo DEAD" >/dev/null # Trigger recovery via the actual operator probe path: # `nemoclaw connect --probe-only` calls diff --git a/test/e2e/test-launchable-smoke.sh b/test/e2e/test-launchable-smoke.sh index e4313004c8..bbb04cf113 100755 --- a/test/e2e/test-launchable-smoke.sh +++ b/test/e2e/test-launchable-smoke.sh @@ -522,7 +522,19 @@ if openshell sandbox ssh-config "$SANDBOX_NAME" >"$ssh_config" 2>/dev/null; then fi rm -f "$ssh_config" -agent_reply=$(printf '%s' "$agent_response" | python3 "$SCRIPT_DIR/lib/openclaw-agent-json.py" 2>/dev/null) || true +agent_reply=$(echo "$agent_response" | python3 -c " +import json, sys +try: + doc = json.load(sys.stdin) +except Exception: + sys.exit(0) +result = doc.get('result') or {} +parts = [] +for p in result.get('payloads') or []: + if isinstance(p, dict) and isinstance(p.get('text'), str): + parts.append(p['text']) +print('\n'.join(parts)) +" 2>/dev/null) || true if grep -qE "(^|[^0-9])42([^0-9]|$)" <<<"$agent_reply"; then pass "[LIVE] openclaw agent: model answered 6×7=42 through openclaw → inference.local" diff --git a/test/e2e/test-messaging-compatible-endpoint.sh b/test/e2e/test-messaging-compatible-endpoint.sh index a66bdfea94..a58069c0fa 100755 --- a/test/e2e/test-messaging-compatible-endpoint.sh +++ b/test/e2e/test-messaging-compatible-endpoint.sh @@ -525,7 +525,19 @@ check_openclaw_agent_turn() { return fi - reply=$(printf '%s' "$raw" | python3 "${SCRIPT_DIR_TIMEOUT}/lib/openclaw-agent-json.py" 2>/dev/null) || true + reply=$(printf '%s' "$raw" | python3 -c " +import json, sys +try: + doc = json.load(sys.stdin) +except Exception: + sys.exit(0) +result = doc.get('result') or {} +parts = [] +for p in result.get('payloads') or []: + if isinstance(p, dict) and isinstance(p.get('text'), str): + parts.append(p['text']) +print('\n'.join(parts)) +" 2>/dev/null) || true if [ "$rc" -eq 0 ] && printf '%s' "$reply" | grep -qi "PONG"; then pass "C8: openclaw agent completed turn via compatible endpoint (http-proxy-fix.js FORWARD-mode path exercised)" diff --git a/test/e2e/test-openclaw-inference-switch.sh b/test/e2e/test-openclaw-inference-switch.sh index de2b20b399..05eb033c42 100755 --- a/test/e2e/test-openclaw-inference-switch.sh +++ b/test/e2e/test-openclaw-inference-switch.sh @@ -270,11 +270,24 @@ check_openclaw_agent_turn() { -o ConnectTimeout=10 \ -o LogLevel=ERROR \ "openshell-${SANDBOX_NAME}" \ - "openclaw agent --agent main --json --session-id '${session_id}' -m 'What is 6 multiplied by 7? Reply with only the integer, no extra words.'" \ - 2>&1) || rc=$? + "openclaw agent --agent main --json --thinking off --session-id '${session_id}' -m 'What is 6 multiplied by 7? Reply with only the integer, no extra words.'" \ + 2>/dev/null) || rc=$? rm -f "$ssh_config" - reply=$(printf '%s' "$raw" | python3 "${E2E_DIR}/lib/openclaw-agent-json.py" 2>/dev/null) || true + reply=$(printf '%s' "$raw" | python3 -c ' +import json +import sys +try: + doc = json.load(sys.stdin) +except Exception: + sys.exit(0) +result = doc.get("result") or {} +parts = [] +for payload in result.get("payloads") or []: + if isinstance(payload, dict) and isinstance(payload.get("text"), str): + parts.append(payload["text"]) +print("\n".join(parts)) +' 2>/dev/null) || true if [ "$rc" -eq 0 ] && grep -qE '(^|[^0-9])42([^0-9]|$)' <<<"$reply"; then pass "OpenClaw agent answered through the switched inference route" diff --git a/test/e2e/test-openshell-gateway-upgrade.sh b/test/e2e/test-openshell-gateway-upgrade.sh index 0d9934b852..7a0f2f7859 100755 --- a/test/e2e/test-openshell-gateway-upgrade.sh +++ b/test/e2e/test-openshell-gateway-upgrade.sh @@ -52,7 +52,7 @@ STATE_DIR="${NEMOCLAW_OPENSHELL_GATEWAY_STATE_DIR:-$HOME/.local/state/nemoclaw/o PID_FILE="${STATE_DIR}/openshell-gateway.pid" OLD_NEMOCLAW_REF="${NEMOCLAW_OLD_NEMOCLAW_REF:-v0.0.36}" OLD_OPENSHELL_VERSION="${NEMOCLAW_OLD_OPENSHELL_VERSION:-0.0.36}" -CURRENT_OPENSHELL_VERSION="${NEMOCLAW_CURRENT_OPENSHELL_VERSION:-0.0.44}" +CURRENT_OPENSHELL_VERSION="${NEMOCLAW_CURRENT_OPENSHELL_VERSION:-0.0.39}" SURVIVOR_SANDBOX="${NEMOCLAW_GATEWAY_UPGRADE_SURVIVOR_NAME:-e2e-gateway-upgrade-survivor}" SURVIVOR_MARKER="gateway-upgrade-survivor-$(date +%s)" SURVIVOR_MARKER_PATH="/sandbox/.openclaw/workspace/nemoclaw-gateway-upgrade-marker" @@ -145,7 +145,7 @@ EOF # request-body-credential-rewrite # websocket-credential-rewrite if [ "${1:-}" = "--version" ]; then - printf 'openshell 0.0.44\n' + printf 'openshell 0.0.39\n' exit 0 fi exit 99 @@ -235,7 +235,7 @@ EOF # request-body-credential-rewrite # websocket-credential-rewrite if [ "${1:-}" = "--version" ]; then - printf 'openshell 0.0.44\n' + printf 'openshell 0.0.39\n' exit 0 fi exit 99 @@ -516,8 +516,7 @@ AGENT install_current_nemoclaw_upgrade() { local current_ref - current_ref="${NEMOCLAW_CURRENT_NEMOCLAW_REF:-$(git rev-parse HEAD 2>/dev/null || printf '%s' "${GITHUB_SHA:-}")}" - [ -n "$current_ref" ] || fail "could not determine current NemoClaw ref" + current_ref="${GITHUB_SHA:-$(git rev-parse HEAD)}" run_installer_payload "current ${current_ref:0:12}" "$current_ref" "${REPO_ROOT}/scripts/install.sh" "$CURRENT_INSTALL_LOG" grep -Fq "Accepted experimental OpenShell gateway upgrade" "$CURRENT_INSTALL_LOG" \ || fail "current installer did not exercise the experimental OpenShell gateway upgrade acceptance path" diff --git a/test/e2e/test-openshell-version-pin.sh b/test/e2e/test-openshell-version-pin.sh index 5fe7496fdb..86c3dfdc31 100755 --- a/test/e2e/test-openshell-version-pin.sh +++ b/test/e2e/test-openshell-version-pin.sh @@ -8,11 +8,11 @@ # pinned compatible version instead of failing before the reinstall path. # # Expected result on unfixed main: FAIL. scripts/install-openshell.sh sees the -# fake installed `openshell 0.0.45`, compares it to MAX_VERSION=0.0.44, and -# exits with "above the maximum" before downloading the pinned 0.0.44 release. +# fake installed `openshell 0.0.40`, compares it to MAX_VERSION=0.0.39, and +# exits with "above the maximum" before downloading the pinned 0.0.39 release. # # Expected result after the fix: PASS. The script warns about the too-new -# installed OpenShell, downloads v0.0.44, replaces openshell plus helper +# installed OpenShell, downloads v0.0.39, replaces openshell plus helper # binaries, and exits successfully. set -euo pipefail @@ -74,7 +74,7 @@ SH # the pinned compatible release. write_executable "$FAKE_BIN/openshell" <<'SH' #!/usr/bin/env bash -if [ "${1:-}" = "--version" ]; then echo "openshell 0.0.45"; exit 0; fi +if [ "${1:-}" = "--version" ]; then echo "openshell 0.0.40"; exit 0; fi # request-body-credential-rewrite websocket-credential-rewrite exit 0 SH @@ -163,7 +163,7 @@ exit 0 SH # The installer extracts three archives. Create the binary each archive would -# have produced. The replacement openshell reports 0.0.44 and contains the +# have produced. The replacement openshell reports 0.0.39 and contains the # feature strings checked by install-openshell.sh. write_executable "$FAKE_BIN/tar" <<'SH' #!/usr/bin/env bash @@ -185,7 +185,7 @@ case "$*" in esac cat > "$outdir/$name" <<'EOS' #!/usr/bin/env bash -if [ "${1:-}" = "--version" ]; then echo "openshell 0.0.44"; exit 0; fi +if [ "${1:-}" = "--version" ]; then echo "openshell 0.0.39"; exit 0; fi # request-body-credential-rewrite websocket-credential-rewrite exit 0 EOS @@ -200,7 +200,7 @@ cat "$@" 2>/dev/null || true SH cd "$REPO_ROOT" -info "Running install-openshell.sh with sticky openshell 0.0.45 and max 0.0.44" +info "Running install-openshell.sh with sticky openshell 0.0.40 and max 0.0.39" set +e env \ PATH="$FAKE_BIN:/usr/bin:/bin" \ @@ -211,26 +211,26 @@ install_rc=$? set -e if [ "$install_rc" -ne 0 ]; then - if grep -q "openshell 0.0.45 is above the maximum (0.0.44)" "$INSTALL_LOG"; then - fail "Installer hard-failed on sticky OpenShell 0.0.45 instead of reinstalling pinned 0.0.44 (#3474)" + if grep -q "openshell 0.0.40 is above the maximum (0.0.39)" "$INSTALL_LOG"; then + fail "Installer hard-failed on sticky OpenShell 0.0.40 instead of reinstalling pinned 0.0.39 (#3474)" fi fail "install-openshell.sh failed before proving sticky-version recovery (exit ${install_rc})" fi pass "install-openshell.sh completed" -if ! grep -q "v0.0.44" "$DOWNLOAD_LOG"; then - fail "Expected installer to download pinned OpenShell v0.0.44" +if ! grep -q "v0.0.39" "$DOWNLOAD_LOG"; then + fail "Expected installer to download pinned OpenShell v0.0.39" fi -pass "Installer downloaded pinned OpenShell v0.0.44" +pass "Installer downloaded pinned OpenShell v0.0.39" -if grep -q "v0.0.45" "$DOWNLOAD_LOG"; then - fail "Installer downloaded OpenShell v0.0.45 despite NemoClaw max 0.0.44" +if grep -q "v0.0.40" "$DOWNLOAD_LOG"; then + fail "Installer downloaded OpenShell v0.0.40 despite NemoClaw max 0.0.39" fi -pass "Installer did not download too-new OpenShell v0.0.45" +pass "Installer did not download too-new OpenShell v0.0.40" -if ! "$FAKE_BIN/openshell" --version 2>&1 | grep -q "0.0.44"; then - fail "openshell binary was not replaced with pinned 0.0.44" +if ! "$FAKE_BIN/openshell" --version 2>&1 | grep -q "0.0.39"; then + fail "openshell binary was not replaced with pinned 0.0.39" fi -pass "Sticky openshell 0.0.45 was replaced with pinned 0.0.44" +pass "Sticky openshell 0.0.40 was replaced with pinned 0.0.39" info "OpenShell sticky-version pin guard complete" diff --git a/test/e2e/test-sandbox-operations.sh b/test/e2e/test-sandbox-operations.sh index 7afd569f84..568e8ac5f5 100755 --- a/test/e2e/test-sandbox-operations.sh +++ b/test/e2e/test-sandbox-operations.sh @@ -347,48 +347,61 @@ test_sbx_01_list_sandboxes() { # # 1. Uses `openclaw agent --json`, which calls routeLogsToStderr() in # openclaw/src/commands/agent-via-gateway.ts:57 so stdout is a clean -# JSON envelope. Merged stdout/stderr is preserved for failure -# diagnostics, but assertions only read JSON payload text. +# JSON envelope. Stderr is dropped (2>/dev/null) so any prompt-echo +# or wrapped error there cannot satisfy the assertion. # 2. The expected token (the integer 42) is not a literal substring of # the prompt, so an error path that quoted the prompt back cannot # false-positive the grep — which is what masked the openclaw 4.9 # SSRF regression from the prior `Say exactly: HELLO_E2E` assertion. -# 3. Asserts on payload text from the JSON envelope, not on merged -# stdout/stderr. -# 4. Relies on generated `thinkingDefault: off` config so the first-turn -# smoke contract is not delayed by model-catalog inferred reasoning -# defaults without depending on transient CLI flags. +# 3. Asserts on `result.payloads[].text` from the JSON envelope, not on +# merged stdout/stderr. +# 4. Pins `--thinking off` so the first-turn smoke contract is not delayed +# by model-catalog inferred reasoning defaults. test_sbx_02_connect_chat() { log "=== TC-SBX-02: Connect & Chat ===" require_sandbox "$SANDBOX_A" "TC-SBX-02" || return log " Sending one-shot message to agent via SSH (openclaw agent --json)..." - local session_id raw ssh_cfg rc + local session_id raw ssh_cfg session_id="e2e-sbx-02-$(date +%s)-$$" - # Use a direct ssh invocation rather than sandbox_exec() so the JSON envelope - # is easy to parse while still preserving stderr in failure output. + # Use a direct ssh invocation rather than sandbox_exec(): sandbox_exec_for + # merges stderr into stdout via 2>&1 so it can log non-zero exits, which + # would pollute the JSON document we need to parse below. Drop stderr at + # the source so node deprecation warnings (UNDICI-EHPA, etc.) and + # progress-bar bytes from openclaw cannot trip up json.load(). ssh_cfg="$(mktemp)" if ! openshell sandbox ssh-config "$SANDBOX_A" >"$ssh_cfg" 2>/dev/null; then rm -f "$ssh_cfg" fail "TC-SBX-02: Connect & Chat" "Failed to fetch SSH config for '$SANDBOX_A'" return fi - rc=0 raw=$(run_with_timeout 90 ssh -F "$ssh_cfg" \ -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ -o ConnectTimeout=10 -o LogLevel=ERROR \ "openshell-${SANDBOX_A}" \ - "openclaw agent --agent main --json --session-id '${session_id}' -m 'What is 6 multiplied by 7? Reply with only the integer, no extra words.'" \ - 2>&1) || rc=$? + "openclaw agent --agent main --json --thinking off --session-id '${session_id}' -m 'What is 6 multiplied by 7? Reply with only the integer, no extra words.'" \ + 2>/dev/null) || true rm -f "$ssh_cfg" local reply - reply=$(printf '%s' "$raw" | python3 "${SCRIPT_DIR_TIMEOUT}/lib/openclaw-agent-json.py" 2>/dev/null) || true - - if [[ $rc -eq 0 && -n "$reply" ]] && echo "$reply" | grep -qE "(^|[^0-9])42([^0-9]|$)"; then + reply=$(echo "$raw" | python3 -c " +import json, sys +try: + doc = json.load(sys.stdin) +except Exception: + sys.exit(0) +result = doc.get('result') or {} +parts = [] +for p in result.get('payloads') or []: + if isinstance(p, dict) and isinstance(p.get('text'), str): + parts.append(p['text']) +print('\n'.join(parts)) +" 2>/dev/null) || true + + if [[ -n "$reply" ]] && echo "$reply" | grep -qE "(^|[^0-9])42([^0-9]|$)"; then pass "TC-SBX-02: Agent computed 6×7=42 through openclaw → inference.local" else - fail "TC-SBX-02: Connect & Chat" "Expected '42' in agent reply (rc=$rc); reply='${reply:0:200}'; raw output='${raw:0:200}'" + fail "TC-SBX-02: Connect & Chat" "Expected '42' in agent reply; reply='${reply:0:200}'; raw stdout='${raw:0:200}'" fi } diff --git a/test/fetch-guard-patch-regression.test.ts b/test/fetch-guard-patch-regression.test.ts index dacf6c1c36..2cfbad745c 100644 --- a/test/fetch-guard-patch-regression.test.ts +++ b/test/fetch-guard-patch-regression.test.ts @@ -163,103 +163,4 @@ if (globalThis.proxyChecks.length !== 0) throw new Error('sandbox proxy validati fs.rmSync(tmp, { recursive: true, force: true }); } }); - - it("keeps the Dockerfile OpenClaw source-shape patches aligned with current dist", () => { - const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-openclaw-patches-")); - const dist = path.join(tmp, "dist"); - fs.mkdirSync(dist, { recursive: true }); - fs.writeFileSync( - path.join(dist, "fetch-guard-test.js"), - [ - "const withStrictGuardedFetchMode = Symbol('strict');", - "const withTrustedEnvProxyGuardedFetchMode = Symbol('trusted');", - "async function assertExplicitProxyAllowed(proxyUrl) { throw new Error(proxyUrl); }", - "export { withStrictGuardedFetchMode as a, withTrustedEnvProxyGuardedFetchMode as b };", - "", - ].join("\n"), - ); - fs.writeFileSync( - path.join(dist, "install-safe-path-test.js"), - "const baseLstat = await fs.lstat(baseDir);\n", - ); - fs.writeFileSync( - path.join(dist, "install-package-dir-test.js"), - [ - "async function assertInstallBaseStable(params) {", - " const baseLstat = await fs.lstat(params.installBaseDir);", - " if (baseLstat.isSymbolicLink()) throw new Error('symlink');", - "}", - "", - ].join("\n"), - ); - fs.writeFileSync( - path.join(dist, "client-test.js"), - "const DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS = 15e3;\n", - ); - fs.writeFileSync( - path.join(dist, "server.impl-test.js"), - "const DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS = 15e3;\n", - ); - - const command = dockerRunCommandBetween( - "# Patch OpenClaw media fetch for proxy-only sandbox", - "# Patch OpenClaw's pinned", - ).replaceAll("/usr/local/lib/node_modules/openclaw/dist", dist); - const fakeBin = path.join(tmp, "bin"); - fs.mkdirSync(fakeBin); - const sedWrapper = path.join(fakeBin, "sed"); - fs.writeFileSync( - sedWrapper, - [ - "#!/usr/bin/env bash", - "set -euo pipefail", - "extended=0", - 'if [ "${1:-}" = "-i" ]; then', - ' if [ "${2:-}" = "-E" ]; then', - " extended=1", - " expr=$3", - " shift 3", - " else", - " expr=$2", - " shift 2", - " fi", - ' for file in "$@"; do', - " tmp=$(mktemp)", - ' if [ "$extended" = "1" ]; then', - ' /usr/bin/sed -E "$expr" "$file" > "$tmp"', - " else", - ' /usr/bin/sed "$expr" "$file" > "$tmp"', - " fi", - ' mv "$tmp" "$file"', - " done", - " exit 0", - "fi", - 'exec /usr/bin/sed "$@"', - ].join("\n"), - { mode: 0o755 }, - ); - const scriptPath = path.join(tmp, "patch-all.sh"); - fs.writeFileSync(scriptPath, ["#!/usr/bin/env bash", command].join("\n"), { mode: 0o700 }); - - try { - const patch = spawnSync("bash", [scriptPath], { - encoding: "utf-8", - env: { ...process.env, PATH: `${fakeBin}:${process.env.PATH || ""}` }, - timeout: 5000, - }); - expect(patch.status, `${patch.stdout}${patch.stderr}`).toBe(0); - const patched = fs.readdirSync(dist).map((file) => fs.readFileSync(path.join(dist, file), "utf-8")); - expect(patched.join("\n")).not.toContain("DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS = 15e3"); - expect(patched.join("\n")).not.toContain("DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS = 1e4"); - expect(patched.join("\n").match(/DEFAULT_PREAUTH_HANDSHAKE_TIMEOUT_MS = 6e4/g)).toHaveLength(2); - expect(fs.readFileSync(path.join(dist, "install-safe-path-test.js"), "utf-8")).toContain( - "const baseLstat = await fs.stat(baseDir)", - ); - expect(fs.readFileSync(path.join(dist, "install-package-dir-test.js"), "utf-8")).toContain( - "const baseLstat = await fs.stat(params.installBaseDir)", - ); - } finally { - fs.rmSync(tmp, { recursive: true, force: true }); - } - }); }); diff --git a/test/generate-openclaw-config.test.ts b/test/generate-openclaw-config.test.ts index 02505ad407..d14c0b2b58 100644 --- a/test/generate-openclaw-config.test.ts +++ b/test/generate-openclaw-config.test.ts @@ -61,12 +61,6 @@ function runConfigScript(envOverrides: Record = {}): any { return JSON.parse(fs.readFileSync(configPath, "utf-8")); } -function writeWeChatPluginMetadata(manifest: Record) { - const pluginDir = path.join(tmpDir, ".openclaw", "extensions", "openclaw-weixin"); - fs.mkdirSync(pluginDir, { recursive: true }); - fs.writeFileSync(path.join(pluginDir, "openclaw.plugin.json"), JSON.stringify(manifest, null, 2)); -} - function writeRegistryManifest( blueprintDir: string, relativeManifestPath: string, @@ -273,7 +267,7 @@ describe("generate-openclaw-config.py: config generation", () => { const configPath = path.join(tmpDir, ".openclaw", "openclaw.json"); const installEntry = { source: "npm", - spec: "@tencent-weixin/openclaw-weixin@2.4.3", + spec: "@tencent-weixin/openclaw-weixin@2.4.2", }; fs.mkdirSync(path.dirname(configPath), { recursive: true }); fs.writeFileSync( @@ -311,49 +305,6 @@ describe("generate-openclaw-config.py: config generation", () => { }); }); - it("seeds channels.openclaw-weixin and restores install registry when installed WeChat plugin metadata exists", () => { - writeWeChatPluginMetadata({ - id: "openclaw-weixin", - channels: ["openclaw-weixin"], - channelConfigs: { "openclaw-weixin": {} }, - }); - - const channels = Buffer.from(JSON.stringify(["wechat"])).toString("base64"); - const wechatConfig = Buffer.from( - JSON.stringify({ accountId: "primary", baseUrl: "https://example", userId: "u1" }), - ).toString("base64"); - const config = runConfigScript({ - NEMOCLAW_MESSAGING_CHANNELS_B64: channels, - NEMOCLAW_WECHAT_CONFIG_B64: wechatConfig, - }); - - expect(config.plugins?.installs?.["openclaw-weixin"]).toEqual({ - source: "npm", - spec: "@tencent-weixin/openclaw-weixin@2.4.3", - }); - expect(config.channels?.["openclaw-weixin"]?.accounts?.primary).toEqual({ - enabled: true, - }); - expect(config.channels?.wechat).toBeUndefined(); - }); - - it("seeds channels.openclaw-weixin when the Dockerfile marks the plugin preinstalled", () => { - const channels = Buffer.from(JSON.stringify(["wechat"])).toString("base64"); - const wechatConfig = Buffer.from( - JSON.stringify({ accountId: "primary", baseUrl: "https://example", userId: "u1" }), - ).toString("base64"); - const config = runConfigScript({ - NEMOCLAW_MESSAGING_CHANNELS_B64: channels, - NEMOCLAW_WECHAT_CONFIG_B64: wechatConfig, - NEMOCLAW_OPENCLAW_WECHAT_PLUGIN_PREINSTALLED: "1", - }); - - expect(config.channels?.["openclaw-weixin"]?.accounts?.primary).toEqual({ - enabled: true, - }); - expect(config.channels?.wechat).toBeUndefined(); - }); - it("omits channels.openclaw-weixin when no accountId was captured", () => { // No QR-login result → seed step bails on the empty accountId and // leaves openclaw.json untouched, so the bridge stays dormant. @@ -376,7 +327,7 @@ describe("generate-openclaw-config.py: config generation", () => { fs.mkdirSync(path.dirname(configPath), { recursive: true }); const installEntry = { source: "npm", - spec: "@tencent-weixin/openclaw-weixin@2.4.3", + spec: "@tencent-weixin/openclaw-weixin@2.4.2", }; fs.writeFileSync( configPath, diff --git a/test/install-openshell-version-check.test.ts b/test/install-openshell-version-check.test.ts index 59a329681c..fd0d78a949 100644 --- a/test/install-openshell-version-check.test.ts +++ b/test/install-openshell-version-check.test.ts @@ -124,34 +124,34 @@ exit 0`, } describe("install-openshell.sh version check", { timeout: 15_000 }, () => { - it("exits cleanly when openshell 0.0.44 and driver binaries are already installed", () => { - const result = runWithInstalledVersion("0.0.44"); + it("exits cleanly when openshell 0.0.39 and driver binaries are already installed", () => { + const result = runWithInstalledVersion("0.0.39"); expect(result.status).toBe(0); - expect(result.stdout).toMatch(/already installed.*0\.0\.44/); + expect(result.stdout).toMatch(/already installed.*0\.0\.39/); }); - it("triggers reinstall when openshell 0.0.44 is missing Docker-driver binaries", () => { - const result = runWithInstalledVersion("0.0.44", {}, { driverBins: false, os: "Linux" }); + it("triggers reinstall when openshell 0.0.39 is missing Docker-driver binaries", () => { + const result = runWithInstalledVersion("0.0.39", {}, { driverBins: false, os: "Linux" }); expect(result.status).not.toBe(0); expect(result.stdout).toMatch(/missing Docker-driver binaries/); - expect(result.stdout).toMatch(/Installing OpenShell from release 'v0\.0\.44'/); + expect(result.stdout).toMatch(/Installing OpenShell from release 'v0\.0\.39'/); }); - it("fails closed when openshell 0.0.44 lacks required messaging rewrite support", () => { - const result = runWithInstalledVersion("0.0.44", {}, { capability: false }); + it("fails closed when openshell 0.0.39 lacks required messaging rewrite support", () => { + const result = runWithInstalledVersion("0.0.39", {}, { capability: false }); expect(result.status).toBe(1); // `fail()` writes to stderr as of #3446; previously stdout. expect(result.stderr).toMatch(/missing request-body-credential-rewrite support/); }); - it("accepts macOS openshell 0.0.44 when the gateway binary is installed", () => { - const result = runWithInstalledVersion("0.0.44", {}, { + it("accepts macOS openshell 0.0.39 when the gateway binary is installed", () => { + const result = runWithInstalledVersion("0.0.39", {}, { driverBins: "gateway", os: "Darwin", arch: "arm64", }); expect(result.status).toBe(0); - expect(result.stdout).toMatch(/already installed.*0\.0\.44/); + expect(result.stdout).toMatch(/already installed.*0\.0\.39/); }); it("does not require the macOS VM driver entitlement for Docker-driver onboarding", () => { @@ -160,7 +160,7 @@ describe("install-openshell.sh version check", { timeout: 15_000 }, () => { const state = path.join(tmp, "codesign-state"); const log = path.join(tmp, "codesign.log"); const result = runWithInstalledVersion( - "0.0.44", + "0.0.39", { NEMOCLAW_FAKE_CODESIGN_HAS_ENTITLEMENT: "0", NEMOCLAW_FAKE_CODESIGN_STATE: state, @@ -174,7 +174,7 @@ describe("install-openshell.sh version check", { timeout: 15_000 }, () => { ); expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); - expect(result.stdout).toMatch(/already installed.*0\.0\.44/); + expect(result.stdout).toMatch(/already installed.*0\.0\.39/); expect(result.stdout).not.toMatch(/missing the macOS Hypervisor entitlement/); expect(result.stdout).not.toMatch(/Signing openshell-driver-vm/); expect(result.stdout).not.toMatch(/Installing OpenShell from release/); @@ -184,15 +184,15 @@ describe("install-openshell.sh version check", { timeout: 15_000 }, () => { } }); - it("triggers reinstall on macOS when openshell 0.0.44 is missing required gateway binaries", () => { - const result = runWithInstalledVersion("0.0.44", {}, { + it("triggers reinstall on macOS when openshell 0.0.39 is missing required gateway binaries", () => { + const result = runWithInstalledVersion("0.0.39", {}, { driverBins: false, os: "Darwin", arch: "arm64", }); expect(result.status).not.toBe(0); expect(result.stdout).toMatch(/missing Docker-driver binaries/); - expect(result.stdout).toMatch(/Installing OpenShell from release 'v0\.0\.44'/); + expect(result.stdout).toMatch(/Installing OpenShell from release 'v0\.0\.39'/); }); it("downloads the macOS arm64 gateway asset during reinstall", () => { @@ -266,7 +266,7 @@ dest="\${@: -1}" mkdir -p "$(dirname "$dest")" cat > "$dest" <<'EOF' #!/usr/bin/env bash -if [ "\${1:-}" = "--version" ]; then echo "openshell 0.0.44"; exit 0; fi +if [ "\${1:-}" = "--version" ]; then echo "openshell 0.0.39"; exit 0; fi # request-body-credential-rewrite websocket-credential-rewrite exit 0 EOF @@ -388,7 +388,7 @@ printf '%s\\n' "$dest" >> ${JSON.stringify(installLog)} mkdir -p "$(dirname "$dest")" case "$(basename "$dest")" in openshell) - printf '#!/usr/bin/env bash\\nif [ "$1" = "--version" ]; then echo "openshell 0.0.44"; else exit 0; fi\\n# request-body-credential-rewrite websocket-credential-rewrite\\n' > "$dest" + printf '#!/usr/bin/env bash\\nif [ "$1" = "--version" ]; then echo "openshell 0.0.39"; else exit 0; fi\\n# request-body-credential-rewrite websocket-credential-rewrite\\n' > "$dest" ;; *) printf '#!/usr/bin/env bash\\nexit 0\\n' > "$dest" @@ -445,23 +445,23 @@ exit 0`, }); it("reinstalls the pinned release when openshell is above MAX_VERSION", () => { - const result = runWithInstalledVersion("0.0.45"); + const result = runWithInstalledVersion("0.0.40"); expect(result.status).not.toBe(0); - expect(result.stdout).toMatch(/above the maximum.*reinstalling pinned OpenShell 0\.0\.44/); - expect(result.stdout).toMatch(/Installing OpenShell from release 'v0\.0\.44'/); + expect(result.stdout).toMatch(/above the maximum.*reinstalling pinned OpenShell 0\.0\.39/); + expect(result.stdout).toMatch(/Installing OpenShell from release 'v0\.0\.39'/); expect(result.stderr).not.toMatch(/Upgrade NemoClaw first/); }); it("reinstalls the pinned release when openshell is at a much newer version", () => { const result = runWithInstalledVersion("0.1.0"); expect(result.status).not.toBe(0); - expect(result.stdout).toMatch(/above the maximum.*reinstalling pinned OpenShell 0\.0\.44/); - expect(result.stdout).toMatch(/Installing OpenShell from release 'v0\.0\.44'/); + expect(result.stdout).toMatch(/above the maximum.*reinstalling pinned OpenShell 0\.0\.39/); + expect(result.stdout).toMatch(/Installing OpenShell from release 'v0\.0\.39'/); expect(result.stderr).not.toMatch(/Upgrade NemoClaw first/); }); it("accepts an installed OpenShell dev-channel Docker-driver build", () => { - const result = runWithInstalledVersion("0.0.44.dev84+g6b2180425", { + const result = runWithInstalledVersion("0.0.39.dev84+g6b2180425", { NEMOCLAW_OPENSHELL_CHANNEL: "dev", }); expect(result.status).toBe(0); diff --git a/test/kimi-inference-compat-plugin.test.ts b/test/kimi-inference-compat-plugin.test.ts index bfaf729f08..b4df0662e7 100644 --- a/test/kimi-inference-compat-plugin.test.ts +++ b/test/kimi-inference-compat-plugin.test.ts @@ -172,26 +172,6 @@ describe("nemoclaw Kimi inference compat plugin", () => { ]); }); - it("normalizes mixed already-split and combined exec commands from OpenClaw trajectories", () => { - const message = { - ...toolMessage("ignored"), - content: [ - toolMessage("hostname", { id: "call_hostname" }).content[0], - toolMessage("date", { id: "call_date" }).content[0], - toolMessage("hostname; date; uptime", { id: "call_combined" }).content[0], - ], - }; - - expect(plugin.__testing.rewriteSafeCombinedExecToolCallInMessage(message)).toBe(true); - - expect(message.content.map((block: any) => block.arguments.command)).toEqual([ - "hostname", - "date", - "uptime", - ]); - expect(JSON.stringify(message)).not.toContain("hostname; date; uptime"); - }); - it.each([ "hostname && date && uptime", "hostname; date; uptime > /tmp/out", @@ -362,72 +342,4 @@ describe("nemoclaw Kimi inference compat plugin", () => { "uptime", ]); }); - - it("rewrites object tool-call deltas at their content index without retaining compound commands", () => { - const event = { - type: "toolcall_delta", - contentIndex: 2, - delta: { command: "hostname; date; uptime" }, - partial: { - ...toolMessage("ignored"), - content: [ - toolMessage("hostname", { id: "call_hostname" }).content[0], - toolMessage("date", { id: "call_date" }).content[0], - toolMessage("hostname; date; uptime", { id: "call_combined" }).content[0], - ], - }, - toolCall: toolMessage("hostname; date; uptime", { id: "call_combined" }).content[0], - }; - - expect(plugin.__testing.rewriteSafeCombinedExecToolCallInEvent(event)).toBe(true); - - expect(event.delta).toEqual({ command: "uptime" }); - expect(event.partial.content.map((block: any) => block.arguments.command)).toEqual([ - "hostname", - "date", - "uptime", - ]); - expect(event.toolCall.arguments.command).toBe("uptime"); - expect(JSON.stringify(event)).not.toContain("hostname; date; uptime"); - }); - - it("does not reapply a delta split at a stale content index after rewriting partial content", () => { - const event = { - type: "toolcall_delta", - contentIndex: 1, - delta: { command: "uptime; date" }, - partial: { - ...toolMessage("ignored"), - content: [ - toolMessage("hostname; date", { id: "call_first" }).content[0], - toolMessage("uptime; date", { id: "call_second" }).content[0], - ], - }, - message: { - ...toolMessage("ignored"), - content: [ - toolMessage("hostname; date", { id: "call_first" }).content[0], - toolMessage("uptime; date", { id: "call_second" }).content[0], - ], - }, - toolCall: toolMessage("uptime; date", { id: "call_second" }).content[0], - }; - - expect(plugin.__testing.rewriteSafeCombinedExecToolCallInEvent(event)).toBe(true); - - expect(event.partial.content.map((block: any) => block.arguments.command)).toEqual([ - "hostname", - "date", - "uptime", - ]); - expect(event.message.content.map((block: any) => block.arguments.command)).toEqual([ - "hostname", - "date", - "uptime", - ]); - expect(event.delta).toEqual({ command: "date" }); - expect(event.toolCall.arguments.command).toBe("date"); - expect(JSON.stringify(event)).not.toContain("hostname; date"); - expect(JSON.stringify(event)).not.toContain("uptime; date"); - }); }); diff --git a/test/openclaw-agent-json.test.ts b/test/openclaw-agent-json.test.ts deleted file mode 100644 index e09d2e1fdc..0000000000 --- a/test/openclaw-agent-json.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -// @ts-nocheck -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { describe, expect, it } from "vitest"; -import path from "node:path"; -import { spawnSync } from "node:child_process"; - -const HELPER = path.join( - import.meta.dirname, - "e2e", - "lib", - "openclaw-agent-json.py", -); - -function runHelper(input: string) { - return spawnSync("python3", [HELPER], { - input, - encoding: "utf-8", - stdio: ["pipe", "pipe", "pipe"], - }); -} - -describe("openclaw-agent-json.py", () => { - it("extracts nested result payload text", () => { - const result = runHelper(JSON.stringify({ result: { payloads: [{ text: "42" }] } })); - expect(result.status).toBe(0); - expect(result.stdout).toBe("42\n"); - expect(result.stderr).toBe(""); - }); - - it("extracts top-level payload text", () => { - const result = runHelper(JSON.stringify({ payloads: [{ text: "PONG" }] })); - expect(result.status).toBe(0); - expect(result.stdout).toBe("PONG\n"); - expect(result.stderr).toBe(""); - }); - - it("prints an empty line for valid JSON with no text payloads", () => { - const result = runHelper(JSON.stringify({ payloads: [{ mediaUrl: null }] })); - expect(result.status).toBe(0); - expect(result.stdout).toBe("\n"); - expect(result.stderr).toBe(""); - }); - - it("exits nonzero for invalid JSON", () => { - const result = runHelper("{not json"); - expect(result.status).not.toBe(0); - expect(result.stderr).toContain("invalid JSON"); - }); - - it("extracts JSON when OpenClaw logs precede the envelope", () => { - const result = runHelper(`progress line\n${JSON.stringify({ payloads: [{ text: "PONG" }] })}`); - expect(result.status).toBe(0); - expect(result.stdout).toBe("PONG\n"); - }); - - it("extracts payload text from later JSON envelopes in a stream", () => { - const result = runHelper([ - JSON.stringify({ payloads: [] }), - "progress line", - JSON.stringify({ payloads: [{ text: "42" }] }), - ].join("\n")); - expect(result.status).toBe(0); - expect(result.stdout).toBe("42\n"); - }); -}); diff --git a/test/openclaw-tool-catalog-patch.test.ts b/test/openclaw-tool-catalog-patch.test.ts index a60cad46e1..dd025ed623 100644 --- a/test/openclaw-tool-catalog-patch.test.ts +++ b/test/openclaw-tool-catalog-patch.test.ts @@ -118,31 +118,6 @@ function realToolFixtureSource(allCustomToolsLine: string) { ].join("\n"); } -function nativeToolSearchFixtureSource() { - return [ - "const uncompactedEffectiveTools = [...tools, ...filteredBundledTools];", - "let effectiveTools = uncompactedEffectiveTools;", - "const toolSearch = applyToolSearchCatalog({", - "\ttools: effectiveTools,", - "\tconfig: params.config", - "});", - "effectiveTools = toolSearch.tools;", - "const toolSearchRunPlan = buildToolSearchRunPlan({", - "\tvisibleTools: effectiveTools,", - "\tuncompactedTools: uncompactedEffectiveTools", - "});", - "const allowedToolNames = toolSearchRunPlan.visibleAllowedToolNames;", - "const replayAllowedToolNames = toolSearchRunPlan.replayAllowedToolNames;", - "const { customTools } = splitSdkTools({ tools: effectiveTools });", - "const clientToolDefs = [];", - "\t\t\tconst allCustomTools = [...customTools, ...clientToolDefs];", - "void allowedToolNames;", - "void replayAllowedToolNames;", - "void allCustomTools;", - "", - ].join("\n"); -} - function makeFixture(opts: { version?: string; allCustomToolsLine?: string } = {}) { const root = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-tool-catalog-patch-")); const dist = path.join(root, "dist"); @@ -213,19 +188,6 @@ describe("OpenClaw compact tool catalog patch", () => { fs.rmSync(changed.root, { recursive: true, force: true }); } - const native = makeFixture(); - try { - fs.writeFileSync(native.selectionPath, nativeToolSearchFixtureSource()); - const result = runPatch(native.dist); - expect(result.status, `${result.stdout}${result.stderr}`).toBe(0); - expect(result.stdout).toContain("native-tool-search"); - const unmodified = fs.readFileSync(native.selectionPath, "utf-8"); - expect(unmodified).not.toContain(MARKER); - expect(unmodified).toContain("buildToolSearchRunPlan"); - } finally { - fs.rmSync(native.root, { recursive: true, force: true }); - } - const partial = makeFixture({ allCustomToolsLine: [ MARKER, diff --git a/test/policies.test.ts b/test/policies.test.ts index b4b9709452..fe6590a927 100644 --- a/test/policies.test.ts +++ b/test/policies.test.ts @@ -404,7 +404,6 @@ describe("policies", () => { const hosts = policies.getPresetEndpoints(content); expect(hosts).toContain("ilinkai.weixin.qq.com"); expect(hosts).toContain("ilinkai.wechat.com"); - expect(hosts.every((host: string) => !host.includes("`"))).toBe(true); }); it("every preset has at least one endpoint", () => { @@ -420,16 +419,6 @@ describe("policies", () => { const hosts = policies.getPresetEndpoints(yaml); expect(hosts).toEqual(["example.com", "other.com"]); }); - - it("ignores commented host examples and inline comments", () => { - const yaml = [ - "# matches `host:` as text", - " # host: commented.example.com", - " - host: real.example.com # host: ignored.example.com", - ].join("\n"); - const hosts = policies.getPresetEndpoints(yaml); - expect(hosts).toEqual(["real.example.com"]); - }); }); describe("getMessagingPresetWarning", () => { diff --git a/test/seed-wechat-accounts.test.ts b/test/seed-wechat-accounts.test.ts index 52905d318e..e093785144 100644 --- a/test/seed-wechat-accounts.test.ts +++ b/test/seed-wechat-accounts.test.ts @@ -54,12 +54,6 @@ function writeOpenclawConfig(extra: Record = {}) { return cfgPath; } -function writeWeChatPluginMetadata(manifest: Record) { - const pluginDir = path.join(tmpDir, ".openclaw", "extensions", "openclaw-weixin"); - fs.mkdirSync(pluginDir, { recursive: true }); - fs.writeFileSync(path.join(pluginDir, "openclaw.plugin.json"), JSON.stringify(manifest, null, 2)); -} - function readJson(p: string): any { return JSON.parse(fs.readFileSync(p, "utf-8")); } @@ -204,38 +198,6 @@ describe("seed-wechat-accounts.py: openclaw.json patching (channels.openclaw-wei expect(cfg.channels["openclaw-weixin"].accounts.primary.enabled).toBe(true); }); - it("derives the WeChat channel id from installed plugin metadata", () => { - writeOpenclawConfig(); - writeWeChatPluginMetadata({ - id: "openclaw-weixin", - channels: ["vendor-weixin"], - channelConfigs: { "vendor-weixin": {} }, - }); - const result = runSeed({ - NEMOCLAW_WECHAT_CONFIG_B64: configB64({ accountId: "primary" }), - }); - expect(result.status).toBe(0); - - const cfg = readJson(path.join(tmpDir, ".openclaw", "openclaw.json")); - expect(cfg.channels["vendor-weixin"].accounts.primary.enabled).toBe(true); - }); - - it("keeps the legacy openclaw-weixin channel registration for older plugin loads", () => { - writeOpenclawConfig(); - writeWeChatPluginMetadata({ - id: "openclaw-weixin", - channels: ["vendor-weixin"], - channelConfigs: { "vendor-weixin": {} }, - }); - runSeed({ - NEMOCLAW_WECHAT_CONFIG_B64: configB64({ accountId: "primary" }), - }); - - const cfg = readJson(path.join(tmpDir, ".openclaw", "openclaw.json")); - expect(cfg.channels["vendor-weixin"].accounts.primary.enabled).toBe(true); - expect(cfg.channels["openclaw-weixin"].accounts.primary.enabled).toBe(true); - }); - it("writes a channelConfigUpdatedAt in JS Date.toISOString() shape (ms + 'Z')", () => { // The upstream plugin compares this string with values it produces via // Date.toISOString(). A Python isoformat() with offset would diverge. @@ -294,7 +256,7 @@ describe("seed-wechat-accounts.py: openclaw.json patching (channels.openclaw-wei const cfg = readJson(path.join(tmpDir, ".openclaw", "openclaw.json")); expect(cfg.plugins.installs["openclaw-weixin"]).toEqual({ source: "npm", - spec: "@tencent-weixin/openclaw-weixin@2.4.3", + spec: "@tencent-weixin/openclaw-weixin@2.4.2", }); expect(cfg.plugins.entries["openclaw-weixin"].enabled).toBe(true); expect(Object.keys(cfg.channels)).toEqual(["telegram", "slack", "openclaw-weixin"]);