init: fork of kejilion/sh with certbot host-network fix
Some checks failed
Weekly Translation Workflow / translate (push) Has been cancelled

- Replace 'docker run -p 80:80' with '--network host' for certbot standalone HTTP-01 to avoid timeout in environments where Docker NAT port-publish breaks LE inbound traffic but host port 80 is reachable.
- Applies to kejilion.sh (all locale variants) and auto_cert_renewal.sh.
- README install command points to this Gitea mirror.
This commit is contained in:
2026-05-18 17:06:42 +08:00
commit 0784a11c19
73 changed files with 167210 additions and 0 deletions

36
tests/openclaw/README.md Normal file
View File

@@ -0,0 +1,36 @@
# OpenClaw tests
这里的脚本用于对 `kejilion.sh` 中的 OpenClaw 相关功能做最小回归与冒烟测试,主要覆盖:
- API 模型同步/协议探测
- 记忆菜单
- 插件/技能菜单
- API 列表颜色/对齐展示(最小验证)
## 运行方式
在仓库根目录执行(推荐):
```bash
bash tests/openclaw/tests_openclaw_api_protocol_detect_smoke.sh
bash tests/openclaw/tests_openclaw_api_sync_diff_smoke.sh
bash tests/openclaw/tests_openclaw_memory_menu_smoke.sh
bash tests/openclaw/tests_openclaw_plugin_skill_menu_smoke.sh
bash tests/openclaw/tests_openclaw_api_color_align_min.sh
```
也可在任意工作目录执行:
```bash
/path/to/repo/tests/openclaw/tests_openclaw_memory_menu_smoke.sh
```
## 安全说明
- 脚本会创建临时工作目录与临时 `HOME`(位于 `/tmp`),并在结束后自动清理(除非设置 `KEEP_WORKDIR=true`)。
- 运行过程使用 stub/模拟命令,不会改动真实 OpenClaw 配置或系统状态。
- 如需保留临时目录用于排查,请执行:
```bash
KEEP_WORKDIR=true bash tests/openclaw/tests_openclaw_memory_menu_smoke.sh
```

View File

@@ -0,0 +1,130 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
SCRIPT="$REPO_ROOT/kejilion.sh"
WORKDIR="${TMPDIR:-/tmp}/openclaw-change-model-dual-probe-$$"
mkdir -p "$WORKDIR/home/.openclaw"
KEEP_WORKDIR=${KEEP_WORKDIR:-false}
trap '[ "$KEEP_WORKDIR" = "true" ] || rm -rf "$WORKDIR"' EXIT
python3 - "$SCRIPT" "$WORKDIR" <<'PY'
import json
import re
import sys
from pathlib import Path
script_path = Path(sys.argv[1])
workdir = Path(sys.argv[2])
text = script_path.read_text(encoding='utf-8')
match = re.search(r'\n\t\topenclaw_model_probe\(\) \{.*?\n\t\t\}', text, re.S)
if not match:
raise SystemExit('openclaw_model_probe not found')
func = match.group(0).strip('\n')
config = {
"models": {
"providers": {
"demo": {
"baseUrl": "https://api.example.com/v1",
"apiKey": "sk-test"
}
}
}
}
(workdir / 'home' / '.openclaw').mkdir(parents=True, exist_ok=True)
(workdir / 'home' / '.openclaw' / 'openclaw.json').write_text(json.dumps(config, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
harness = f'''#!/usr/bin/env bash
set -euo pipefail
HOME="{workdir / 'home'}"
OPENCLAW_PROBE_STATUS=""
OPENCLAW_PROBE_MESSAGE=""
OPENCLAW_PROBE_LATENCY=""
OPENCLAW_PROBE_REPLY=""
{func}
'''
(workdir / 'harness.sh').write_text(harness, encoding='utf-8')
PY
run_case() {
local case_name="$1"
CASE_NAME="$case_name" WORKDIR="$WORKDIR" bash <<'EOF_CASE'
set -euo pipefail
source "$WORKDIR/harness.sh"
python3() {
if [ "$1" != "-" ]; then
command python3 "$@"
return
fi
local script
script=$(cat)
shift
CASE_NAME="$CASE_NAME" command python3 - "$script" "$@" <<'PYWRAP'
import io
import json
import os
import sys
import urllib.error
import urllib.request
from pathlib import Path
script = sys.argv[1]
args = sys.argv[2:]
case = os.environ['CASE_NAME']
class Resp(io.BytesIO):
def __init__(self, data, code=200):
super().__init__(data)
self.status = code
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
real_urlopen = urllib.request.urlopen
def fake_urlopen(req, timeout=0):
url = getattr(req, 'full_url', '')
if case == 'fallback-success':
if url.endswith('/chat/completions'):
raise urllib.error.HTTPError(url, 404, 'Not Found', {}, io.BytesIO(b'{"error":"chat disabled"}'))
if url.endswith('/responses'):
body = json.dumps({"output": [{"content": [{"text": "pong from responses"}]}]}).encode('utf-8')
return Resp(body, 200)
elif case == 'all-fail':
if url.endswith('/chat/completions'):
raise urllib.error.HTTPError(url, 404, 'Not Found', {}, io.BytesIO(b'{"error":"chat disabled"}'))
if url.endswith('/responses'):
raise urllib.error.HTTPError(url, 404, 'Not Found', {}, io.BytesIO(b'{"error":"responses disabled"}'))
return real_urlopen(req, timeout=timeout)
urllib.request.urlopen = fake_urlopen
sys.argv = ['python3'] + args
ns = {'__name__': '__main__'}
exec(compile(script, '<stdin>', 'exec'), ns, ns)
PYWRAP
}
set +e
openclaw_model_probe "demo/test-model"
probe_rc=$?
set -e
printf 'RC=%s\nSTATUS=%s\nMESSAGE=%s\nLATENCY=%s\nREPLY=%s\n' "$probe_rc" "$OPENCLAW_PROBE_STATUS" "$OPENCLAW_PROBE_MESSAGE" "$OPENCLAW_PROBE_LATENCY" "$OPENCLAW_PROBE_REPLY"
EOF_CASE
}
out1=$(run_case fallback-success)
printf '%s\n' "$out1" | grep -q 'STATUS=OK'
printf '%s\n' "$out1" | grep -q 'MESSAGE=/responses -> HTTP 200'
printf '%s\n' "$out1" | grep -q 'REPLY=pong from responses'
out2=$(run_case all-fail || true)
printf '%s\n' "$out2" | grep -q 'RC=1'
printf '%s\n' "$out2" | grep -q 'STATUS=FAIL'
printf '%s\n' "$out2" | grep -q '/responses -> HTTP 404 / exit 22/chat/completions -> HTTP 404 / exit 22'
printf '%s\n' 'SMOKE_OK'

View File

@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
# 模拟脚本原生颜色变量
# shellcheck disable=SC2034
gl_hong='\033[31m'
# shellcheck disable=SC2034
gl_lv='\033[32m'
# shellcheck disable=SC2034
gl_huang='\033[33m'
# shellcheck disable=SC2034
gl_bai='\033[0m'
printf '%s\n' '--- OpenClaw API 列表(最小颜色/对齐验证) ---'
printf '%-4s %-18s %-44s %-8s %-12s\n' '序号' '名称' 'API地址' '模型数量' '延迟/状态'
printf '%s\n' '----------------------------------------------------------------------------------------------'
# 模拟 Python 计算后的字段(纯数据,不含颜色码)
while IFS=$'\t' read -r rec_type idx name base_url model_count latency_txt latency_level; do
[[ "$rec_type" == "ROW" ]] || continue
latency_color="$gl_bai"
case "$latency_level" in
low) latency_color="$gl_lv" ;;
medium) latency_color="$gl_huang" ;;
high|unavailable) latency_color="$gl_hong" ;;
unchecked) latency_color="$gl_bai" ;;
esac
# 颜色在 Shell 层拼接并输出
printf '%-4s %-18s %-44s %b%-8s%b %b%-12s%b\n' \
"$idx" "$name" "$base_url" \
"$gl_huang" "$model_count" "$gl_bai" \
"$latency_color" "$latency_txt" "$gl_bai"
done <<'EOF'
ROW 1. alpha-openai https://api.alpha.example/v1 128 120ms low
ROW 2. beta-proxy https://api.beta.example/v1 64 1300ms medium
ROW 3. gamma-down https://api.gamma.example/v1 0 不可用 unavailable
ROW 4. delta-custom - 12 未检测 unchecked
EOF

View File

@@ -0,0 +1,79 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
SCRIPT="$REPO_ROOT/kejilion.sh"
WORKDIR="${TMPDIR:-/tmp}/openclaw-api-protocol-test-$$"
mkdir -p "$WORKDIR/bin" "$WORKDIR/home/.openclaw"
KEEP_WORKDIR=${KEEP_WORKDIR:-false}
trap '[ "$KEEP_WORKDIR" = "true" ] || rm -rf "$WORKDIR"' EXIT
# 抽取 OpenClaw API 添加逻辑(只验证:不再做协议探测/自动纠正)
cat > "$WORKDIR/harness.sh" <<'EOF_INNER'
#!/usr/bin/env bash
set -euo pipefail
break_end() { return 0; }
send_stats() { return 0; }
start_gateway() { return 0; }
install() { return 0; }
EOF_INNER
awk 'BEGIN{p=0}
/add-all-models-from-provider\(\) \{/{p=1}
/openclaw_api_manage_list\(\) \{/{p=0}
p{print}
' "$SCRIPT" >> "$WORKDIR/harness.sh"
awk 'BEGIN{p=0}
/openclaw_detect_api_protocol_by_provider\(\) \{/{p=1}
/fix-openclaw-provider-protocol-interactive\(\) \{/{p=0}
p{print}
' "$SCRIPT" >> "$WORKDIR/harness.sh"
chmod +x "$WORKDIR/harness.sh"
# stub: curl
cat > "$WORKDIR/bin/curl" <<'EOF_INNER'
#!/usr/bin/env bash
set -e
args="$*"
if [[ "$args" == *"/models"* ]]; then
cat <<'JSON'
{"data": [{"id": "gpt-4o"}, {"id": "gpt-4o-mini"}]}
JSON
exit 0
fi
printf ""
exit 0
EOF_INNER
chmod +x "$WORKDIR/bin/curl"
export HOME="$WORKDIR/home"
export PATH="$WORKDIR/bin:$PATH"
export TERM=xterm
cat > "$HOME/.openclaw/openclaw.json" <<'JSON'
{
"models": {
"mode": "merge",
"providers": {}
},
"agents": {
"defaults": {
"models": {}
}
}
}
JSON
source "$WORKDIR/harness.sh"
# 添加 provider不应依赖协议探测若脚本内部仍调用已删除函数会报错
add-all-models-from-provider "demo" "https://api.example.com/v1" "sk-test"
echo "SMOKE_OK: no protocol probing required during add"
# 协议修复函数现在不再修改配置,只输出提示
openclaw_detect_api_protocol_by_provider "$HOME/.openclaw/openclaw.json" "demo" >/dev/null 2>&1 || true
echo "SMOKE_OK: protocol switch is available"

View File

@@ -0,0 +1,133 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
SCRIPT="$REPO_ROOT/kejilion.sh"
WORKDIR="${TMPDIR:-/tmp}/openclaw-api-sync-diff-test-$$"
mkdir -p "$WORKDIR/bin" "$WORKDIR/home/.openclaw"
KEEP_WORKDIR=${KEEP_WORKDIR:-false}
trap '[ "$KEEP_WORKDIR" = "true" ] || rm -rf "$WORKDIR"' EXIT
cat > "$WORKDIR/harness.sh" <<'EOF_INNER'
#!/usr/bin/env bash
set -euo pipefail
break_end() { return 0; }
send_stats() { return 0; }
start_gateway() { return 0; }
install() { return 0; }
EOF_INNER
awk 'BEGIN{p=0}
/sync_openclaw_api_models\(\) \{/{p=1}
/install_moltbot\(\) \{/{p=0}
p{print}
' "$SCRIPT" >> "$WORKDIR/harness.sh"
chmod +x "$WORKDIR/harness.sh"
cat > "$WORKDIR/bin/python3" <<'EOF_INNER'
#!/usr/bin/env bash
if [[ "$1" != "-" ]]; then
exec /usr/bin/python3 "$@"
fi
SCRIPT=$(cat)
export SCRIPT
/usr/bin/python3 - "${@:2}" <<"PY_STDIN"
import io
import json
import os
import urllib.request
import urllib.error
class _Resp(io.BytesIO):
def __init__(self, data, code=200):
super().__init__(data)
self._code = code
def getcode(self):
return self._code
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
_real = urllib.request.urlopen
def urlopen(req, timeout=0):
url = getattr(req, "full_url", "") or getattr(req, "get_full_url", lambda: "")()
if url.endswith("/models"):
data = json.dumps({"data": [{"id": "alpha"}, {"id": "beta"}]}).encode("utf-8")
return _Resp(data, 200)
if url.endswith("/responses") or url.endswith("/chat/completions"):
raise urllib.error.HTTPError(url, 404, "Not Found", {}, None)
return _real(req, timeout=timeout)
urllib.request.urlopen = urlopen
script = os.environ.get("SCRIPT", "")
exec(compile(script, "<stdin>", "exec"))
PY_STDIN
exit $?
EOF_INNER
chmod +x "$WORKDIR/bin/python3"
cat > "$WORKDIR/bin/curl" <<'EOF_INNER'
#!/usr/bin/env bash
# fake curl for /models, /responses, /chat/completions (for shell probes)
set -e
args="$*"
if [[ "$args" == *"/responses"* ]]; then
printf "404"
exit 0
fi
if [[ "$args" == *"/chat/completions"* ]]; then
printf "404"
exit 0
fi
printf "000"
exit 0
EOF_INNER
chmod +x "$WORKDIR/bin/curl"
export HOME="$WORKDIR/home"
export PATH="$WORKDIR/bin:$PATH"
export TERM=xterm
export ENABLE_STATS=false
export sh_v=testing
cat > "$HOME/.openclaw/openclaw.json" <<'JSON'
{
"models": {
"mode": "merge",
"providers": {
"demo": {
"api": "openai-completions",
"baseUrl": "https://api.example.com/v1",
"apiKey": "sk-test",
"models": [
{"id": "alpha", "name": "demo / alpha"},
{"id": "gamma", "name": "demo / gamma"}
]
}
}
},
"agents": {
"defaults": {
"models": {
"demo/alpha": {},
"demo/gamma": {}
},
"model": "demo/alpha"
}
}
}
JSON
source "$WORKDIR/harness.sh"
output=$(sync_openclaw_api_models 2>&1 || true)
printf '%s\n' "$output" | grep -q " 新增模型(1):"
printf '%s\n' "$output" | grep -q "^ + beta$"
printf '%s\n' "$output" | grep -q " 删除模型(1):"
printf '%s\n' "$output" | grep -q "^ - gamma$"
printf '%s\n' "SMOKE_OK"

View File

@@ -0,0 +1,237 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
SCRIPT="$REPO_ROOT/kejilion.sh"
WORKDIR="${TMPDIR:-/tmp}/openclaw-memory-auto-setup-test-$$"
mkdir -p "$WORKDIR/bin"
KEEP_WORKDIR=${KEEP_WORKDIR:-false}
trap '[ "$KEEP_WORKDIR" = "true" ] || rm -rf "$WORKDIR"' EXIT
# 抽取 OpenClaw 记忆菜单实现
cat > "$WORKDIR/harness.sh" <<'EOF_INNER'
#!/usr/bin/env bash
set -euo pipefail
break_end() { return 0; }
send_stats() { return 0; }
EOF_INNER
awk 'BEGIN{p=0} /openclaw_memory_config_file\(\) \{/{p=1} /openclaw_memory_menu\(\) \{/{p=0} p{print}' "$SCRIPT" >> "$WORKDIR/harness.sh"
chmod +x "$WORKDIR/harness.sh"
# stub: openclaw
cat > "$WORKDIR/bin/openclaw" <<'EOF_INNER'
#!/usr/bin/env bash
set -euo pipefail
cmd="$*"
config_dir="$HOME/.openclaw/config"
mkdir -p "$config_dir"
key_file() {
local key="$1"
echo "$config_dir/${key//./_}"
}
if [[ "$cmd" == "config get"* ]]; then
key="$3"
file=$(key_file "$key")
if [ -f "$file" ]; then
cat "$file"
fi
exit 0
fi
if [[ "$cmd" == "config set"* ]]; then
key="$3"
val="$4"
file=$(key_file "$key")
echo "$val" > "$file"
exit 0
fi
if [[ "$1" == "memory" && "$2" == "status" ]]; then
agent="main"
while [ $# -gt 0 ]; do
if [ "$1" = "--agent" ] && [ $# -ge 2 ]; then
agent="$2"
shift 2
continue
fi
shift
done
cat <<TXT
Provider: qmd (requested: qmd)
Vector: ready
Indexed: 0/0 files
Workspace: ${HOME}/.openclaw/workspace
Store: ${OPENCLAW_STORE:-~/.openclaw/workspace/memory/index.sqlite}
TXT
exit 0
fi
if [[ "$1" == "memory" && "$2" == "index" ]]; then
echo "$cmd" >> "${HOME}/.openclaw/index_calls"
echo "index ok"
exit 0
fi
echo "mock openclaw $*"
EOF_INNER
chmod +x "$WORKDIR/bin/openclaw"
# stub: curl
cat > "$WORKDIR/bin/curl" <<'EOF_INNER'
#!/usr/bin/env bash
set -euo pipefail
args=("$@")
is_head=0
for arg in "${args[@]}"; do
if [ "$arg" = "-I" ]; then
is_head=1
fi
done
url="${args[$((${#args[@]}-1))]}"
for arg in "${args[@]}"; do
if [[ "$arg" == *"ipinfo.io/country"* ]]; then
echo "${FAKE_COUNTRY:-US}"
exit 0
fi
if [[ "$arg" == *"bun.sh/install"* ]]; then
cat <<'SCRIPT'
mkdir -p "$HOME/.bun/bin"
cat > "$HOME/.bun/bin/bun" <<'BUN'
#!/usr/bin/env bash
set -euo pipefail
if [[ "$1" == "install" && "$2" == "-g" ]]; then
mkdir -p "$HOME/.bun/bin"
cat > "$HOME/.bun/bin/qmd" <<'QMD'
#!/usr/bin/env bash
exit 0
QMD
chmod +x "$HOME/.bun/bin/qmd"
fi
echo "$*" >> "$HOME/.openclaw/bun_calls"
exit 0
BUN
chmod +x "$HOME/.bun/bin/bun"
SCRIPT
exit 0
fi
done
if [ "$is_head" = "1" ]; then
if [[ "$url" == *"huggingface.co"* ]]; then
exit ${FAKE_HF_OK:-0}
fi
if [[ "$url" == *"hf-mirror.com"* ]]; then
exit ${FAKE_MIRROR_OK:-0}
fi
exit 0
fi
dest=""
for ((i=0;i<${#args[@]};i++)); do
if [ "${args[$i]}" = "-o" ] && [ $((i+1)) -lt ${#args[@]} ]; then
dest="${args[$((i+1))]}"
fi
done
if [ -n "$dest" ] && [[ "$url" == *"embeddinggemma"* ]]; then
mkdir -p "$(dirname "$dest")"
echo "model" > "$dest"
echo "$url" >> "$HOME/.openclaw/download_calls"
exit 0
fi
exit 0
EOF_INNER
chmod +x "$WORKDIR/bin/curl"
# stub: wget (fallback, not used in test)
cat > "$WORKDIR/bin/wget" <<'EOF_INNER'
#!/usr/bin/env bash
exit 0
EOF_INNER
chmod +x "$WORKDIR/bin/wget"
export PATH="$WORKDIR/bin:$PATH"
export TERM=xterm
source "$WORKDIR/harness.sh"
make_home() {
local name="$1"
local home="$WORKDIR/$name/home"
mkdir -p "$home/.openclaw/workspace/memory" "$home/.openclaw"
echo "$home"
}
run_auto() {
local input="$1"
local scheme="$2"
local outfile="$3"
printf "%b" "$input" | openclaw_memory_auto_setup_run "$scheme" >"$outfile"
}
# Case 1: qmd 不存在 -> 安装 -> 写入绝对路径 -> index
HOME_QMD=$(make_home "qmd")
export HOME="$HOME_QMD"
FAKE_COUNTRY=US FAKE_HF_OK=0 FAKE_MIRROR_OK=1 \
run_auto "yes\n" "qmd" "$WORKDIR/out_qmd.txt"
# Case 2: local 模型不存在 -> 下载 -> 写入 modelPath -> index (CN)
HOME_LOCAL=$(make_home "local")
export HOME="$HOME_LOCAL"
FAKE_COUNTRY=CN FAKE_HF_OK=1 FAKE_MIRROR_OK=0 \
run_auto "yes\n" "local" "$WORKDIR/out_local.txt"
# Case 3: 已存在 -> 跳过下载/安装
HOME_SKIP=$(make_home "skip")
export HOME="$HOME_SKIP"
mkdir -p "$HOME/.openclaw/models/embedding" "$HOME/.openclaw/config"
MODEL_EXIST="$HOME/.openclaw/models/embedding/embeddinggemma-300M-Q8_0.gguf"
echo "local" > "$HOME/.openclaw/config/memory_backend"
echo "local" > "$HOME/.openclaw/config/agents_defaults_memorySearch_provider"
echo "$MODEL_EXIST" > "$HOME/.openclaw/config/agents_defaults_memorySearch_local_modelPath"
touch "$MODEL_EXIST"
# 预置 qmd
mkdir -p "$HOME/.bun/bin"
cat > "$HOME/.bun/bin/qmd" <<'QMD'
#!/usr/bin/env bash
exit 0
QMD
chmod +x "$HOME/.bun/bin/qmd"
export PATH="$HOME/.bun/bin:$PATH"
FAKE_COUNTRY=US FAKE_HF_OK=0 FAKE_MIRROR_OK=0 \
run_auto "yes\n" "local" "$WORKDIR/out_skip.txt"
export WORKDIR
python3 - <<'PY'
import os
from pathlib import Path
workdir = os.environ['WORKDIR']
# Case1: qmd command absolute + index called
home_qmd = Path(workdir) / 'qmd' / 'home'
qmd_cmd = (home_qmd / '.openclaw/config/memory_qmd_command').read_text().strip()
if not qmd_cmd.startswith('/'):
raise SystemExit('qmd command not absolute')
index_calls = (home_qmd / '.openclaw/index_calls').read_text()
if 'memory index' not in index_calls or '--force' not in index_calls:
raise SystemExit('qmd preheat index not called')
# Case2: local model downloaded + modelPath written
home_local = Path(workdir) / 'local' / 'home'
model_path = (home_local / '.openclaw/config/agents_defaults_memorySearch_local_modelPath').read_text().strip()
if not Path(model_path).exists():
raise SystemExit('model file not downloaded')
if 'hf-mirror.com' not in (Path(workdir) / 'out_local.txt').read_text():
raise SystemExit('mirror not used for CN')
# Case3: skip download
home_skip = Path(workdir) / 'skip' / 'home'
download_calls = home_skip / '.openclaw/download_calls'
if download_calls.exists():
raise SystemExit('download should be skipped when model exists')
print('SMOKE_OK')
PY

View File

@@ -0,0 +1,230 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
SCRIPT="$REPO_ROOT/kejilion.sh"
WORKDIR="${TMPDIR:-/tmp}/openclaw-memory-menu-test-$$"
mkdir -p "$WORKDIR/bin" "$WORKDIR/home/.openclaw/workspace/memory"
KEEP_WORKDIR=${KEEP_WORKDIR:-false}
trap '[ "$KEEP_WORKDIR" = "true" ] || rm -rf "$WORKDIR"' EXIT
# 抽取 OpenClaw 记忆菜单实现
cat > "$WORKDIR/harness.sh" <<'EOF_INNER'
#!/usr/bin/env bash
set -euo pipefail
break_end() { return 0; }
send_stats() { return 0; }
EOF_INNER
awk 'BEGIN{p=0} /openclaw_memory_config_file\(\) \{/{p=1} /openclaw_memory_menu\(\) \{/{p=0} p{print}' "$SCRIPT" >> "$WORKDIR/harness.sh"
awk 'BEGIN{p=0} /openclaw_memory_menu\(\) \{/{p=1} /openclaw_backup_restore_menu\(\) \{/{p=0} p{print}' "$SCRIPT" >> "$WORKDIR/harness.sh"
chmod +x "$WORKDIR/harness.sh"
# stub: openclaw
cat > "$WORKDIR/bin/openclaw" <<'EOF_INNER'
#!/usr/bin/env bash
cmd="$*"
if [[ "$cmd" == "config get"* ]]; then
key="$3"
case "$key" in
memory.backend) echo "qmd" ;;
memory.qmd.includeDefaultMemory)
if [ -f "${HOME}/.openclaw/includeDefaultMemory" ]; then
cat "${HOME}/.openclaw/includeDefaultMemory"
else
echo "true"
fi
;;
memory.qmd.command) echo "qmd" ;;
agents.defaults.memorySearch.local.modelPath) echo "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf" ;;
agents.defaults.memorySearch.provider) echo "local" ;;
*) echo "" ;;
esac
exit 0
fi
if [[ "$cmd" == "config set"* ]]; then
key="$3"
val="$4"
if [[ "$key" == "memory.qmd.includeDefaultMemory" ]]; then
echo "$val" > "${HOME}/.openclaw/includeDefaultMemory"
fi
if [[ "$key" == "memory.backend" ]]; then
echo "$val" > "${HOME}/.openclaw/backend"
fi
if [[ "$key" == "memory.qmd.command" ]]; then
echo "$val" > "${HOME}/.openclaw/qmd_command"
fi
if [[ "$key" == "agents.defaults.memorySearch.provider" ]]; then
echo "$val" > "${HOME}/.openclaw/provider"
fi
exit 0
fi
if [[ "$1" == "memory" && "$2" == "status" ]]; then
agent="main"
while [ $# -gt 0 ]; do
if [ "$1" = "--agent" ] && [ $# -ge 2 ]; then
agent="$2"
shift 2
continue
fi
shift
done
cat <<TXT
Provider: qmd (requested: qmd)
Vector: ready
Indexed: 23/14 files
Workspace: ${HOME}/.openclaw/workspace
Store: ${OPENCLAW_STORE:-~/.openclaw/workspace/memory/index.sqlite}
TXT
exit 0
fi
if [[ "$1" == "memory" && "$2" == "index" ]]; then
echo "$cmd" >> "${HOME}/.openclaw/index_calls"
echo "index ok"
exit 0
fi
if [[ "$cmd" == "gateway restart" ]]; then
echo "$cmd" >> "${HOME}/.openclaw/gateway_calls"
echo "gateway restarted"
exit 0
fi
if [[ "$cmd" == "gateway stop" ]]; then
echo "gateway stopped"
exit 0
fi
if [[ "$cmd" == "gateway start" ]]; then
echo "gateway started"
exit 0
fi
echo "mock openclaw $*"
EOF_INNER
chmod +x "$WORKDIR/bin/openclaw"
# stub: qmd
cat > "$WORKDIR/bin/qmd" <<'EOF_INNER'
#!/usr/bin/env bash
exit 0
EOF_INNER
chmod +x "$WORKDIR/bin/qmd"
# stub: curl (用于网络探测)
cat > "$WORKDIR/bin/curl" <<'EOF_INNER'
#!/usr/bin/env bash
if [[ "$*" == *"huggingface.co"* ]]; then
exit 1
fi
if [[ "$*" == *"hf-mirror.com"* ]]; then
exit 0
fi
exit 0
EOF_INNER
chmod +x "$WORKDIR/bin/curl"
export HOME="$WORKDIR/home"
export PATH="$WORKDIR/bin:$PATH"
export TERM=xterm
# minimal openclaw.json
cat > "$HOME/.openclaw/openclaw.json" <<'JSON'
{
"memory": {
"backend": "qmd",
"qmd": {"command": "qmd", "includeDefaultMemory": true}
},
"agents": {
"defaults": {
"memorySearch": {
"provider": "local",
"local": {
"modelPath": "hf:ggml-org/embeddinggemma-300M-GGUF/embeddinggemma-300M-Q8_0.gguf"
}
}
}
}
}
JSON
# mock index sqlite
mkdir -p "$HOME/.openclaw/workspace/memory"
touch "$HOME/.openclaw/workspace/memory/index.sqlite"
# mock memory files
cat > "$HOME/.openclaw/workspace/MEMORY.md" <<'TXT'
# MEMORY
line1
line2
line3
TXT
cat > "$HOME/.openclaw/workspace/memory/2026-03-11.md" <<'TXT'
# 2026-03-11
foo
bar
TXT
source "$WORKDIR/harness.sh"
run_menu() {
local input="$1"
local title="$2"
local outfile="$3"
echo "[TEST] $title"
printf "%b" "$input" | openclaw_memory_menu >"$outfile"
}
# 1) 状态展示(直接返回)
run_menu "0\n" "status" "$WORKDIR/out_status.txt"
# 2) 更新索引(增量)
run_menu "1\nyes\n\n\n0\n" "index" "$WORKDIR/out_index.txt"
# 3) 查看记忆文件(列表+查看)
run_menu "2\n1\n\n\n\n\n0\n0\n" "files" "$WORKDIR/out_files.txt"
# 4) 索引修复(检测到 includeDefaultMemory=false → 恢复为 true 并重建)
echo "false" > "$HOME/.openclaw/includeDefaultMemory"
run_menu "3\ny\n\n\n0\n" "fix" "$WORKDIR/out_fix.txt"
# 5) 记忆方案(自动推荐并取消)
run_menu "4\n1\n\n0\n0\n" "scheme" "$WORKDIR/out_scheme.txt"
# 6) Store 缺失时仍执行 rebuild
: > "$HOME/.openclaw/index_calls"
OPENCLAW_STORE="~/.openclaw/workspace/memory/missing.sqlite" \
openclaw_memory_rebuild_index_safe >"$WORKDIR/out_missing.txt"
export WORKDIR
python3 - <<'PY'
import glob
import os
flag_path = os.path.expanduser('~/.openclaw/includeDefaultMemory')
if not os.path.exists(flag_path):
raise SystemExit('includeDefaultMemory not updated')
flag = open(flag_path, 'r', encoding='utf-8').read().strip()
if flag != 'true':
raise SystemExit('includeDefaultMemory not restored to true')
# ensure backup created
baks = glob.glob(os.path.expanduser('~/.openclaw/workspace/memory/index.sqlite.bak.*'))
if not baks:
raise SystemExit('index sqlite backup not created')
# ensure missing store still triggers force index
calls_path = os.path.expanduser('~/.openclaw/index_calls')
if not os.path.exists(calls_path):
raise SystemExit('index calls not recorded')
with open(calls_path, 'r', encoding='utf-8') as fh:
calls = fh.read()
if 'memory index' not in calls or '--force' not in calls:
raise SystemExit('force index not called for missing store')
# ensure gateway restart called after rebuild
gw_calls_path = os.path.expanduser('~/.openclaw/gateway_calls')
if not os.path.exists(gw_calls_path):
raise SystemExit('gateway restart not recorded')
with open(gw_calls_path, 'r', encoding='utf-8') as fh:
gw_calls = fh.read()
if 'gateway restart' not in gw_calls:
raise SystemExit('gateway restart not called after rebuild')
# ensure warning emitted
with open(os.path.join(os.environ['WORKDIR'], 'out_missing.txt'), 'r', encoding='utf-8') as fh:
out = fh.read()
if 'Store 原始值' not in out:
raise SystemExit('missing store warning not found')
if '索引已重建并自动重启网关' not in out:
raise SystemExit('auto restart message not found')
print('SMOKE_OK')
PY

View File

@@ -0,0 +1,150 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
SCRIPT="$REPO_ROOT/kejilion.sh"
WORKDIR="${TMPDIR:-/tmp}/openclaw-memory-multi-agent-test-$$"
mkdir -p "$WORKDIR/bin" "$WORKDIR/home/.openclaw/workspace/memory"
KEEP_WORKDIR=${KEEP_WORKDIR:-false}
trap '[ "$KEEP_WORKDIR" = "true" ] || rm -rf "$WORKDIR"' EXIT
cat > "$WORKDIR/harness.sh" <<'EOF_INNER'
#!/usr/bin/env bash
set -euo pipefail
break_end() { return 0; }
send_stats() { return 0; }
EOF_INNER
awk 'BEGIN{p=0} /openclaw_memory_config_file\(\) \{/{p=1} /openclaw_permission_config_file\(\) \{/{p=0} p{print}' "$SCRIPT" >> "$WORKDIR/harness.sh"
chmod +x "$WORKDIR/harness.sh"
cat > "$WORKDIR/bin/openclaw" <<'EOF_INNER'
#!/usr/bin/env bash
set -euo pipefail
cmd="$*"
if [[ "$cmd" == "agents list --json" ]]; then
cat <<'JSON'
[
{"id":"main","workspace":"~/.openclaw/workspace"},
{"id":"work","workspace":"~/.openclaw/workspace-work"}
]
JSON
exit 0
fi
if [[ "$cmd" == "config get"* ]]; then
key="$3"
case "$key" in
memory.backend) echo "qmd" ;;
memory.qmd.command) echo "qmd" ;;
agents.defaults.memorySearch.provider) echo "local" ;;
agents.defaults.memorySearch.local.modelPath) echo "$HOME/.openclaw/models/embedding/model.gguf" ;;
*) echo "" ;;
esac
exit 0
fi
if [[ "$cmd" == "config set"* || "$cmd" == "config unset"* ]]; then
exit 0
fi
if [[ "$1" == "memory" && "$2" == "status" ]]; then
agent="main"
while [ $# -gt 0 ]; do
if [ "$1" = "--agent" ] && [ $# -ge 2 ]; then
agent="$2"
shift 2
continue
fi
shift
done
if [ "$agent" = "work" ]; then
cat <<TXT
Provider: qmd (requested: qmd)
Vector: ready
Indexed: 3/2 files
Workspace: $HOME/.openclaw/workspace-work
Store: $HOME/.openclaw/workspace-work/memory/index.sqlite
TXT
else
cat <<TXT
Provider: qmd (requested: qmd)
Vector: ready
Indexed: 5/4 files
Workspace: $HOME/.openclaw/workspace
Store: $HOME/.openclaw/workspace/memory/index.sqlite
TXT
fi
exit 0
fi
if [[ "$1" == "memory" && "$2" == "index" ]]; then
echo "$cmd" >> "$HOME/.openclaw/index_calls"
exit 0
fi
if [[ "$1" == "gateway" && "$2" == "restart" ]]; then
echo "$cmd" >> "$HOME/.openclaw/gateway_calls"
exit 0
fi
echo "mock openclaw $*"
EOF_INNER
chmod +x "$WORKDIR/bin/openclaw"
export HOME="$WORKDIR/home"
export PATH="$WORKDIR/bin:$PATH"
export TERM=xterm
mkdir -p "$HOME/.openclaw/workspace/memory" "$HOME/.openclaw/workspace-work/memory"
touch "$HOME/.openclaw/workspace/memory/index.sqlite" "$HOME/.openclaw/workspace-work/memory/index.sqlite"
cat > "$HOME/.openclaw/openclaw.json" <<'JSON'
{
"agents": {
"list": [
{"id": "work", "workspace": "~/.openclaw/workspace-work"}
]
}
}
JSON
cat > "$HOME/.openclaw/workspace/MEMORY.md" <<'TXT'
# main memory
TXT
cat > "$HOME/.openclaw/workspace-work/MEMORY.md" <<'TXT'
# work memory
TXT
cat > "$HOME/.openclaw/workspace-work/memory/2026-03-23.md" <<'TXT'
# work note
TXT
source "$WORKDIR/harness.sh"
openclaw_memory_render_status > "$WORKDIR/out_status.txt"
openclaw_memory_file_render_list > "$WORKDIR/out_files.txt"
openclaw_memory_prepare_workspace_all > "$WORKDIR/out_prepare.txt"
openclaw_memory_rebuild_index_all > "$WORKDIR/out_rebuild.txt"
export WORKDIR
python3 - <<'PY'
import os
from pathlib import Path
workdir = Path(os.environ['WORKDIR'])
home = workdir / 'home'
status_out = (workdir / 'out_status.txt').read_text(encoding='utf-8')
files_out = (workdir / 'out_files.txt').read_text(encoding='utf-8')
prepare_out = (workdir / 'out_prepare.txt').read_text(encoding='utf-8')
rebuild_out = (workdir / 'out_rebuild.txt').read_text(encoding='utf-8')
index_calls = (home / '.openclaw' / 'index_calls').read_text(encoding='utf-8')
gateway_calls = (home / '.openclaw' / 'gateway_calls').read_text(encoding='utf-8')
if 'Agent: main' not in status_out or 'Agent: work' not in status_out:
raise SystemExit('multi-agent status missing agent sections')
if 'main/MEMORY.md' not in files_out or 'work/MEMORY.md' not in files_out:
raise SystemExit('multi-agent file list missing memory files')
if 'work/memory/2026-03-23.md' not in files_out:
raise SystemExit('work daily memory file missing')
if '检查并准备 2 个智能体工作区' not in prepare_out:
raise SystemExit('prepare summary missing agent count')
if '--agent main --force' not in index_calls or '--agent work --force' not in index_calls:
raise SystemExit('force index not called for all agents')
if 'gateway restart' not in gateway_calls:
raise SystemExit('gateway restart not called after multi-agent rebuild')
if '已为 2 个智能体重建索引' not in rebuild_out:
raise SystemExit('multi-agent rebuild summary missing')
print('SMOKE_OK')
PY

View File

@@ -0,0 +1,132 @@
#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
SCRIPT="$REPO_ROOT/kejilion.sh"
WORKDIR="${TMPDIR:-/tmp}/openclaw-plugin-skill-menu-test-$$"
mkdir -p "$WORKDIR/bin" "$WORKDIR/home/.openclaw/workspace/skills" "$WORKDIR/home/.openclaw"
KEEP_WORKDIR=${KEEP_WORKDIR:-false}
trap '[ "$KEEP_WORKDIR" = "true" ] || rm -rf "$WORKDIR"' EXIT
cat > "$WORKDIR/harness.sh" <<'EOF_INNER'
#!/usr/bin/env bash
set -euo pipefail
break_end() { return 0; }
send_stats() { return 0; }
start_gateway() { openclaw gateway stop; openclaw gateway start; }
EOF_INNER
# 抽取插件/技能管理实现
awk 'BEGIN{p=0} /resolve_openclaw_plugin_id\(\) \{/{p=1} /openclaw_json_get_bool\(\) \{/{p=0} p{print}' "$SCRIPT" >> "$WORKDIR/harness.sh"
chmod +x "$WORKDIR/harness.sh"
# stub: openclaw
cat > "$WORKDIR/bin/openclaw" <<'EOF_INNER'
#!/usr/bin/env bash
cmd="$*"
if [[ "$cmd" == "plugins list" ]]; then
cat <<TXT
feishu disabled
telegram enabled
TXT
exit 0
fi
if [[ "$cmd" == "plugins enable"* ]]; then
echo "enabled $*" >> "${TEST_LOG:-/tmp/openclaw-plugin.log}"
exit 0
fi
if [[ "$cmd" == "plugins install"* ]]; then
echo "install $*" >> "${TEST_LOG:-/tmp/openclaw-plugin.log}"
exit 0
fi
if [[ "$cmd" == "plugins disable"* ]]; then
echo "disable $*" >> "${TEST_LOG:-/tmp/openclaw-plugin.log}"
exit 0
fi
if [[ "$cmd" == "plugins uninstall"* ]]; then
# 模拟预装卸载失败
if [[ "$cmd" == *"feishu"* ]]; then
exit 1
fi
echo "uninstall $*" >> "${TEST_LOG:-/tmp/openclaw-plugin.log}"
exit 0
fi
if [[ "$cmd" == "skills list" ]]; then
echo "skill1"
exit 0
fi
if [[ "$cmd" == "gateway stop" ]]; then
echo "gateway stop" >> "${TEST_LOG:-/tmp/openclaw-plugin.log}"
exit 0
fi
if [[ "$cmd" == "gateway start" ]]; then
echo "gateway start" >> "${TEST_LOG:-/tmp/openclaw-plugin.log}"
exit 0
fi
echo "mock openclaw $*" >> "${TEST_LOG:-/tmp/openclaw-plugin.log}"
EOF_INNER
chmod +x "$WORKDIR/bin/openclaw"
# stub: npx
cat > "$WORKDIR/bin/npx" <<'EOF_INNER'
#!/usr/bin/env bash
if [[ "$*" == *"clawhub uninstall"* ]]; then
echo "uninstall $*" >> "${TEST_LOG:-/tmp/openclaw-plugin.log}"
exit 0
fi
if [[ "$*" == *"clawhub install"* ]]; then
echo "install $*" >> "${TEST_LOG:-/tmp/openclaw-plugin.log}"
exit 0
fi
exit 0
EOF_INNER
chmod +x "$WORKDIR/bin/npx"
export HOME="$WORKDIR/home"
export PATH="$WORKDIR/bin:$PATH"
export TERM=xterm
export TEST_LOG="$WORKDIR/test.log"
cat > "$HOME/.openclaw/openclaw.json" <<'JSON'
{"plugins":{"allow":["telegram","feishu"]}}
JSON
# 模拟用户技能目录
mkdir -p "$HOME/.openclaw/workspace/skills/skillA"
source "$WORKDIR/harness.sh"
# 插件安装:多输入,确保只重启一次
printf "1\nfeishu telegram\n0\n" | install_plugin > "$WORKDIR/plugin_install.out"
# 插件删除:多输入,确保删除流程与 allowlist 移除
printf "2\nfeishu telegram\n0\n" | install_plugin > "$WORKDIR/plugin_delete.out"
# 技能删除:二次确认 + 多输入,删除目录
printf "2\nskillA skillB\ny\n0\n" | install_skill > "$WORKDIR/skill_delete.out"
# 断言start_gateway 只调用一次/每次菜单
start_count=$(grep -c "gateway start" "$TEST_LOG" || true)
if [ "$start_count" -ne 3 ]; then
echo "Expected 3 gateway starts (plugin install/delete + skill delete), got $start_count"
exit 1
fi
# 断言:用户技能目录已删除
if [ -d "$HOME/.openclaw/workspace/skills/skillA" ]; then
echo "Skill directory was not removed"
exit 1
fi
# 断言plugins.allow 移除
if grep -q '"feishu"' "$HOME/.openclaw/openclaw.json"; then
echo "feishu still in plugins.allow"
exit 1
fi
# 断言:输出汇总
grep -q "操作汇总" "$WORKDIR/plugin_install.out"
grep -q "操作汇总" "$WORKDIR/plugin_delete.out"
grep -q "操作汇总" "$WORKDIR/skill_delete.out"
echo "SMOKE_OK"

View File

@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root=$(cd "$(dirname "$0")/../.." && pwd)
script="$repo_root/kejilion.sh"
workdir=$(mktemp -d)
trap 'rm -rf "$workdir"' EXIT
export HOME="$workdir/home"
mkdir -p "$HOME/.openclaw/agents/main/sessions"
mkdir -p "$HOME/.openclaw/agents/work/sessions"
funcs_file="$workdir/funcs.sh"
{
awk '/^[[:space:]]*openclaw_get_agents_dir\(\)/,/^[[:space:]]*\}$/' "$script"
echo
awk '/^[[:space:]]*openclaw_sync_sessions_model\(\)/,/^[[:space:]]*\}$/' "$script"
} > "$funcs_file"
cat > "$HOME/.openclaw/agents/main/sessions/sessions.json" <<'JSON'
{
"agent:main:telegram:direct:123": {
"sessionId": "abc-123",
"modelOverride": "gpt-4",
"providerOverride": "openai"
},
"agent:main:main": {
"sessionId": "def-456"
}
}
JSON
cat > "$HOME/.openclaw/agents/work/sessions/sessions.json" <<'JSON'
{
"agent:work:slack:channel:C789": {
"sessionId": "ghi-789",
"modelOverride": "claude-3",
"providerOverride": "anthropic"
}
}
JSON
source "$funcs_file"
openclaw_sync_sessions_model "cli-api/gpt-5-pro"
echo "=== VERIFICATION ==="
main_file="$HOME/.openclaw/agents/main/sessions/sessions.json"
work_file="$HOME/.openclaw/agents/work/sessions/sessions.json"
main_model=$(jq -r '."agent:main:telegram:direct:123".modelOverride' "$main_file")
main_provider=$(jq -r '."agent:main:telegram:direct:123".providerOverride' "$main_file")
main2_model=$(jq -r '."agent:main:main".modelOverride' "$main_file")
main2_provider=$(jq -r '."agent:main:main".providerOverride // "null"' "$main_file")
work_model=$(jq -r '."agent:work:slack:channel:C789".modelOverride' "$work_file")
work_provider=$(jq -r '."agent:work:slack:channel:C789".providerOverride' "$work_file")
[ "$main_model" = "gpt-5-pro" ] || { echo "FAIL: main model (got: $main_model)"; exit 1; }
[ "$main_provider" = "cli-api" ] || { echo "FAIL: main provider (got: $main_provider)"; exit 1; }
[ "$main2_model" = "gpt-5-pro" ] || { echo "FAIL: main2 model (got: $main2_model)"; exit 1; }
[ "$main2_provider" = "cli-api" ] || { echo "FAIL: main2 provider (got: $main2_provider)"; exit 1; }
[ "$work_model" = "gpt-5-pro" ] || { echo "FAIL: work model (got: $work_model)"; exit 1; }
[ "$work_provider" = "cli-api" ] || { echo "FAIL: work provider (got: $work_provider)"; exit 1; }
echo "PASS: openclaw_sync_sessions_model smoke"

View File

@@ -0,0 +1,77 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root=$(cd "$(dirname "$0")/.." && pwd)
script="$repo_root/kejilion.sh"
workdir=$(mktemp -d)
trap 'rm -rf "$workdir"' EXIT
export HOME="$workdir/home"
mkdir -p "$HOME/.openclaw"
python_extract() {
python3 - "$script" <<'PY'
import re, sys
text = open(sys.argv[1], encoding='utf-8').read()
patterns = [
r'openclaw_get_config_file\(\) \{.*?\n\t\t\}',
r'openclaw_memory_config_file\(\) \{.*?\n\t\}',
r'openclaw_permission_config_file\(\) \{.*?\n\t\}',
]
for pat in patterns:
m = re.search(pat, text, re.S)
if not m:
raise SystemExit(f'missing pattern: {pat}')
print(m.group(0))
print()
PY
}
snippet="$workdir/snippet.sh"
{
echo '#!/usr/bin/env bash'
echo 'set -euo pipefail'
python_extract
cat <<'SH'
main() {
echo "CFG=$(openclaw_get_config_file)"
echo "MEM=$(openclaw_memory_config_file)"
echo "PERM=$(openclaw_permission_config_file)"
}
main "$@"
SH
} > "$snippet"
chmod +x "$snippet"
assert_eq() {
local got="$1" expected="$2" msg="$3"
if [ "$got" != "$expected" ]; then
echo "ASSERT FAIL: $msg"
echo "got: $got"
echo "exp: $expected"
exit 1
fi
}
user_cfg="$HOME/.openclaw/openclaw.json"
root_cfg="$workdir/root/.openclaw/openclaw.json"
mkdir -p "$(dirname "$root_cfg")"
out=$("$snippet")
assert_eq "$(printf '%s\n' "$out" | sed -n '1p')" "CFG=$user_cfg" 'default config path should be user path'
assert_eq "$(printf '%s\n' "$out" | sed -n '2p')" "MEM=$user_cfg" 'memory config path should follow helper'
assert_eq "$(printf '%s\n' "$out" | sed -n '3p')" "PERM=$user_cfg" 'permission config path should follow helper'
printf '{}\n' > "$root_cfg"
rm -f "$user_cfg"
out=$("$snippet")
assert_eq "$(printf '%s\n' "$out" | sed -n '1p')" "CFG=$user_cfg" 'non-root should not silently fallback to root config'
assert_eq "$(printf '%s\n' "$out" | sed -n '2p')" "MEM=$user_cfg" 'memory config should stay in user path for non-root'
assert_eq "$(printf '%s\n' "$out" | sed -n '3p')" "PERM=$user_cfg" 'permission config should stay in user path for non-root'
mkdir -p "$(dirname "$user_cfg")"
printf '{"user":true}\n' > "$user_cfg"
out=$("$snippet")
assert_eq "$(printf '%s\n' "$out" | sed -n '1p')" "CFG=$user_cfg" 'user config should take precedence'
echo 'PASS: openclaw config path resolution smoke'