#!/usr/bin/env bash # verify-compat.sh — Compatibility constraint harness # # Tests all known mutual-exclusion constraints simultaneously. # A fix for one axis must not break another. Run after every # glibc-compat.js or wrapper change. # # Usage: # bash tests/verify-compat.sh (on device) # ssh device 'bash ~/verify-compat.sh' (remote) set -uo pipefail PASS=0; FAIL=0; TOTAL=0 RED='\033[0;31m'; GREEN='\033[0;32m'; NC='\033[0m' pass() { echo -e "${GREEN}[PASS]${NC} $1"; PASS=$((PASS+1)); TOTAL=$((TOTAL+1)); } fail() { echo -e "${RED}[FAIL]${NC} $1"; FAIL=$((FAIL+1)); TOTAL=$((TOTAL+1)); } check() { local desc="$1"; shift if "$@" >/dev/null 2>&1; then pass "$desc"; else fail "$desc"; fi } node_check() { local desc="$1" code="$2" if node -e "$code" 2>/dev/null; then pass "$desc"; else fail "$desc"; fi } echo "=== Compatibility Constraint Harness ===" echo "" # ───────────────────────────────────────────────── # AXIS 1: LD_PRELOAD lifecycle # Constraint A: node.real must load WITHOUT bionic LD_PRELOAD # Constraint B: child bionic processes must HAVE LD_PRELOAD # ───────────────────────────────────────────────── echo "--- Axis 1: LD_PRELOAD lifecycle ---" # 1a: node.real itself must not have libtermux-exec loaded in its own process # (if it did, glibc/bionic mismatch would crash — the fact that we're # running means it didn't, but verify the wrapper structure) WRAPPER=$(node -e "process.stdout.write(process.env._OA_WRAPPER_PATH || '')" 2>/dev/null) if [ -n "$WRAPPER" ] && [ -f "$WRAPPER" ] && grep -q "unset LD_PRELOAD" "$WRAPPER"; then pass "1a: node wrapper unsets LD_PRELOAD before exec" else fail "1a: node wrapper missing or missing 'unset LD_PRELOAD' (path: ${WRAPPER:-not set})" fi # 1b: LD_PRELOAD must be restored in node's environment (for child inheritance) node_check "1b: LD_PRELOAD restored in node env" \ "if (!process.env.LD_PRELOAD) process.exit(1)" # 1c: child /bin/sh inherits LD_PRELOAD CHILD_LP=$(node -e " const { execSync } = require('child_process'); process.stdout.write(execSync('echo \$LD_PRELOAD', {encoding:'utf8'}).trim()); " 2>/dev/null) if [ -n "$CHILD_LP" ] && echo "$CHILD_LP" | grep -q "libtermux-exec"; then pass "1c: child sh inherits LD_PRELOAD (libtermux-exec)" else fail "1c: child sh missing LD_PRELOAD (got: ${CHILD_LP:-empty})" fi # ───────────────────────────────────────────────── # AXIS 2: Shebang resolution # Constraint A: #!/usr/bin/env must resolve in child processes # Constraint B: our own wrappers must NOT use #!/usr/bin/env # ───────────────────────────────────────────────── echo "--- Axis 2: Shebang resolution ---" # 2a: shebang with /usr/bin/env works from node child process TMPSCRIPT="$(mktemp "${TMPDIR:-/tmp}/compat-test.XXXXXX")" echo '#!/usr/bin/env sh' > "$TMPSCRIPT" echo 'echo shebang-ok' >> "$TMPSCRIPT" chmod +x "$TMPSCRIPT" SHEBANG_OUT=$(node -e " const { execSync } = require('child_process'); process.stdout.write(execSync('$TMPSCRIPT', {encoding:'utf8'}).trim()); " 2>/dev/null) if [ "$SHEBANG_OUT" = "shebang-ok" ]; then pass "2a: #!/usr/bin/env sh shebang resolves from node" else fail "2a: #!/usr/bin/env sh shebang failed (got: ${SHEBANG_OUT:-empty})" fi rm -f "$TMPSCRIPT" # 2b: our wrappers do NOT use #!/usr/bin/env OUR_WRAPPERS_OK=true WRAPPER_DIR=$(dirname "$WRAPPER" 2>/dev/null) for f in "$WRAPPER_DIR/node" "$WRAPPER_DIR/npm" "$WRAPPER_DIR/npx"; do if [ -f "$f" ] && head -1 "$f" | grep -q "/usr/bin/env"; then fail "2b: $f uses #!/usr/bin/env (will break)" OUR_WRAPPERS_OK=false fi done $OUR_WRAPPERS_OK && pass "2b: our wrappers avoid #!/usr/bin/env" # ───────────────────────────────────────────────── # AXIS 3: process identity # Constraint A: process.platform must be 'linux' # Constraint B: process.execPath must point to wrapper, not ld.so # ───────────────────────────────────────────────── echo "--- Axis 3: process identity ---" node_check "3a: process.platform === 'linux'" \ "if (process.platform !== 'linux') process.exit(1)" EXEC_PATH=$(node -e "process.stdout.write(process.execPath)" 2>/dev/null) if [ -x "$EXEC_PATH" ] && ! file "$EXEC_PATH" 2>/dev/null | grep -q ELF; then pass "3b: process.execPath → wrapper script (not ld.so)" else fail "3b: process.execPath is not a wrapper script: $EXEC_PATH" fi # ───────────────────────────────────────────────── # AXIS 4: OS API shims # Constraint: patched APIs must return valid data # ───────────────────────────────────────────────── echo "--- Axis 4: OS API shims ---" node_check "4a: os.cpus().length > 0" \ "if (require('os').cpus().length === 0) process.exit(1)" node_check "4b: os.networkInterfaces() does not throw" \ "require('os').networkInterfaces()" # ───────────────────────────────────────────────── # AXIS 5: DNS resolution # Constraint: dns.lookup must work without resolv.conf # ───────────────────────────────────────────────── echo "--- Axis 5: DNS resolution ---" DNS_OK=$(node -e " const dns = require('dns'); dns.lookup('github.com', (err, addr) => { if (err) process.exit(1); process.stdout.write(addr); process.exit(0); }); " 2>/dev/null) if [ -n "$DNS_OK" ]; then pass "5a: dns.lookup('github.com') → $DNS_OK" else fail "5a: dns.lookup('github.com') failed" fi # ───────────────────────────────────────────────── # AXIS 6: child_process shell # Constraint A: /bin/sh must work (or shim must be active) # Constraint B: exec/execSync must default to valid shell # ───────────────────────────────────────────────── echo "--- Axis 6: child_process shell ---" node_check "6a: child_process.execSync works" \ "require('child_process').execSync('echo ok', {encoding:'utf8'})" CHILD_PLATFORM=$(node -e " const { execSync } = require('child_process'); process.stdout.write(execSync('node -e \"process.stdout.write(process.platform)\"', {encoding:'utf8'})); " 2>/dev/null) if [ "$CHILD_PLATFORM" = "linux" ]; then pass "6b: child node also reports platform=linux" else fail "6b: child node platform=$CHILD_PLATFORM (expected linux)" fi # ───────────────────────────────────────────────── # AXIS 7: npm lifecycle # Constraint: npm install with lifecycle scripts must succeed # ───────────────────────────────────────────────── echo "--- Axis 7: npm lifecycle ---" NPM_SCRIPT_SHELL=$(npm config get script-shell 2>/dev/null) if [ -n "$NPM_SCRIPT_SHELL" ] && [ -x "$NPM_SCRIPT_SHELL" ]; then pass "7a: npm script-shell=$NPM_SCRIPT_SHELL (executable)" else fail "7a: npm script-shell not set or not executable ($NPM_SCRIPT_SHELL)" fi # ───────────────────────────────────────────────── # AXIS 8: glibc-compat.js integrity # Constraint: all shims must be loaded # ───────────────────────────────────────────────── echo "--- Axis 8: glibc-compat.js integrity ---" COMPAT="$HOME/.openclaw-android/patches/glibc-compat.js" if [ -f "$COMPAT" ]; then pass "8a: glibc-compat.js exists" else fail "8a: glibc-compat.js missing" fi NODE_OPTS=$(node -e "process.stdout.write(process.env.NODE_OPTIONS||'')" 2>/dev/null) if echo "$NODE_OPTS" | grep -q "glibc-compat.js"; then pass "8b: NODE_OPTIONS includes glibc-compat.js" else fail "8b: glibc-compat.js not in NODE_OPTIONS ($NODE_OPTS)" fi # ───────────────────────────────────────────────── # Summary # ───────────────────────────────────────────────── echo "" echo "===============================" echo -e " Results: ${GREEN}$PASS passed${NC} / ${RED}$FAIL failed${NC} / $TOTAL total" echo "===============================" echo "" if [ "$FAIL" -gt 0 ]; then echo -e "${RED}COMPAT CHECK FAILED${NC} — fix failures before committing." exit 1 else echo -e "${GREEN}ALL CONSTRAINTS SATISFIED${NC}" fi