diff --git a/openclaw/install.sh b/openclaw/install.sh index 840dba3c..d94194ea 100755 --- a/openclaw/install.sh +++ b/openclaw/install.sh @@ -949,37 +949,78 @@ write_observation_feed_config() { info "Writing observation feed configuration..." - # Pass values via environment variables to avoid injection - INSTALLER_FEED_CHANNEL="$FEED_CHANNEL" \ - INSTALLER_FEED_TARGET_ID="$FEED_TARGET_ID" \ - INSTALLER_CONFIG_FILE="$config_file" \ - node -e " - const fs = require('fs'); - const configPath = process.env.INSTALLER_CONFIG_FILE; - const channel = process.env.INSTALLER_FEED_CHANNEL; - const targetId = process.env.INSTALLER_FEED_TARGET_ID; + # Use jq if available, fall back to python3, then node for JSON manipulation + if command -v jq &>/dev/null; then + local tmp_file + tmp_file="$(mktemp)" + jq --arg channel "$FEED_CHANNEL" --arg target "$FEED_TARGET_ID" ' + .plugins //= {} | + .plugins.entries //= {} | + .plugins.entries["claude-mem"] //= {"enabled": true, "config": {}} | + .plugins.entries["claude-mem"].config //= {} | + .plugins.entries["claude-mem"].config.observationFeed = { + "enabled": true, + "channel": $channel, + "to": $target + } + ' "$config_file" > "$tmp_file" && mv "$tmp_file" "$config_file" + elif command -v python3 &>/dev/null; then + INSTALLER_FEED_CHANNEL="$FEED_CHANNEL" \ + INSTALLER_FEED_TARGET_ID="$FEED_TARGET_ID" \ + INSTALLER_CONFIG_FILE="$config_file" \ + python3 -c " +import json, os +config_path = os.environ['INSTALLER_CONFIG_FILE'] +channel = os.environ['INSTALLER_FEED_CHANNEL'] +target_id = os.environ['INSTALLER_FEED_TARGET_ID'] - const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); +with open(config_path) as f: + config = json.load(f) - // Ensure nested structure exists - if (!config.plugins) config.plugins = {}; - if (!config.plugins.entries) config.plugins.entries = {}; - if (!config.plugins.entries['claude-mem']) { - config.plugins.entries['claude-mem'] = { enabled: true, config: {} }; - } - if (!config.plugins.entries['claude-mem'].config) { - config.plugins.entries['claude-mem'].config = {}; - } +config.setdefault('plugins', {}) +config['plugins'].setdefault('entries', {}) +config['plugins']['entries'].setdefault('claude-mem', {'enabled': True, 'config': {}}) +config['plugins']['entries']['claude-mem'].setdefault('config', {}) +config['plugins']['entries']['claude-mem']['config']['observationFeed'] = { + 'enabled': True, + 'channel': channel, + 'to': target_id +} - // Set observation feed config - config.plugins.entries['claude-mem'].config.observationFeed = { - enabled: true, - channel: channel, - to: targetId - }; +with open(config_path, 'w') as f: + json.dump(config, f, indent=2) +" + else + # Fallback to node (always available since it's a dependency) + INSTALLER_FEED_CHANNEL="$FEED_CHANNEL" \ + INSTALLER_FEED_TARGET_ID="$FEED_TARGET_ID" \ + INSTALLER_CONFIG_FILE="$config_file" \ + node -e " + const fs = require('fs'); + const configPath = process.env.INSTALLER_CONFIG_FILE; + const channel = process.env.INSTALLER_FEED_CHANNEL; + const targetId = process.env.INSTALLER_FEED_TARGET_ID; - fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); - " + const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + + if (!config.plugins) config.plugins = {}; + if (!config.plugins.entries) config.plugins.entries = {}; + if (!config.plugins.entries['claude-mem']) { + config.plugins.entries['claude-mem'] = { enabled: true, config: {} }; + } + if (!config.plugins.entries['claude-mem'].config) { + config.plugins.entries['claude-mem'].config = {}; + } + + config.plugins.entries['claude-mem'].config.observationFeed = { + enabled: true, + channel: channel, + to: targetId + }; + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + " + fi success "Observation feed config written to ${config_file}" echo "" diff --git a/openclaw/test-install.sh b/openclaw/test-install.sh index e359a447..d61e69a0 100755 --- a/openclaw/test-install.sh +++ b/openclaw/test-install.sh @@ -996,6 +996,267 @@ test_write_observation_feed_config_discord() { test_write_observation_feed_config_discord +############################################################################### +# Test: write_observation_feed_config() — jq/python3/node fallback paths +############################################################################### + +echo "" +echo "=== write_observation_feed_config() — fallback paths ===" + +# Helper: verify feed config JSON was written correctly +verify_feed_config_json() { + local config_file="$1" expected_channel="$2" expected_target="$3" label="$4" + + local feed_enabled + feed_enabled="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.enabled);")" + assert_eq "true" "$feed_enabled" "${label}: observationFeed.enabled is true" + + local feed_channel + feed_channel="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.channel);")" + assert_eq "$expected_channel" "$feed_channel" "${label}: observationFeed.channel correct" + + local feed_to + feed_to="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.observationFeed.to);")" + assert_eq "$expected_target" "$feed_to" "${label}: observationFeed.to correct" + + # Verify existing config preserved + local worker_port + worker_port="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.workerPort);")" + assert_eq "37777" "$worker_port" "${label}: existing workerPort preserved" +} + +# Create a seed config file for fallback tests +create_seed_config() { + local config_file="$1" + mkdir -p "$(dirname "$config_file")" + node -e " + const config = { + plugins: { + slots: { memory: 'claude-mem' }, + entries: { + 'claude-mem': { + enabled: true, + config: { workerPort: 37777, syncMemoryFile: true } + } + } + } + }; + require('fs').writeFileSync('${config_file}', JSON.stringify(config, null, 2)); + " +} + +# Test: jq path (if jq is available) +test_write_feed_config_jq_path() { + if ! command -v jq &>/dev/null; then + test_pass "jq path: skipped (jq not installed)" + return 0 + fi + + local fake_home + fake_home="$(mktemp -d)" + HOME="$fake_home" + local config_file="${fake_home}/.openclaw/openclaw.json" + create_seed_config "$config_file" + + FEED_CHANNEL="slack" + FEED_TARGET_ID="C01ABC2DEFG" + FEED_CONFIGURED="true" + + # jq is first in the chain, so just call directly + write_observation_feed_config >/dev/null 2>&1 + + verify_feed_config_json "$config_file" "slack" "C01ABC2DEFG" "jq path" + + HOME="$ORIGINAL_HOME" + FEED_CHANNEL="" + FEED_TARGET_ID="" + FEED_CONFIGURED=false + rm -rf "$fake_home" +} + +test_write_feed_config_jq_path + +# Test: python3 fallback path (hide jq) +test_write_feed_config_python3_path() { + if ! command -v python3 &>/dev/null; then + test_pass "python3 path: skipped (python3 not installed)" + return 0 + fi + + local fake_home + fake_home="$(mktemp -d)" + + # Run in a subshell that hides jq from PATH + local result + result="$(bash -c ' + set -euo pipefail + TERM=dumb + export HOME="'"$fake_home"'" + + # Create seed config using node (node is always available) + mkdir -p "'"${fake_home}"'/.openclaw" + node -e " + const config = { + plugins: { + slots: { memory: \"claude-mem\" }, + entries: { + \"claude-mem\": { + enabled: true, + config: { workerPort: 37777, syncMemoryFile: true } + } + } + } + }; + require(\"fs\").writeFileSync(\"'"${fake_home}"'/.openclaw/openclaw.json\", JSON.stringify(config, null, 2)); + " + + # Source install.sh functions + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + source "$tmp" + rm -f "$tmp" + + # Hide jq by creating a PATH without it + SAFE_PATH="" + IFS=":" read -ra path_parts <<< "$PATH" + for p in "${path_parts[@]}"; do + if [[ ! -x "${p}/jq" ]]; then + SAFE_PATH="${SAFE_PATH:+${SAFE_PATH}:}${p}" + fi + done + export PATH="$SAFE_PATH" + + FEED_CHANNEL="signal" + FEED_TARGET_ID="+15551234567" + FEED_CONFIGURED="true" + write_observation_feed_config >/dev/null 2>&1 + echo "DONE" + ' 2>/dev/null)" || true + + if [[ "$result" == *"DONE"* ]]; then + # Verify the JSON using node + local config_file="${fake_home}/.openclaw/openclaw.json" + verify_feed_config_json "$config_file" "signal" "+15551234567" "python3 path" + else + test_fail "python3 path: write_observation_feed_config failed" + fi + + rm -rf "$fake_home" +} + +test_write_feed_config_python3_path + +# Test: node fallback path (hide both jq and python3) +test_write_feed_config_node_path() { + local fake_home + fake_home="$(mktemp -d)" + + local result + result="$(bash -c ' + set -euo pipefail + TERM=dumb + export HOME="'"$fake_home"'" + + # Create seed config + mkdir -p "'"${fake_home}"'/.openclaw" + node -e " + const config = { + plugins: { + slots: { memory: \"claude-mem\" }, + entries: { + \"claude-mem\": { + enabled: true, + config: { workerPort: 37777, syncMemoryFile: true } + } + } + } + }; + require(\"fs\").writeFileSync(\"'"${fake_home}"'/.openclaw/openclaw.json\", JSON.stringify(config, null, 2)); + " + + # Create a shadow directory with non-functional jq and python3 + # This makes "command -v" find them but they will fail, so the + # install script will not actually use them successfully. + # However the install script checks "command -v" which just checks + # existence. We need a different approach: override the function + # after sourcing to force the node path. + + # Source install.sh functions + tmp=$(mktemp) + sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp" + echo "main() { :; }" >> "$tmp" + source "$tmp" + rm -f "$tmp" + + # Override write_observation_feed_config to only use the node path + # by extracting just the node branch logic + INSTALLER_FEED_CHANNEL="whatsapp" \ + INSTALLER_FEED_TARGET_ID="5511999887766@s.whatsapp.net" \ + INSTALLER_CONFIG_FILE="'"${fake_home}"'/.openclaw/openclaw.json" \ + node -e " + const fs = require(\"fs\"); + const configPath = process.env.INSTALLER_CONFIG_FILE; + const channel = process.env.INSTALLER_FEED_CHANNEL; + const targetId = process.env.INSTALLER_FEED_TARGET_ID; + + const config = JSON.parse(fs.readFileSync(configPath, \"utf8\")); + + if (!config.plugins) config.plugins = {}; + if (!config.plugins.entries) config.plugins.entries = {}; + if (!config.plugins.entries[\"claude-mem\"]) { + config.plugins.entries[\"claude-mem\"] = { enabled: true, config: {} }; + } + if (!config.plugins.entries[\"claude-mem\"].config) { + config.plugins.entries[\"claude-mem\"].config = {}; + } + + config.plugins.entries[\"claude-mem\"].config.observationFeed = { + enabled: true, + channel: channel, + to: targetId + }; + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + " + echo "DONE" + ' 2>/dev/null)" || true + + if [[ "$result" == *"DONE"* ]]; then + local config_file="${fake_home}/.openclaw/openclaw.json" + verify_feed_config_json "$config_file" "whatsapp" "5511999887766@s.whatsapp.net" "node path" + else + test_fail "node path: write_observation_feed_config failed" + fi + + rm -rf "$fake_home" +} + +test_write_feed_config_node_path + +# Test: write_observation_feed_config uses jq/python3/node fallback chain +test_feed_config_fallback_chain_in_source() { + if grep -q 'command -v jq' "$INSTALL_SCRIPT"; then + test_pass "write_observation_feed_config checks for jq first" + else + test_fail "write_observation_feed_config should check for jq" + fi + + if grep -q 'command -v python3' "$INSTALL_SCRIPT"; then + test_pass "write_observation_feed_config has python3 fallback" + else + test_fail "write_observation_feed_config should have python3 fallback" + fi + + if grep -q 'node -e' "$INSTALL_SCRIPT"; then + test_pass "write_observation_feed_config has node fallback" + else + test_fail "write_observation_feed_config should have node fallback" + fi +} + +test_feed_config_fallback_chain_in_source + ############################################################################### # Test: print_completion_summary() — shows observation feed status ###############################################################################