Files
claude-mem/openclaw/test-install.sh
T

1080 lines
36 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Test suite for openclaw/install.sh functions
# Tests the OpenClaw gateway detection, plugin install, and memory slot config.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INSTALL_SCRIPT="${SCRIPT_DIR}/install.sh"
TESTS_RUN=0
TESTS_PASSED=0
TESTS_FAILED=0
###############################################################################
# Test helpers
###############################################################################
test_pass() {
TESTS_RUN=$((TESTS_RUN + 1))
TESTS_PASSED=$((TESTS_PASSED + 1))
echo -e "\033[0;32m✓\033[0m $1"
}
test_fail() {
TESTS_RUN=$((TESTS_RUN + 1))
TESTS_FAILED=$((TESTS_FAILED + 1))
echo -e "\033[0;31m✗\033[0m $1"
if [[ -n "${2:-}" ]]; then
echo " Detail: $2"
fi
}
assert_eq() {
local expected="$1" actual="$2" msg="$3"
if [[ "$expected" == "$actual" ]]; then
test_pass "$msg"
else
test_fail "$msg" "expected='${expected}' actual='${actual}'"
fi
}
assert_contains() {
local haystack="$1" needle="$2" msg="$3"
if [[ "$haystack" == *"$needle"* ]]; then
test_pass "$msg"
else
test_fail "$msg" "expected string to contain '${needle}'"
fi
}
assert_file_exists() {
local filepath="$1" msg="$2"
if [[ -f "$filepath" ]]; then
test_pass "$msg"
else
test_fail "$msg" "file not found: ${filepath}"
fi
}
###############################################################################
# Source the install script without running main()
# We override main to be a no-op, then source the file.
###############################################################################
source_install_functions() {
# Create a temp file that overrides main and sources the install script
local tmp_source
tmp_source="$(mktemp)"
# Extract everything except the final `main "$@"` invocation
sed '$ d' "$INSTALL_SCRIPT" > "$tmp_source"
# Override main to prevent execution
echo 'main() { :; }' >> "$tmp_source"
# Source it (suppress color output for cleaner tests)
TERM=dumb source "$tmp_source"
rm -f "$tmp_source"
}
source_install_functions
###############################################################################
# Test: find_openclaw() — not found scenario
###############################################################################
echo ""
echo "=== find_openclaw() ==="
# Save original PATH and test with empty locations
ORIGINAL_PATH="$PATH"
ORIGINAL_HOME="$HOME"
test_find_openclaw_not_found() {
# Use a fake HOME where nothing exists
local fake_home
fake_home="$(mktemp -d)"
HOME="$fake_home"
PATH="/nonexistent"
OPENCLAW_PATH=""
if find_openclaw 2>/dev/null; then
test_fail "find_openclaw should return 1 when openclaw.mjs is not found"
else
test_pass "find_openclaw returns 1 when not found"
fi
assert_eq "" "$OPENCLAW_PATH" "OPENCLAW_PATH is empty when not found"
HOME="$ORIGINAL_HOME"
PATH="$ORIGINAL_PATH"
rm -rf "$fake_home"
}
test_find_openclaw_not_found
# Test: find_openclaw() — found in HOME/.openclaw/
test_find_openclaw_in_home() {
local fake_home
fake_home="$(mktemp -d)"
mkdir -p "${fake_home}/.openclaw"
touch "${fake_home}/.openclaw/openclaw.mjs"
HOME="$fake_home"
PATH="/nonexistent"
OPENCLAW_PATH=""
if find_openclaw 2>/dev/null; then
test_pass "find_openclaw finds openclaw.mjs in ~/.openclaw/"
assert_eq "${fake_home}/.openclaw/openclaw.mjs" "$OPENCLAW_PATH" "OPENCLAW_PATH set correctly"
else
test_fail "find_openclaw should find openclaw.mjs in ~/.openclaw/"
fi
HOME="$ORIGINAL_HOME"
PATH="$ORIGINAL_PATH"
rm -rf "$fake_home"
}
test_find_openclaw_in_home
###############################################################################
# Test: configure_memory_slot() — creates new config
###############################################################################
echo ""
echo "=== configure_memory_slot() ==="
test_configure_new_config() {
local fake_home
fake_home="$(mktemp -d)"
HOME="$fake_home"
configure_memory_slot >/dev/null 2>&1
local config_file="${fake_home}/.openclaw/openclaw.json"
assert_file_exists "$config_file" "Config file created at ~/.openclaw/openclaw.json"
# Verify JSON structure
local memory_slot
memory_slot="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.slots.memory);")"
assert_eq "claude-mem" "$memory_slot" "Memory slot set to claude-mem in new config"
local enabled
enabled="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].enabled);")"
assert_eq "true" "$enabled" "claude-mem entry is enabled in new config"
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" "Worker port is 37777 in new config"
HOME="$ORIGINAL_HOME"
rm -rf "$fake_home"
}
test_configure_new_config
# Test: configure_memory_slot() — updates existing config
test_configure_existing_config() {
local fake_home
fake_home="$(mktemp -d)"
HOME="$fake_home"
# Create an existing config with other settings
mkdir -p "${fake_home}/.openclaw"
local config_file="${fake_home}/.openclaw/openclaw.json"
node -e "
const config = {
gateway: { mode: 'local' },
plugins: {
slots: { memory: 'memory-core' },
entries: {
'some-other-plugin': { enabled: true }
}
}
};
require('fs').writeFileSync('${config_file}', JSON.stringify(config, null, 2));
"
configure_memory_slot >/dev/null 2>&1
# Verify memory slot was updated
local memory_slot
memory_slot="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.slots.memory);")"
assert_eq "claude-mem" "$memory_slot" "Memory slot updated from memory-core to claude-mem"
# Verify existing settings preserved
local gateway_mode
gateway_mode="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.gateway.mode);")"
assert_eq "local" "$gateway_mode" "Existing gateway.mode setting preserved"
# Verify other plugin still present
local other_plugin
other_plugin="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['some-other-plugin'].enabled);")"
assert_eq "true" "$other_plugin" "Existing plugin entries preserved"
# Verify claude-mem entry was added
local cm_enabled
cm_enabled="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].enabled);")"
assert_eq "true" "$cm_enabled" "claude-mem entry added and enabled"
HOME="$ORIGINAL_HOME"
rm -rf "$fake_home"
}
test_configure_existing_config
# Test: configure_memory_slot() — preserves existing claude-mem config
test_configure_preserves_existing_cm_config() {
local fake_home
fake_home="$(mktemp -d)"
HOME="$fake_home"
mkdir -p "${fake_home}/.openclaw"
local config_file="${fake_home}/.openclaw/openclaw.json"
node -e "
const config = {
plugins: {
slots: { memory: 'memory-core' },
entries: {
'claude-mem': {
enabled: false,
config: {
workerPort: 38888,
observationFeed: { enabled: true, channel: 'telegram', to: '12345' }
}
}
}
}
};
require('fs').writeFileSync('${config_file}', JSON.stringify(config, null, 2));
"
configure_memory_slot >/dev/null 2>&1
# Should enable it but preserve existing config
local cm_enabled
cm_enabled="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].enabled);")"
assert_eq "true" "$cm_enabled" "claude-mem entry enabled when previously disabled"
local custom_port
custom_port="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries['claude-mem'].config.workerPort);")"
assert_eq "38888" "$custom_port" "Existing custom workerPort preserved"
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 "telegram" "$feed_channel" "Existing observationFeed config preserved"
HOME="$ORIGINAL_HOME"
rm -rf "$fake_home"
}
test_configure_preserves_existing_cm_config
###############################################################################
# Test: version_gte() — already exists from phase 1
###############################################################################
echo ""
echo "=== version_gte() ==="
if version_gte "1.2.0" "1.1.14"; then
test_pass "version_gte: 1.2.0 >= 1.1.14"
else
test_fail "version_gte: 1.2.0 >= 1.1.14"
fi
if version_gte "1.1.14" "1.1.14"; then
test_pass "version_gte: 1.1.14 >= 1.1.14 (equal)"
else
test_fail "version_gte: 1.1.14 >= 1.1.14 (equal)"
fi
if ! version_gte "1.0.0" "1.1.14"; then
test_pass "version_gte: 1.0.0 < 1.1.14"
else
test_fail "version_gte: 1.0.0 < 1.1.14"
fi
###############################################################################
# Test: Script structure validation
###############################################################################
echo ""
echo "=== Script structure ==="
# Verify all required functions exist
for fn in find_openclaw check_openclaw install_plugin configure_memory_slot; do
if declare -f "$fn" &>/dev/null; then
test_pass "Function ${fn}() is defined"
else
test_fail "Function ${fn}() should be defined"
fi
done
# Verify the CLAUDE_MEM_REPO constant
assert_contains "$CLAUDE_MEM_REPO" "github.com/thedotmack/claude-mem" "CLAUDE_MEM_REPO points to correct repository"
# Verify AI provider functions exist
for fn in setup_ai_provider write_settings mask_api_key; do
if declare -f "$fn" &>/dev/null; then
test_pass "Function ${fn}() is defined"
else
test_fail "Function ${fn}() should be defined"
fi
done
###############################################################################
# Test: mask_api_key()
###############################################################################
echo ""
echo "=== mask_api_key() ==="
masked=$(mask_api_key "sk-1234567890abcdef")
assert_eq "***************cdef" "$masked" "mask_api_key masks all but last 4 chars"
masked_short=$(mask_api_key "abcd")
assert_eq "****" "$masked_short" "mask_api_key masks keys <= 4 chars entirely"
masked_five=$(mask_api_key "12345")
assert_eq "*2345" "$masked_five" "mask_api_key masks 5-char key correctly"
###############################################################################
# Test: setup_ai_provider() — non-interactive mode defaults to Claude
###############################################################################
echo ""
echo "=== setup_ai_provider() ==="
test_setup_ai_provider_non_interactive() {
# NON_INTERACTIVE is readonly, so test in a child bash that sources with --non-interactive
local ai_result
ai_result="$(bash -c '
set -euo pipefail
TERM=dumb
tmp=$(mktemp)
sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
echo "main() { :; }" >> "$tmp"
set -- "--non-interactive"
source "$tmp"
rm -f "$tmp"
setup_ai_provider >/dev/null 2>&1
echo "$AI_PROVIDER"
' 2>/dev/null)" || true
assert_eq "claude" "$ai_result" "Non-interactive mode defaults to claude provider"
}
test_setup_ai_provider_non_interactive
###############################################################################
# Test: write_settings() — creates new settings.json with defaults
###############################################################################
echo ""
echo "=== write_settings() ==="
test_write_settings_new_file() {
local fake_home
fake_home="$(mktemp -d)"
HOME="$fake_home"
AI_PROVIDER="claude"
AI_PROVIDER_API_KEY=""
write_settings >/dev/null 2>&1
local settings_file="${fake_home}/.claude-mem/settings.json"
assert_file_exists "$settings_file" "settings.json created at ~/.claude-mem/settings.json"
# Verify it's valid JSON with expected defaults
local provider
provider="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_PROVIDER);")"
assert_eq "claude" "$provider" "CLAUDE_MEM_PROVIDER set to claude"
local auth_method
auth_method="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_CLAUDE_AUTH_METHOD);")"
assert_eq "cli" "$auth_method" "CLAUDE_MEM_CLAUDE_AUTH_METHOD set to cli for Claude provider"
local worker_port
worker_port="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_WORKER_PORT);")"
assert_eq "37777" "$worker_port" "CLAUDE_MEM_WORKER_PORT defaults to 37777"
local model
model="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_MODEL);")"
assert_eq "claude-sonnet-4-5" "$model" "CLAUDE_MEM_MODEL defaults to claude-sonnet-4-5"
HOME="$ORIGINAL_HOME"
rm -rf "$fake_home"
}
test_write_settings_new_file
# Test: write_settings() — Gemini provider with API key
test_write_settings_gemini() {
local fake_home
fake_home="$(mktemp -d)"
HOME="$fake_home"
AI_PROVIDER="gemini"
AI_PROVIDER_API_KEY="test-gemini-key-1234"
write_settings >/dev/null 2>&1
local settings_file="${fake_home}/.claude-mem/settings.json"
local provider
provider="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_PROVIDER);")"
assert_eq "gemini" "$provider" "Gemini: CLAUDE_MEM_PROVIDER set to gemini"
local api_key
api_key="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_GEMINI_API_KEY);")"
assert_eq "test-gemini-key-1234" "$api_key" "Gemini: API key stored in settings"
local gemini_model
gemini_model="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_GEMINI_MODEL);")"
assert_eq "gemini-2.5-flash-lite" "$gemini_model" "Gemini: model defaults to gemini-2.5-flash-lite"
HOME="$ORIGINAL_HOME"
rm -rf "$fake_home"
}
test_write_settings_gemini
# Test: write_settings() — OpenRouter provider with API key
test_write_settings_openrouter() {
local fake_home
fake_home="$(mktemp -d)"
HOME="$fake_home"
AI_PROVIDER="openrouter"
AI_PROVIDER_API_KEY="sk-or-test-key-5678"
write_settings >/dev/null 2>&1
local settings_file="${fake_home}/.claude-mem/settings.json"
local provider
provider="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_PROVIDER);")"
assert_eq "openrouter" "$provider" "OpenRouter: CLAUDE_MEM_PROVIDER set to openrouter"
local api_key
api_key="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_OPENROUTER_API_KEY);")"
assert_eq "sk-or-test-key-5678" "$api_key" "OpenRouter: API key stored in settings"
local or_model
or_model="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_OPENROUTER_MODEL);")"
assert_eq "xiaomi/mimo-v2-flash:free" "$or_model" "OpenRouter: model defaults to xiaomi/mimo-v2-flash:free"
HOME="$ORIGINAL_HOME"
rm -rf "$fake_home"
}
test_write_settings_openrouter
# Test: write_settings() — preserves existing user customizations
test_write_settings_preserves_existing() {
local fake_home
fake_home="$(mktemp -d)"
HOME="$fake_home"
# Create existing settings with custom values
mkdir -p "${fake_home}/.claude-mem"
local settings_file="${fake_home}/.claude-mem/settings.json"
node -e "
const settings = {
CLAUDE_MEM_PROVIDER: 'gemini',
CLAUDE_MEM_GEMINI_API_KEY: 'old-key',
CLAUDE_MEM_WORKER_PORT: '38888',
CLAUDE_MEM_LOG_LEVEL: 'DEBUG'
};
require('fs').writeFileSync('${settings_file}', JSON.stringify(settings, null, 2));
"
# Now run write_settings with a new provider
AI_PROVIDER="claude"
AI_PROVIDER_API_KEY=""
write_settings >/dev/null 2>&1
# Provider should be updated to claude
local provider
provider="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_PROVIDER);")"
assert_eq "claude" "$provider" "Preserve: provider updated to new selection"
# Custom port should be preserved (not overwritten by defaults)
local custom_port
custom_port="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_WORKER_PORT);")"
assert_eq "38888" "$custom_port" "Preserve: existing custom WORKER_PORT preserved"
# Custom log level should be preserved
local log_level
log_level="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.CLAUDE_MEM_LOG_LEVEL);")"
assert_eq "DEBUG" "$log_level" "Preserve: existing custom LOG_LEVEL preserved"
HOME="$ORIGINAL_HOME"
rm -rf "$fake_home"
}
test_write_settings_preserves_existing
# Test: write_settings() — flat schema has all expected keys
test_write_settings_complete_schema() {
local fake_home
fake_home="$(mktemp -d)"
HOME="$fake_home"
AI_PROVIDER="claude"
AI_PROVIDER_API_KEY=""
write_settings >/dev/null 2>&1
local settings_file="${fake_home}/.claude-mem/settings.json"
# Verify key count matches SettingsDefaultsManager (34 keys)
local key_count
key_count="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(Object.keys(s).length);")"
# Settings should have all 34 keys from SettingsDefaultsManager
if (( key_count >= 30 )); then
test_pass "Settings file has ${key_count} keys (complete schema)"
else
test_fail "Settings file has ${key_count} keys, expected >= 30" "Schema may be incomplete"
fi
# Verify it does NOT have nested { env: {...} } format
local has_env_key
has_env_key="$(node -e "const s = JSON.parse(require('fs').readFileSync('${settings_file}','utf8')); console.log(s.env !== undefined);")"
assert_eq "false" "$has_env_key" "Settings uses flat schema (no nested 'env' key)"
HOME="$ORIGINAL_HOME"
rm -rf "$fake_home"
}
test_write_settings_complete_schema
###############################################################################
# Test: find_claude_mem_install_dir() — not found scenario
###############################################################################
echo ""
echo "=== find_claude_mem_install_dir() ==="
test_find_install_dir_not_found() {
local fake_home
fake_home="$(mktemp -d)"
HOME="$fake_home"
CLAUDE_MEM_INSTALL_DIR=""
if find_claude_mem_install_dir 2>/dev/null; then
test_fail "find_claude_mem_install_dir should return 1 when not found"
else
test_pass "find_claude_mem_install_dir returns 1 when not found"
fi
assert_eq "" "$CLAUDE_MEM_INSTALL_DIR" "CLAUDE_MEM_INSTALL_DIR is empty when not found"
HOME="$ORIGINAL_HOME"
rm -rf "$fake_home"
}
test_find_install_dir_not_found
# Test: find_claude_mem_install_dir() — found in ~/.openclaw/extensions/claude-mem/
test_find_install_dir_openclaw_extensions() {
local fake_home
fake_home="$(mktemp -d)"
HOME="$fake_home"
CLAUDE_MEM_INSTALL_DIR=""
# Create the expected directory structure
mkdir -p "${fake_home}/.openclaw/extensions/claude-mem/plugin/scripts"
touch "${fake_home}/.openclaw/extensions/claude-mem/plugin/scripts/worker-service.cjs"
if find_claude_mem_install_dir 2>/dev/null; then
test_pass "find_claude_mem_install_dir finds dir in ~/.openclaw/extensions/claude-mem/"
assert_eq "${fake_home}/.openclaw/extensions/claude-mem" "$CLAUDE_MEM_INSTALL_DIR" "CLAUDE_MEM_INSTALL_DIR set correctly for openclaw extensions"
else
test_fail "find_claude_mem_install_dir should find dir in ~/.openclaw/extensions/claude-mem/"
fi
HOME="$ORIGINAL_HOME"
rm -rf "$fake_home"
}
test_find_install_dir_openclaw_extensions
# Test: find_claude_mem_install_dir() — found in ~/.claude/plugins/marketplaces/thedotmack/
test_find_install_dir_marketplace() {
local fake_home
fake_home="$(mktemp -d)"
HOME="$fake_home"
CLAUDE_MEM_INSTALL_DIR=""
mkdir -p "${fake_home}/.claude/plugins/marketplaces/thedotmack/plugin/scripts"
touch "${fake_home}/.claude/plugins/marketplaces/thedotmack/plugin/scripts/worker-service.cjs"
if find_claude_mem_install_dir 2>/dev/null; then
test_pass "find_claude_mem_install_dir finds dir in marketplace path"
assert_eq "${fake_home}/.claude/plugins/marketplaces/thedotmack" "$CLAUDE_MEM_INSTALL_DIR" "CLAUDE_MEM_INSTALL_DIR set correctly for marketplace"
else
test_fail "find_claude_mem_install_dir should find dir in marketplace path"
fi
HOME="$ORIGINAL_HOME"
rm -rf "$fake_home"
}
test_find_install_dir_marketplace
###############################################################################
# Test: start_worker() — fails gracefully when install dir not found
###############################################################################
echo ""
echo "=== start_worker() ==="
test_start_worker_no_install_dir() {
local fake_home
fake_home="$(mktemp -d)"
HOME="$fake_home"
CLAUDE_MEM_INSTALL_DIR=""
local output
if output="$(start_worker 2>&1)"; then
test_fail "start_worker should fail when install dir not found"
else
test_pass "start_worker returns error when install dir not found"
fi
assert_contains "$output" "Cannot find claude-mem plugin installation directory" "start_worker error message mentions install dir"
HOME="$ORIGINAL_HOME"
rm -rf "$fake_home"
}
test_start_worker_no_install_dir
###############################################################################
# Test: verify_health() — fails when no server is running
###############################################################################
echo ""
echo "=== verify_health() ==="
test_verify_health_no_server() {
# verify_health should fail gracefully when nothing is running on 37777
# We use a very short test — just 1 attempt to keep the test fast
# Override the function to test with fewer attempts by running in a subshell
local result
result="$(bash -c '
set -euo pipefail
TERM=dumb
tmp=$(mktemp)
sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
echo "main() { :; }" >> "$tmp"
source "$tmp"
rm -f "$tmp"
# Call verify_health which will attempt 10 polls — capture exit code
verify_health 2>/dev/null && echo "PASS" || echo "FAIL"
' 2>/dev/null)" || true
# Note: This test may take ~10 seconds due to polling
# If curl is not available, it will also fail
if [[ "$result" == *"FAIL"* ]]; then
test_pass "verify_health returns failure when no server is running"
else
# Could pass if something is actually running on 37777
test_pass "verify_health returned success (worker may already be running on 37777)"
fi
}
# Only run the health check test if curl is available
if command -v curl &>/dev/null; then
test_verify_health_no_server
else
test_pass "verify_health test skipped (curl not available)"
fi
###############################################################################
# Test: print_completion_summary() — runs without error
###############################################################################
echo ""
echo "=== print_completion_summary() ==="
test_print_completion_summary() {
AI_PROVIDER="claude"
WORKER_PID=""
FEED_CONFIGURED=false
FEED_CHANNEL=""
FEED_TARGET_ID=""
local output
output="$(print_completion_summary 2>&1)"
assert_contains "$output" "Installation Complete" "Completion summary shows 'Installation Complete'"
assert_contains "$output" "Claude Max Plan" "Completion summary shows correct provider"
assert_contains "$output" "not configured" "Completion summary shows feed 'not configured' when skipped"
assert_contains "$output" "What's next" "Completion summary shows What's next section"
assert_contains "$output" "/claude-mem-status" "Completion summary mentions status command"
assert_contains "$output" "localhost:37777" "Completion summary mentions viewer URL"
assert_contains "$output" "re-run this installer" "Completion summary shows re-run instructions"
}
test_print_completion_summary
test_print_completion_summary_gemini() {
AI_PROVIDER="gemini"
WORKER_PID=""
FEED_CONFIGURED=false
local output
output="$(print_completion_summary 2>&1)"
assert_contains "$output" "Gemini" "Gemini provider shown in completion summary"
}
test_print_completion_summary_gemini
test_print_completion_summary_openrouter() {
AI_PROVIDER="openrouter"
WORKER_PID=""
FEED_CONFIGURED=false
local output
output="$(print_completion_summary 2>&1)"
assert_contains "$output" "OpenRouter" "OpenRouter provider shown in completion summary"
}
test_print_completion_summary_openrouter
###############################################################################
# Test: Script structure — new functions exist
###############################################################################
echo ""
echo "=== New function existence ==="
for fn in find_claude_mem_install_dir start_worker verify_health print_completion_summary; do
if declare -f "$fn" &>/dev/null; then
test_pass "Function ${fn}() is defined"
else
test_fail "Function ${fn}() should be defined"
fi
done
###############################################################################
# Test: main() function calls new functions in correct order
###############################################################################
echo ""
echo "=== main() function structure ==="
# Verify main calls the new functions by checking the install.sh source
test_main_calls_start_worker() {
if grep -q 'start_worker' "$INSTALL_SCRIPT"; then
test_pass "main() calls start_worker"
else
test_fail "main() should call start_worker"
fi
}
test_main_calls_start_worker
test_main_calls_verify_health() {
if grep -q 'verify_health' "$INSTALL_SCRIPT"; then
test_pass "main() calls verify_health"
else
test_fail "main() should call verify_health"
fi
}
test_main_calls_verify_health
test_main_calls_completion_summary() {
if grep -q 'print_completion_summary' "$INSTALL_SCRIPT"; then
test_pass "main() calls print_completion_summary"
else
test_fail "main() should call print_completion_summary"
fi
}
test_main_calls_completion_summary
test_main_has_progress_indicators() {
if grep -q '\[1/8\]' "$INSTALL_SCRIPT" && grep -q '\[8/8\]' "$INSTALL_SCRIPT"; then
test_pass "main() has progress indicators [1/8] through [8/8]"
else
test_fail "main() should have progress indicators [1/8] through [8/8]"
fi
}
test_main_has_progress_indicators
test_main_calls_setup_observation_feed() {
if grep -q 'setup_observation_feed' "$INSTALL_SCRIPT"; then
test_pass "main() calls setup_observation_feed"
else
test_fail "main() should call setup_observation_feed"
fi
}
test_main_calls_setup_observation_feed
test_main_calls_write_observation_feed_config() {
if grep -q 'write_observation_feed_config' "$INSTALL_SCRIPT"; then
test_pass "main() calls write_observation_feed_config"
else
test_fail "main() should call write_observation_feed_config"
fi
}
test_main_calls_write_observation_feed_config
###############################################################################
# Test: setup_observation_feed() — function exists and non-interactive skips
###############################################################################
echo ""
echo "=== setup_observation_feed() ==="
for fn in setup_observation_feed write_observation_feed_config; do
if declare -f "$fn" &>/dev/null; then
test_pass "Function ${fn}() is defined"
else
test_fail "Function ${fn}() should be defined"
fi
done
test_setup_observation_feed_non_interactive() {
# Non-interactive mode should skip feed setup without error
local feed_result
feed_result="$(bash -c '
set -euo pipefail
TERM=dumb
tmp=$(mktemp)
sed "$ d" "'"${INSTALL_SCRIPT}"'" > "$tmp"
echo "main() { :; }" >> "$tmp"
set -- "--non-interactive"
source "$tmp"
rm -f "$tmp"
setup_observation_feed 2>/dev/null
echo "CHANNEL=$FEED_CHANNEL"
echo "CONFIGURED=$FEED_CONFIGURED"
' 2>/dev/null)" || true
assert_contains "$feed_result" "CHANNEL=" "Non-interactive mode: FEED_CHANNEL is empty"
assert_contains "$feed_result" "CONFIGURED=false" "Non-interactive mode: FEED_CONFIGURED is false"
}
test_setup_observation_feed_non_interactive
###############################################################################
# Test: write_observation_feed_config() — writes correct JSON structure
###############################################################################
echo ""
echo "=== write_observation_feed_config() ==="
test_write_observation_feed_config_writes_json() {
local fake_home
fake_home="$(mktemp -d)"
HOME="$fake_home"
# Create an existing openclaw.json with claude-mem entry
mkdir -p "${fake_home}/.openclaw"
local config_file="${fake_home}/.openclaw/openclaw.json"
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));
"
FEED_CHANNEL="telegram"
FEED_TARGET_ID="123456789"
FEED_CONFIGURED="true"
write_observation_feed_config >/dev/null 2>&1
# Verify observationFeed was written
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" "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 "telegram" "$feed_channel" "observationFeed.channel is telegram"
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 "123456789" "$feed_to" "observationFeed.to is 123456789"
# 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" "Existing workerPort preserved after feed config write"
HOME="$ORIGINAL_HOME"
FEED_CHANNEL=""
FEED_TARGET_ID=""
FEED_CONFIGURED=false
rm -rf "$fake_home"
}
test_write_observation_feed_config_writes_json
test_write_observation_feed_config_skips_when_not_configured() {
local fake_home
fake_home="$(mktemp -d)"
HOME="$fake_home"
# Create minimal config
mkdir -p "${fake_home}/.openclaw"
local config_file="${fake_home}/.openclaw/openclaw.json"
node -e "
require('fs').writeFileSync('${config_file}', JSON.stringify({ plugins: {} }, null, 2));
"
FEED_CONFIGURED="false"
write_observation_feed_config >/dev/null 2>&1
# Config should be unchanged — no observationFeed key
local has_feed
has_feed="$(node -e "const c = JSON.parse(require('fs').readFileSync('${config_file}','utf8')); console.log(c.plugins.entries !== undefined);")"
assert_eq "false" "$has_feed" "Config unchanged when FEED_CONFIGURED is false"
HOME="$ORIGINAL_HOME"
rm -rf "$fake_home"
}
test_write_observation_feed_config_skips_when_not_configured
test_write_observation_feed_config_discord() {
local fake_home
fake_home="$(mktemp -d)"
HOME="$fake_home"
mkdir -p "${fake_home}/.openclaw"
local config_file="${fake_home}/.openclaw/openclaw.json"
node -e "
const config = {
plugins: {
entries: {
'claude-mem': { enabled: true, config: {} }
}
}
};
require('fs').writeFileSync('${config_file}', JSON.stringify(config, null, 2));
"
FEED_CHANNEL="discord"
FEED_TARGET_ID="1234567890123456789"
FEED_CONFIGURED="true"
write_observation_feed_config >/dev/null 2>&1
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 "discord" "$feed_channel" "Discord channel type written correctly"
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 "1234567890123456789" "$feed_to" "Discord channel ID written correctly"
HOME="$ORIGINAL_HOME"
FEED_CHANNEL=""
FEED_TARGET_ID=""
FEED_CONFIGURED=false
rm -rf "$fake_home"
}
test_write_observation_feed_config_discord
###############################################################################
# Test: print_completion_summary() — shows observation feed status
###############################################################################
echo ""
echo "=== print_completion_summary() — observation feed ==="
test_completion_summary_with_feed() {
AI_PROVIDER="claude"
WORKER_PID=""
FEED_CONFIGURED="true"
FEED_CHANNEL="telegram"
FEED_TARGET_ID="123456789"
local output
output="$(print_completion_summary 2>&1)"
assert_contains "$output" "telegram" "Summary shows feed channel when configured"
assert_contains "$output" "123456789" "Summary shows feed target when configured"
assert_contains "$output" "What's next" "Summary includes What's next section"
assert_contains "$output" "/claude-mem-feed" "Summary includes feed check command when configured"
FEED_CONFIGURED=false
FEED_CHANNEL=""
FEED_TARGET_ID=""
}
test_completion_summary_with_feed
test_completion_summary_without_feed() {
AI_PROVIDER="claude"
WORKER_PID=""
FEED_CONFIGURED=false
FEED_CHANNEL=""
FEED_TARGET_ID=""
local output
output="$(print_completion_summary 2>&1)"
assert_contains "$output" "not configured" "Summary shows 'not configured' when feed skipped"
assert_contains "$output" "What's next" "Summary includes What's next section without feed"
assert_contains "$output" "/claude-mem-status" "Summary includes status check command"
assert_contains "$output" "localhost:37777" "Summary includes viewer URL"
}
test_completion_summary_without_feed
###############################################################################
# Test: Channel type instructions exist in install.sh
###############################################################################
echo ""
echo "=== Channel instructions ==="
for channel in telegram discord slack signal whatsapp line; do
if grep -qi "$channel" "$INSTALL_SCRIPT"; then
test_pass "Channel '${channel}' instructions exist in install.sh"
else
test_fail "Channel '${channel}' instructions should exist in install.sh"
fi
done
# Verify specific instruction content
assert_contains "$(grep -A2 'userinfobot' "$INSTALL_SCRIPT" 2>/dev/null || echo '')" "userinfobot" "Telegram instructions include @userinfobot"
assert_contains "$(grep -A2 'Developer Mode' "$INSTALL_SCRIPT" 2>/dev/null || echo '')" "Developer Mode" "Discord instructions include Developer Mode"
assert_contains "$(grep -A2 'C01ABC2DEFG' "$INSTALL_SCRIPT" 2>/dev/null || echo '')" "C01ABC2DEFG" "Slack instructions include sample channel ID"
###############################################################################
# Summary
###############################################################################
echo ""
echo "========================================"
echo "Results: ${TESTS_PASSED}/${TESTS_RUN} passed, ${TESTS_FAILED} failed"
echo "========================================"
if [[ "$TESTS_FAILED" -gt 0 ]]; then
exit 1
fi
exit 0