|
| 1 | +#!/usr/bin/env bash |
| 2 | +# test_env_bridge.sh — Generic LPB_ → bare-name bridge tests. |
| 3 | +# |
| 4 | +# Tests the MECHANISM, not individual variable names. |
| 5 | +# The single source of truth is the BARE_NAMES array in start.sh. |
| 6 | +# |
| 7 | +# Validates: |
| 8 | +# 1. Bridge function: LPB_ → bare-name with correct priority |
| 9 | +# 2. BARE_NAMES array: syntax, completeness (mcp.json coverage) |
| 10 | +# 3. persist_devstack_env() uses BARE_NAMES (not hardcoded string) |
| 11 | +# 4. No hardcoded API keys in .env.example files |
| 12 | +# 5. mcp.json env vars use ${VAR} references (structural) |
| 13 | +# 6. .env.example files document the bridge |
| 14 | +# 7. Full priority chain: shell > .env(LPB_) > conf > hardcoded |
| 15 | + |
| 16 | +set -euo pipefail |
| 17 | + |
| 18 | +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" |
| 19 | +DEVSTACK_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" |
| 20 | +AGENT_DIR="$HOME/.pi/agent" |
| 21 | +SUPPORT_SCRIPT="$DEVSTACK_DIR/support/start.sh" |
| 22 | + |
| 23 | +PASS=0 |
| 24 | +FAIL=0 |
| 25 | + |
| 26 | +pass() { echo " PASS: $1"; PASS=$((PASS + 1)); } |
| 27 | +fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); } |
| 28 | + |
| 29 | +# ─── Source start.sh bridge logic in a clean subshell ──────────────────────── |
| 30 | +_source_bridge() { |
| 31 | + local bare_names_block bridge_block lib_func load_env_func |
| 32 | + bare_names_block=$(sed -n '/^BARE_NAMES=(/,/^)$/p' "$SUPPORT_SCRIPT") |
| 33 | + bridge_block=$(sed -n '/^_bridge() {/,/^}/p' "$SUPPORT_SCRIPT") |
| 34 | + lib_func=$(sed -n '/^parse_env_file() {/,/^}/p' "$DEVSTACK_DIR/support/_lib.sh") |
| 35 | + load_env_func=' |
| 36 | +_load_env_into_vars() { |
| 37 | + local file="$1" |
| 38 | + while IFS= read -r line; do |
| 39 | + [[ "$line" =~ ^LPB_ ]] || continue |
| 40 | + [[ -z "$line" ]] && continue |
| 41 | + local key="${line%%=*}" val="${line#*=}" |
| 42 | + val="${val#\"}"; val="${val%\"}" |
| 43 | + export "$key=$val" |
| 44 | + done < "$file" |
| 45 | + } |
| 46 | +' |
| 47 | + |
| 48 | + bash -c " |
| 49 | + $lib_func |
| 50 | + $load_env_func |
| 51 | + $bare_names_block |
| 52 | + $bridge_block |
| 53 | + $1 |
| 54 | + " |
| 55 | +} |
| 56 | + |
| 57 | +# Bridge-only: like _source_bridge but skips the BARE_NAMES export step. |
| 58 | +# Used for priority tests where we control the initial LPB_ state. |
| 59 | +_bridge_only() { |
| 60 | + local bridge_block lib_func |
| 61 | + bridge_block=$(sed -n '/^_bridge() {/,/^}/p' "$SUPPORT_SCRIPT") |
| 62 | + lib_func=$(sed -n '/^parse_env_file() {/,/^}/p' "$DEVSTACK_DIR/support/_lib.sh") |
| 63 | + local load_env_func=' |
| 64 | +_load_env_into_vars() { |
| 65 | + local file="$1" |
| 66 | + while IFS= read -r line; do |
| 67 | + [[ "$line" =~ ^LPB_ ]] || continue |
| 68 | + [[ -z "$line" ]] && continue |
| 69 | + local key="${line%%=*}" val="${line#*=}" |
| 70 | + val="${val#\"}"; val="${val%\"}" |
| 71 | + export "$key=$val" |
| 72 | + done < "$file" |
| 73 | + } |
| 74 | +' |
| 75 | + local bare_inline='BARE_NAMES=(EXA_API_KEY CONTEXT7_API_KEY LEMONADE_BASE_URL OPENROUTER_BASE_URL ED_PORT HOST CONNECTION_TOKEN DEVCONTAINER_WORKSPACE_DIR MAX_TOKENS_CONTEXT_RATIO)' |
| 76 | + bash -c " |
| 77 | + $lib_func |
| 78 | + $load_env_func |
| 79 | + $bare_inline |
| 80 | + $bridge_block |
| 81 | + $1 |
| 82 | + " |
| 83 | +} |
| 84 | + |
| 85 | +# ─── 1. Bridge Mechanism ───────────────────────────────────────────────────── |
| 86 | + |
| 87 | +echo "=== 1. LPB_ → Bare Name Bridge Mechanism ===" |
| 88 | + |
| 89 | +# 1a. Generic test: pick EXA_API_KEY, set LPB_FOO, verify FOO |
| 90 | +r=$(_source_bridge ' |
| 91 | + export "LPB_EXA_API_KEY=gen-test-value" |
| 92 | + _bridge |
| 93 | + echo "${EXA_API_KEY:-UNSET}" |
| 94 | +') |
| 95 | +[[ "$r" == "gen-test-value" ]] && pass "Generic: LPB_ bridges to bare" || fail "Generic: LPB_ bridge, got '$r'" |
| 96 | + |
| 97 | +# 1b. Shell env priority: set bare + LPB_, verify bare wins |
| 98 | +r=$(_source_bridge ' |
| 99 | + export "CONTEXT7_API_KEY=shell-wins" |
| 100 | + export "LPB_CONTEXT7_API_KEY=lpb-loses" |
| 101 | + _bridge |
| 102 | + echo "${CONTEXT7_API_KEY}" |
| 103 | +') |
| 104 | +[[ "$r" == "shell-wins" ]] && pass "Generic: shell env > LPB_" || fail "Generic: shell priority, got '$r'" |
| 105 | + |
| 106 | +# 1c. LPB_ value used when bare not set |
| 107 | +r=$(_source_bridge ' |
| 108 | + export "LPB_EXA_API_KEY=lpb-solo" |
| 109 | + _bridge |
| 110 | + echo "${EXA_API_KEY:-UNSET}" |
| 111 | +') |
| 112 | +[[ "$r" == "lpb-solo" ]] && pass "Generic: LPB_ fills when bare unset" || fail "Generic: LPB_ fill, got '$r'" |
| 113 | + |
| 114 | +# ─── 2. BARE_NAMES Array ──────────────────────────────────────────────────── |
| 115 | + |
| 116 | +echo "" |
| 117 | +echo "=== 2. BARE_NAMES Array ===" |
| 118 | + |
| 119 | +count=$(grep -c '^ [A-Z_][A-Z_0-9]*$' "$SUPPORT_SCRIPT" | head -1) |
| 120 | +if [[ "$count" -gt 0 ]]; then |
| 121 | + pass "BARE_NAMES: $count entries defined" |
| 122 | +else |
| 123 | + fail "BARE_NAMES: no entries found" |
| 124 | +fi |
| 125 | + |
| 126 | +# All API key names in mcp.json should be in BARE_NAMES |
| 127 | +mcp_json="$AGENT_DIR/mcp.json" |
| 128 | +if [[ -f "$mcp_json" ]]; then |
| 129 | + all_covered=true |
| 130 | + for var in $(grep -oP '"[A-Z_]+_API_KEY"' "$mcp_json" | tr -d '"'); do |
| 131 | + if ! grep -q "^ ${var}$" "$SUPPORT_SCRIPT"; then |
| 132 | + all_covered=false |
| 133 | + break |
| 134 | + fi |
| 135 | + done |
| 136 | + $all_covered && pass "All mcp.json API keys in BARE_NAMES" || fail "Some mcp.json keys missing from BARE_NAMES" |
| 137 | +fi |
| 138 | + |
| 139 | +# ─── 3. persist_devstack_env Integration ───────────────────────────────────── |
| 140 | + |
| 141 | +echo "" |
| 142 | +echo "=== 3. persist_devstack_env Integration ===" |
| 143 | + |
| 144 | +grep -q '${BARE_NAMES\[@\]}' "$SUPPORT_SCRIPT" && pass "Uses BARE_NAMES array (not hardcoded string)" || fail "Uses hardcoded bare_names" |
| 145 | +grep -q '_bridge' "$SUPPORT_SCRIPT" && grep -c '_bridge' "$SUPPORT_SCRIPT" | grep -q '[2-9]' && pass "_bridge called multiple times (initial + after .env)" || fail "_bridge not called enough times" |
| 146 | + |
| 147 | +# ─── 4. No Hardcoded Secrets in .env.example ───────────────────────────────── |
| 148 | + |
| 149 | +echo "" |
| 150 | +echo "=== 4. No Hardcoded Secrets ===" |
| 151 | + |
| 152 | +for f in "$DEVSTACK_DIR/.env.example" "$AGENT_DIR/.env.example"; do |
| 153 | + [[ -f "$f" ]] || continue |
| 154 | + fname=$(basename "$f") |
| 155 | + has_key=false |
| 156 | + while IFS= read -r line; do |
| 157 | + [[ "$line" =~ ^# ]] && continue |
| 158 | + [[ -z "$line" ]] && continue |
| 159 | + val=$(echo "$line" | cut -d= -f2-) |
| 160 | + [[ -z "$val" ]] && continue |
| 161 | + [[ "$val" =~ ^(your-|change-|placeholder|REPLACE|sk-|ghp_|abc123) ]] && continue |
| 162 | + [[ ${#val} -gt 10 ]] && has_key=true && break |
| 163 | + done < <(grep -E '(EXA|CONTEXT7|GITHUB|OPENROUTER).*_API_KEY=|^.*_TOKEN=' "$f") |
| 164 | + $has_key && fail "Possible hardcoded secret in $fname" || pass "No hardcoded secrets in $fname" |
| 165 | +done |
| 166 | + |
| 167 | +# ─── 5. mcp.json Env Var Syntax (Structural) ───────────────────────────────── |
| 168 | + |
| 169 | +echo "" |
| 170 | +echo "=== 5. mcp.json Env Var Syntax ===" |
| 171 | + |
| 172 | +if [[ -f "$mcp_json" ]]; then |
| 173 | + bad_refs=0 |
| 174 | + while IFS= read -r val; do |
| 175 | + [[ -z "$val" ]] && continue |
| 176 | + [[ "$val" =~ ^\$\{ ]] && continue |
| 177 | + [[ ${#val} -gt 2 ]] && bad_refs=$((bad_refs + 1)) |
| 178 | + done < <(grep -oP '"env"\s*:\s*\{[^}]*\}' "$mcp_json" | grep -oP '"[A-Z_]+_?\w*"\s*:\s*"(.*?)"' | cut -d'"' -f4) |
| 179 | + [[ "$bad_refs" -eq 0 ]] && pass "All mcp.json env values use \${VAR} references" || fail "$bad_refs non-reference values" |
| 180 | +else |
| 181 | + pass "mcp.json not found — skipping" |
| 182 | +fi |
| 183 | + |
| 184 | +# ─── 6. .env.example Consistency ───────────────────────────────────────────── |
| 185 | + |
| 186 | +echo "" |
| 187 | +echo "=== 6. Consistency ===" |
| 188 | + |
| 189 | +for f in "$DEVSTACK_DIR/.env.example" "$AGENT_DIR/.env.example"; do |
| 190 | + [[ -f "$f" && -s "$f" ]] && pass ".env.example exists: $(basename "$f")" || fail ".env.example missing/empty: $(basename "$f")" |
| 191 | +done |
| 192 | +grep -qi 'LPB_.*bridge\|LPB_.*bare\|LPB_.*prefix' "$AGENT_DIR/.env.example" 2>/dev/null && pass "Agent docs: LPB_→bare bridge documented" || fail "Agent docs: missing bridge note" |
| 193 | +grep -q 'LPB_' "$DEVSTACK_DIR/.env.example" && pass "Devstack: uses LPB_ prefix" || fail "Devstack: missing LPB_ prefix" |
| 194 | + |
| 195 | +# ─── 7. Full Priority Chain (End-to-End) ───────────────────────────────────── |
| 196 | + |
| 197 | +echo "" |
| 198 | +echo "=== 7. Full Priority Chain ===" |
| 199 | + |
| 200 | +# 7a. Priority: shell env > .env(LP B_) > conf defaults |
| 201 | +r=$(_bridge_only ' |
| 202 | + echo "LPB_EXA_API_KEY=conf-default" > /tmp/test_conf.env |
| 203 | + _load_env_into_vars /tmp/test_conf.env |
| 204 | + export EXA_API_KEY=shell-key |
| 205 | + echo "LPB_EXA_API_KEY=env-key" > /tmp/test_env.env |
| 206 | + _load_env_into_vars /tmp/test_env.env |
| 207 | + _bridge |
| 208 | + echo "${EXA_API_KEY:-UNSET}" |
| 209 | +') |
| 210 | +[[ "$r" == "shell-key" ]] && pass "7a: shell > .env > conf" || fail "7a: expected shell-key, got $r" |
| 211 | + |
| 212 | +# 7b. Priority: .env(LP B_) > conf defaults (no shell env) |
| 213 | +r=$(_bridge_only ' |
| 214 | + echo "LPB_EXA_API_KEY=conf-default" > /tmp/test_conf.env |
| 215 | + _load_env_into_vars /tmp/test_conf.env |
| 216 | + echo "LPB_EXA_API_KEY=env-key" > /tmp/test_env.env |
| 217 | + _load_env_into_vars /tmp/test_env.env |
| 218 | + _bridge |
| 219 | + echo "${EXA_API_KEY:-UNSET}" |
| 220 | +') |
| 221 | +[[ "$r" == "env-key" ]] && pass "7b: .env(LP B_) > conf default" || fail "7b: expected env-key, got $r" |
| 222 | + |
| 223 | +# 7c. Priority: conf defaults > nothing |
| 224 | +r=$(_bridge_only ' |
| 225 | + echo "LPB_EXA_API_KEY=conf-default" > /tmp/test_conf.env |
| 226 | + _load_env_into_vars /tmp/test_conf.env |
| 227 | + _bridge |
| 228 | + echo "${EXA_API_KEY:-UNSET}" |
| 229 | +') |
| 230 | +[[ "$r" == "conf-default" ]] && pass "7c: conf default used when nothing else" || fail "7c: expected conf-default, got $r" |
| 231 | + |
| 232 | +# 7d. Multiple LPB_ vars bridged (EXA + CONTEXT7 are the clean ones without special defaults) |
| 233 | +r=$(_bridge_only ' |
| 234 | + export "LPB_EXA_API_KEY=exa1" |
| 235 | + export "LPB_CONTEXT7_API_KEY=c71" |
| 236 | + _bridge |
| 237 | + echo "${EXA_API_KEY}:${CONTEXT7_API_KEY}" |
| 238 | +') |
| 239 | +[[ "$r" == "exa1:c71" ]] && pass "7d: multiple LPB_ vars bridged" || fail "7d: expected exa1:c71, got $r" |
| 240 | + |
| 241 | +# ─── Summary ───────────────────────────────────────────────────────────────── |
| 242 | + |
| 243 | +echo "" |
| 244 | +echo "========================================" |
| 245 | +echo "Results: $PASS passed, $FAIL failed" |
| 246 | +echo "========================================" |
| 247 | + |
| 248 | +exit $FAIL |
0 commit comments