diff --git a/.github/workflows/nightly-e2e.yaml b/.github/workflows/nightly-e2e.yaml index 5badfde8d8b..e31a6d64cf7 100644 --- a/.github/workflows/nightly-e2e.yaml +++ b/.github/workflows/nightly-e2e.yaml @@ -91,7 +91,7 @@ on: description: >- Comma-separated job names to run (empty = all). Valid: cloud-e2e, cloud-onboard-e2e, cloud-inference-e2e, - skill-agent-e2e, docs-validation-e2e, messaging-providers-e2e, + skill-agent-e2e, openclaw-skill-cli-e2e, docs-validation-e2e, messaging-providers-e2e, openclaw-slack-pairing-e2e, openclaw-tui-chat-correlation-e2e, issue-3600-gpu-proof-optional-e2e, @@ -232,6 +232,21 @@ jobs: env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-skill-agent"}' nvidia_api_key: true secrets: *nightly-e2e-default-secrets + openclaw-skill-cli-e2e: + if: >- + github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' || + inputs.jobs == '' || + contains(format(',{0},', inputs.jobs), ',openclaw-skill-cli-e2e,')) + uses: ./.github/workflows/e2e-script.yaml + with: + ref: ${{ inputs.target_ref || github.ref }} + script: test/e2e/test-openclaw-skill-cli-e2e.sh + timeout_minutes: 25 + artifact_name: "install-log-openclaw-skill-cli" + artifact_path: "/tmp/nemoclaw-e2e-openclaw-skill-cli-install.log" + env_json: '{"NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-openclaw-skill-cli"}' + nvidia_api_key: true + secrets: *nightly-e2e-default-secrets docs-validation-e2e: if: >- github.repository == 'NVIDIA/NemoClaw' && @@ -1899,6 +1914,7 @@ jobs: cloud-onboard-e2e, cloud-inference-e2e, skill-agent-e2e, + openclaw-skill-cli-e2e, docs-validation-e2e, messaging-providers-e2e, openclaw-slack-pairing-e2e, @@ -2007,6 +2023,7 @@ jobs: cloud-onboard-e2e, cloud-inference-e2e, skill-agent-e2e, + openclaw-skill-cli-e2e, docs-validation-e2e, messaging-providers-e2e, openclaw-slack-pairing-e2e, @@ -2172,6 +2189,7 @@ jobs: cloud-onboard-e2e, cloud-inference-e2e, skill-agent-e2e, + openclaw-skill-cli-e2e, docs-validation-e2e, messaging-providers-e2e, openclaw-slack-pairing-e2e, diff --git a/scripts/nemoclaw-start.sh b/scripts/nemoclaw-start.sh index 114320b4bdf..65e4228740f 100755 --- a/scripts/nemoclaw-start.sh +++ b/scripts/nemoclaw-start.sh @@ -1994,7 +1994,7 @@ export https_proxy="$_PROXY_URL" export no_proxy="$_NO_PROXY_VAL" PROXYEOF local _openclaw_env_name _openclaw_env_value _escaped_openclaw_env_value - for _openclaw_env_name in OPENCLAW_HOME OPENCLAW_STATE_DIR OPENCLAW_CONFIG_PATH OPENCLAW_OAUTH_DIR; do + for _openclaw_env_name in OPENCLAW_HOME OPENCLAW_STATE_DIR OPENCLAW_CONFIG_PATH OPENCLAW_OAUTH_DIR OPENCLAW_WORKSPACE_DIR; do _openclaw_env_value="${!_openclaw_env_name:-}" [ -n "$_openclaw_env_value" ] || continue _escaped_openclaw_env_value="$(printf '%s' "$_openclaw_env_value" | sed "s/'/'\\\\''/g")" diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index b287a2b5567..181decec821 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -3563,11 +3563,8 @@ async function createSandbox( // 18789 and the gateway listens on the wrong port. (#2267, #1925) const effectiveDashboardPort = getDashboardForwardPort(chatUiUrl); envArgs.push(formatEnvAssignment("NEMOCLAW_DASHBOARD_PORT", effectiveDashboardPort)); - onboardHermesDashboard.appendHermesDashboardEnvArgs( - envArgs, - hermesDashboardState, - formatEnvAssignment, - ); + require("./onboard/openclaw-runtime-env").appendOpenClawRuntimeEnvArgs(envArgs, agent); + onboardHermesDashboard.appendHermesDashboardEnvArgs(envArgs, hermesDashboardState, formatEnvAssignment); require("./onboard/host-proxy-env").appendHostProxyEnvArgs(envArgs); // Propagate NEMOCLAW_PROXY_HOST / NEMOCLAW_PROXY_PORT to the runtime // sandbox container. patchStagedDockerfile() already substitutes them diff --git a/src/lib/onboard/openclaw-runtime-env.test.ts b/src/lib/onboard/openclaw-runtime-env.test.ts new file mode 100644 index 00000000000..b66dac65925 --- /dev/null +++ b/src/lib/onboard/openclaw-runtime-env.test.ts @@ -0,0 +1,56 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, expect, it } from "vitest"; +import { appendOpenClawRuntimeEnvArgs } from "./openclaw-runtime-env"; + +describe("appendOpenClawRuntimeEnvArgs", () => { + it("pins HOME, STATE_DIR, and WORKSPACE_DIR for the default OpenClaw config dir", () => { + const envArgs: string[] = []; + appendOpenClawRuntimeEnvArgs(envArgs, null); + expect(envArgs).toEqual([ + "OPENCLAW_HOME=/sandbox", + "OPENCLAW_STATE_DIR=/sandbox/.openclaw", + "OPENCLAW_WORKSPACE_DIR=/sandbox/.openclaw/workspace", + ]); + }); + + it("derives the env values from agent.configPaths.dir when supplied for an OpenClaw agent", () => { + const envArgs: string[] = []; + appendOpenClawRuntimeEnvArgs(envArgs, { + name: "openclaw", + configPaths: { dir: "/srv/agent-root/.openclaw" }, + }); + expect(envArgs).toEqual([ + "OPENCLAW_HOME=/srv/agent-root", + "OPENCLAW_STATE_DIR=/srv/agent-root/.openclaw", + "OPENCLAW_WORKSPACE_DIR=/srv/agent-root/.openclaw/workspace", + ]); + }); + + it("falls back to the default dir when the OpenClaw agent omits configPaths", () => { + const envArgs: string[] = []; + appendOpenClawRuntimeEnvArgs(envArgs, { name: "openclaw", configPaths: undefined }); + expect(envArgs).toEqual([ + "OPENCLAW_HOME=/sandbox", + "OPENCLAW_STATE_DIR=/sandbox/.openclaw", + "OPENCLAW_WORKSPACE_DIR=/sandbox/.openclaw/workspace", + ]); + }); + + it("appends to an existing envArgs array without dropping prior entries", () => { + const envArgs = ["CHAT_UI_URL=http://127.0.0.1:18789"]; + appendOpenClawRuntimeEnvArgs(envArgs, null); + expect(envArgs[0]).toBe("CHAT_UI_URL=http://127.0.0.1:18789"); + expect(envArgs).toHaveLength(4); + }); + + it("skips injection for non-OpenClaw agents so OPENCLAW_* state cannot leak across agent runtimes", () => { + const envArgs: string[] = []; + appendOpenClawRuntimeEnvArgs(envArgs, { + name: "hermes", + configPaths: { dir: "/sandbox/.hermes" }, + }); + expect(envArgs).toEqual([]); + }); +}); diff --git a/src/lib/onboard/openclaw-runtime-env.ts b/src/lib/onboard/openclaw-runtime-env.ts new file mode 100644 index 00000000000..843ad14b1c1 --- /dev/null +++ b/src/lib/onboard/openclaw-runtime-env.ts @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import path from "node:path"; +import { formatEnvAssignment } from "../core/url-utils"; + +const DEFAULT_OPENCLAW_CONFIG_DIR = "/sandbox/.openclaw"; + +type AgentLike = { + readonly name?: string; + readonly configPaths?: { readonly dir?: string }; +} | null; + +function isOpenClawAgent(agent: AgentLike): boolean { + return !agent || agent.name === "openclaw"; +} + +export function appendOpenClawRuntimeEnvArgs(envArgs: string[], agent: AgentLike): void { + if (!isOpenClawAgent(agent)) return; + const configDir = agent?.configPaths?.dir || DEFAULT_OPENCLAW_CONFIG_DIR; + const homeDir = path.posix.dirname(configDir); + envArgs.push(formatEnvAssignment("OPENCLAW_HOME", homeDir)); + envArgs.push(formatEnvAssignment("OPENCLAW_STATE_DIR", configDir)); + envArgs.push(formatEnvAssignment("OPENCLAW_WORKSPACE_DIR", `${configDir}/workspace`)); +} diff --git a/test/e2e/test-openclaw-skill-cli-e2e.sh b/test/e2e/test-openclaw-skill-cli-e2e.sh new file mode 100755 index 00000000000..77fae6d9820 --- /dev/null +++ b/test/e2e/test-openclaw-skill-cli-e2e.sh @@ -0,0 +1,340 @@ +#!/bin/bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# OpenClaw skills install/list E2E — direct CLI roundtrip inside sandbox. +# +# Asserts that when a user runs `openclaw skills install ` directly +# inside a NemoClaw sandbox, the installed skill is enumerated by +# `openclaw skills list`. The sandbox onboard flow pins OPENCLAW_HOME, +# OPENCLAW_STATE_DIR, and OPENCLAW_WORKSPACE_DIR so install and list resolve +# the same workspace dir. +# +# Unlike test-skill-agent-e2e.sh, this script does NOT exercise the agent — +# it exercises the CLI contract only, so it has no LLM dependency and no +# retry/fuzzy-match logic. +# +# Prerequisites: +# - Docker running +# - NVIDIA_API_KEY set (needed to onboard the sandbox) +# - NEMOCLAW_NON_INTERACTIVE=1, NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 +# +# Environment: +# NEMOCLAW_SANDBOX_NAME — sandbox name (default: e2e-openclaw-skill-cli) +# NEMOCLAW_RECREATE_SANDBOX=1 — recreate if exists +# +# Usage: +# NEMOCLAW_NON_INTERACTIVE=1 NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 \ +# NVIDIA_API_KEY=nvapi-... bash test/e2e/test-openclaw-skill-cli-e2e.sh + +# shellcheck disable=SC2317 +set -uo pipefail + +PASS=0 +FAIL=0 +TOTAL=0 + +pass() { + ((PASS++)) + ((TOTAL++)) + printf '\033[32m PASS: %s\033[0m\n' "$1" +} +fail() { + ((FAIL++)) + ((TOTAL++)) + printf '\033[31m FAIL: %s\033[0m\n' "$1" +} +section() { + echo "" + printf '\033[1;36m=== %s ===\033[0m\n' "$1" +} +info() { printf '\033[1;34m [info]\033[0m %s\n' "$1"; } + +# ── Repo root ── +_script_dir="$(cd "$(dirname "$0")" && pwd)" +_candidate="$(cd "${_script_dir}/../.." && pwd)" +if [ -d /workspace ] && [ -f /workspace/package.json ] && [ -d /workspace/test/e2e ]; then + REPO="/workspace" +elif [ -f "${_candidate}/package.json" ] && [ -d "${_candidate}/test/e2e" ]; then + REPO="${_candidate}" +else + echo "ERROR: Cannot find repo root." + exit 1 +fi +unset _script_dir _candidate + +E2E_DIR="$(cd "$(dirname "$0")" && pwd)" +SANDBOX_NAME="${NEMOCLAW_SANDBOX_NAME:-e2e-openclaw-skill-cli}" +SKILL_ID="openclaw-skill-cli-fixture" +SKILL_DESCRIPTION="E2E fixture proving openclaw skills install + list roundtrip" + +# Source shared teardown helper +# shellcheck source=test/e2e/lib/sandbox-teardown.sh +. "${E2E_DIR}/lib/sandbox-teardown.sh" +register_sandbox_for_teardown "$SANDBOX_NAME" + +# ══════════════════════════════════════════════════════════════════════ +# Phase 1: Install + Prerequisites +# ══════════════════════════════════════════════════════════════════════ +section "Phase 1: Install + Prerequisites" + +if ! docker info >/dev/null 2>&1; then + fail "Docker is not running" + exit 1 +fi +pass "Docker is running" + +if [ -z "${NVIDIA_API_KEY:-}" ] || [[ "${NVIDIA_API_KEY}" != nvapi-* ]]; then + fail "NVIDIA_API_KEY not set or invalid" + exit 1 +fi +pass "NVIDIA_API_KEY is set" + +cd "$REPO" || { + fail "Could not cd to repo root" + exit 1 +} + +export NEMOCLAW_SANDBOX_NAME="$SANDBOX_NAME" +export NEMOCLAW_RECREATE_SANDBOX="${NEMOCLAW_RECREATE_SANDBOX:-1}" + +info "Installing NemoClaw via install.sh --non-interactive..." +INSTALL_LOG="/tmp/nemoclaw-e2e-openclaw-skill-cli-install.log" +bash install.sh --non-interactive --yes-i-accept-third-party-software >"$INSTALL_LOG" 2>&1 & +install_pid=$! +tail -f "$INSTALL_LOG" --pid=$install_pid 2>/dev/null & +tail_pid=$! +wait "$install_pid" +install_exit=$? +kill "$tail_pid" 2>/dev/null || true +wait "$tail_pid" 2>/dev/null || true + +if [ -f "$HOME/.bashrc" ]; then + # shellcheck source=/dev/null + source "$HOME/.bashrc" 2>/dev/null || true +fi +export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" +# shellcheck source=/dev/null +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" +[ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]] && export PATH="$HOME/.local/bin:$PATH" + +if [ "$install_exit" -ne 0 ]; then + fail "install.sh failed (exit $install_exit)" + tail -30 "$INSTALL_LOG" + exit 1 +fi +pass "NemoClaw installed" + +command -v openshell >/dev/null 2>&1 || { + fail "openshell not on PATH" + exit 1 +} +pass "openshell on PATH" + +# ══════════════════════════════════════════════════════════════════════ +# Phase 2: Pre-flight — verify the OPENCLAW_* runtime env pins reach +# the sandbox's runtime shell rc. Drift here means the workaround in +# src/lib/onboard.ts never propagates past `nemoclaw-start` and +# `openclaw skills list` will fall back to a hardcoded default workspace. +# ══════════════════════════════════════════════════════════════════════ +section "Phase 2: Pre-flight runtime env propagation check" + +set +e +# Single-quote the inner script so the OPENCLAW_* variables expand inside the +# sandbox shell, not on the host. +# shellcheck disable=SC2016 +env_check_out=$(openshell sandbox exec --name "$SANDBOX_NAME" -- sh -lc 'printf "OPENCLAW_HOME=%s\nOPENCLAW_STATE_DIR=%s\nOPENCLAW_WORKSPACE_DIR=%s\n" "${OPENCLAW_HOME:-}" "${OPENCLAW_STATE_DIR:-}" "${OPENCLAW_WORKSPACE_DIR:-}"' 2>&1) +env_check_rc=$? +set -uo pipefail + +if [ "$env_check_rc" -ne 0 ]; then + fail "Failed to read OPENCLAW_* env vars from sandbox runtime shell (exit ${env_check_rc})" + printf '%s\n' "$env_check_out" + exit 1 +fi + +for required_var in OPENCLAW_HOME OPENCLAW_STATE_DIR OPENCLAW_WORKSPACE_DIR; do + if ! printf '%s\n' "$env_check_out" | grep -Eq "^${required_var}=.+"; then + fail "${required_var} not exported in sandbox runtime shell" + printf '%s\n' "$env_check_out" + exit 1 + fi +done +pass "OPENCLAW_HOME, OPENCLAW_STATE_DIR, and OPENCLAW_WORKSPACE_DIR are exported in sandbox runtime shell" + +# ══════════════════════════════════════════════════════════════════════ +# Phase 3: Write a skill fixture into the sandbox under /tmp and install +# it through the OpenClaw CLI from a non-managed source path. +# ══════════════════════════════════════════════════════════════════════ +section "Phase 3: Install skill via 'openclaw skills install ' inside sandbox" + +remote_skill_dir="/tmp/${SKILL_ID}" +# openshell sandbox exec rejects command arguments that contain newlines or CRs +# ("InvalidArgument: command argument N contains newline or carriage return +# characters"), so the SKILL.md payload is base64-encoded on the host and decoded +# inside the sandbox. The encoder uses base64 -w0 (or tr -d) so the encoded +# payload is itself single-line. +skill_payload=$(printf '%s\n' \ + "---" \ + "name: \"${SKILL_ID}\"" \ + "description: \"${SKILL_DESCRIPTION}\"" \ + "---" \ + "" \ + "# OpenClaw skill CLI roundtrip fixture" \ + "" \ + "Written by test/e2e/test-openclaw-skill-cli-e2e.sh.") +skill_payload_b64=$(printf '%s' "$skill_payload" | base64 | tr -d '\n') +write_skill_cmd="rm -rf $(printf "%q" "$remote_skill_dir") && mkdir -p $(printf "%q" "$remote_skill_dir") && printf '%s' '${skill_payload_b64}' | base64 -d > $(printf "%q" "${remote_skill_dir}/SKILL.md")" + +set +e +write_out=$(openshell sandbox exec --name "$SANDBOX_NAME" -- sh -lc "$write_skill_cmd" 2>&1) +write_rc=$? +set -uo pipefail +if [ "$write_rc" -ne 0 ]; then + fail "Failed to write skill fixture into sandbox (exit ${write_rc})" + printf '%s\n' "$write_out" + exit 1 +fi +pass "Wrote skill fixture into sandbox at ${remote_skill_dir}" + +set +e +install_out=$(openshell sandbox exec --name "$SANDBOX_NAME" -- sh -lc "openclaw skills install $(printf "%q" "$remote_skill_dir")" 2>&1) +install_rc=$? +set -uo pipefail +if [ "$install_rc" -ne 0 ]; then + fail "openclaw skills install failed (exit ${install_rc})" + printf '%s\n' "$install_out" + exit 1 +fi +pass "openclaw skills install completed (exit 0)" +info "install output:" +printf '%s\n' "$install_out" + +# ══════════════════════════════════════════════════════════════════════ +# Phase 4: Disk verification — install must land under the workspace dir +# the runtime env pin advertises, NOT under the managed dir or a host +# fallback. The reporter's repro on disk was ls /sandbox/.openclaw/workspace/skills/. +# ══════════════════════════════════════════════════════════════════════ +section "Phase 4: Verify install landed under \${OPENCLAW_WORKSPACE_DIR}/skills/" + +expected_disk_path="/sandbox/.openclaw/workspace/skills/${SKILL_ID}/SKILL.md" +set +e +disk_out=$(openshell sandbox exec --name "$SANDBOX_NAME" -- sh -lc "ls -1 \"\${OPENCLAW_WORKSPACE_DIR}/skills/${SKILL_ID}/\" 2>&1 ; test -f \"\${OPENCLAW_WORKSPACE_DIR}/skills/${SKILL_ID}/SKILL.md\" && echo SKILL_MD_PRESENT" 2>&1) +disk_rc=$? +set -uo pipefail +if [ "$disk_rc" -ne 0 ] || ! printf '%s' "$disk_out" | grep -Fq "SKILL_MD_PRESENT"; then + fail "Installed skill not present at \${OPENCLAW_WORKSPACE_DIR}/skills/${SKILL_ID}/SKILL.md (expected ${expected_disk_path})" + printf '%s\n' "$disk_out" + exit 1 +fi +pass "SKILL.md present on disk at \${OPENCLAW_WORKSPACE_DIR}/skills/${SKILL_ID}/" + +# ══════════════════════════════════════════════════════════════════════ +# Phase 5: List skills via 'openclaw skills list --json' and assert the +# installed fixture is enumerated. This is the contract the issue reports as +# broken when the runtime env pin is missing; passing here proves the +# install path and the list path agree on the workspace dir. +# ══════════════════════════════════════════════════════════════════════ +section "Phase 5: Verify 'openclaw skills list' surfaces the installed skill" + +set +e +list_out=$(openshell sandbox exec --name "$SANDBOX_NAME" -- sh -lc 'openclaw skills list --json' 2>&1) +list_rc=$? +set -uo pipefail +if [ "$list_rc" -ne 0 ]; then + fail "openclaw skills list --json failed (exit ${list_rc})" + printf '%s\n' "$list_out" + exit 1 +fi +pass "openclaw skills list --json completed (exit 0)" + +if ! printf '%s' "$list_out" | grep -Fq "\"${SKILL_ID}\""; then + fail "Installed skill '${SKILL_ID}' did not appear in 'openclaw skills list --json' output" + printf '%s\n' "$list_out" | tail -c 8000 + exit 1 +fi +pass "Installed skill '${SKILL_ID}' is enumerated by 'openclaw skills list --json'" + +# Assert the list entry's source labels it as openclaw-workspace (not +# openclaw-managed or openclaw-extra) so we know the skill came from the +# workspace install path and not a fallback location. +if ! printf '%s' "$list_out" | grep -Fq "openclaw-workspace"; then + fail "Expected at least one entry with source 'openclaw-workspace' in 'openclaw skills list --json' output" + printf '%s\n' "$list_out" | tail -c 8000 + exit 1 +fi +pass "list output includes an entry with source 'openclaw-workspace'" + +# ══════════════════════════════════════════════════════════════════════ +# Phase 6: 'openclaw skills info ' must resolve the same skill that +# install wrote and report its on-disk location. This catches drift +# between the install resolver and the per-skill info resolver. +# ══════════════════════════════════════════════════════════════════════ +section "Phase 6: Verify 'openclaw skills info ${SKILL_ID}' resolves the workspace path" + +set +e +info_out=$(openshell sandbox exec --name "$SANDBOX_NAME" -- sh -lc "openclaw skills info $(printf "%q" "$SKILL_ID") --json" 2>&1) +info_rc=$? +set -uo pipefail +if [ "$info_rc" -ne 0 ]; then + fail "openclaw skills info ${SKILL_ID} --json failed (exit ${info_rc})" + printf '%s\n' "$info_out" + exit 1 +fi +pass "openclaw skills info ${SKILL_ID} --json completed (exit 0)" + +if ! printf '%s' "$info_out" | grep -Fq "${SKILL_ID}"; then + fail "'openclaw skills info' output did not include the skill id" + printf '%s\n' "$info_out" | tail -c 8000 + exit 1 +fi +if ! printf '%s' "$info_out" | grep -Fq "/.openclaw/workspace/skills/${SKILL_ID}"; then + fail "'openclaw skills info' did not report the workspace install path" + printf '%s\n' "$info_out" | tail -c 8000 + exit 1 +fi +pass "'openclaw skills info' reports the skill at the workspace install path" + +# ══════════════════════════════════════════════════════════════════════ +# Phase 7: 'openclaw skills check' is the eligibility report users run to +# diagnose missing skills. The installed fixture must appear there too so +# users do not see a partial view of their workspace. +# ══════════════════════════════════════════════════════════════════════ +section "Phase 7: Verify 'openclaw skills check' includes the installed skill" + +set +e +check_out=$(openshell sandbox exec --name "$SANDBOX_NAME" -- sh -lc 'openclaw skills check --json' 2>&1) +check_rc=$? +set -uo pipefail +if [ "$check_rc" -ne 0 ]; then + fail "openclaw skills check --json failed (exit ${check_rc})" + printf '%s\n' "$check_out" + exit 1 +fi +pass "openclaw skills check --json completed (exit 0)" + +if ! printf '%s' "$check_out" | grep -Fq "\"${SKILL_ID}\""; then + fail "Installed skill '${SKILL_ID}' did not appear in 'openclaw skills check --json' output" + printf '%s\n' "$check_out" | tail -c 8000 + exit 1 +fi +pass "Installed skill '${SKILL_ID}' is enumerated by 'openclaw skills check --json'" + +# ══════════════════════════════════════════════════════════════════════ +# Summary +# ══════════════════════════════════════════════════════════════════════ +echo "" +echo "========================================" +echo " OpenClaw skill CLI E2E Results:" +echo " Passed: $PASS" +echo " Failed: $FAIL" +echo " Total: $TOTAL" +echo "========================================" + +if [ "$FAIL" -eq 0 ]; then + printf '\033[1;32m\n OpenClaw skill CLI E2E PASSED.\033[0m\n' + exit 0 +else + printf '\033[1;31m\n %d test(s) failed.\033[0m\n' "$FAIL" + exit 1 +fi diff --git a/test/onboard.test.ts b/test/onboard.test.ts index d78dd358bd3..2ff4027a6f2 100644 --- a/test/onboard.test.ts +++ b/test/onboard.test.ts @@ -3133,6 +3133,17 @@ const { createSandbox } = require(${onboardPath}); assert.match(createCommand.command, /NEMOCLAW_DASHBOARD_PORT=19000/); assert.match(createCommand.command, /HTTP_PROXY=http:\/\/127\.0\.0\.1:8888/); assert.match(createCommand.command, /HTTPS_PROXY=http:\/\/127\.0\.0\.1:8888/); + // OpenClaw home/state/workspace dirs must be pinned in the sandbox env so + // `openclaw skills install` and `openclaw skills list` resolve the same + // paths. Without this, the upstream skill loader can fall back to a + // hardcoded DEFAULT_AGENT_WORKSPACE_DIR that drifts from the install path + // and hides workspace-installed skills from `skills list`. + assert.match(createCommand.command, /OPENCLAW_HOME=\/sandbox(?:\s|$)/); + assert.match(createCommand.command, /OPENCLAW_STATE_DIR=\/sandbox\/\.openclaw(?:\s|$)/); + assert.match( + createCommand.command, + /OPENCLAW_WORKSPACE_DIR=\/sandbox\/\.openclaw\/workspace(?:\s|$)/, + ); const noProxyMatch = createCommand.command.match(/(?:^|\s)NO_PROXY=([^\s]+)/); assert.ok(noProxyMatch, `expected NO_PROXY in sandbox create command:\n${createCommand.command}`); const noProxyEntries = noProxyMatch[1].split(",");