From 633ce8eaacc2d8a248ae765d926c230921f6203e Mon Sep 17 00:00:00 2001 From: Ahmed Abushagur Date: Tue, 17 Feb 2026 22:17:08 -0800 Subject: [PATCH] feat: upgrade default server sizes, fix Fly.io agent installs, improve E2E tests (#1428) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Upgrade default VM sizes across clouds for better agent performance: - Hetzner: cpx11 → cx23 (with cx22 fallback support for deprecated types) - DigitalOcean: s-2vcpu-2gb → s-2vcpu-4gb - Daytona: 2048MB → 4096MB memory - Oracle: VM.Standard.E2.1.Micro → VM.Standard.A1.Flex - OVH: d2-2 → d2-4 - Fix Fly.io agent failures: - Add Node.js + build-essential to wait_for_cloud_init (fixes npm-based agents) - Prepend PATH in interactive_session (fixes "source not found" errors) - Fix openclaw installs across clouds: use explicit PATH export instead of source - Fix DigitalOcean token validation (check "uuid" not "id") - Fix AWS cloud-init: chown .bashrc/.zshrc to ubuntu user - Improve Hetzner fallback: add "cheapest available" as last-resort fallback - Upgrade E2E tests: per-combo auto-fix, credential collection, robustness fixes Co-authored-by: Claude Opus 4.6 --- aws/lib/common.sh | 1 + aws/openclaw.sh | 4 +- daytona/lib/common.sh | 2 +- digitalocean/lib/common.sh | 4 +- digitalocean/openclaw.sh | 4 +- fly/lib/common.sh | 13 +- fly/openclaw.sh | 4 +- hetzner/lib/common.sh | 34 ++- oracle/lib/common.sh | 2 +- ovh/lib/common.sh | 2 +- shared/common.sh | 4 +- test/e2e.sh | 267 ++++++++++++++---------- test/fixtures/hetzner/_env.sh | 2 +- test/fixtures/hetzner/datacenters.json | 2 +- test/fixtures/hetzner/server_types.json | 81 +++++++ 15 files changed, 290 insertions(+), 136 deletions(-) diff --git a/aws/lib/common.sh b/aws/lib/common.sh index 27c70ffb..14f49049 100644 --- a/aws/lib/common.sh +++ b/aws/lib/common.sh @@ -120,6 +120,7 @@ su - ubuntu -c 'curl -fsSL https://claude.ai/install.sh | bash' # Configure PATH echo 'export PATH="${HOME}/.claude/local/bin:${HOME}/.bun/bin:${PATH}"' >> /home/ubuntu/.bashrc echo 'export PATH="${HOME}/.claude/local/bin:${HOME}/.bun/bin:${PATH}"' >> /home/ubuntu/.zshrc +chown ubuntu:ubuntu /home/ubuntu/.bashrc /home/ubuntu/.zshrc touch /home/ubuntu/.cloud-init-complete chown ubuntu:ubuntu /home/ubuntu/.cloud-init-complete CLOUD_INIT_EOF diff --git a/aws/openclaw.sh b/aws/openclaw.sh index 29431f64..585175c2 100755 --- a/aws/openclaw.sh +++ b/aws/openclaw.sh @@ -16,7 +16,7 @@ echo "" AGENT_MODEL_PROMPT=1 AGENT_MODEL_DEFAULT="openrouter/auto" -agent_install() { install_agent "openclaw" "source ~/.bashrc && bun install -g openclaw" cloud_run; } +agent_install() { install_agent "openclaw" "export PATH=\$HOME/.bun/bin:\$PATH && bun install -g openclaw" cloud_run; } agent_env_vars() { generate_env_config \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ @@ -25,7 +25,7 @@ agent_env_vars() { } agent_configure() { setup_openclaw_config "${OPENROUTER_API_KEY}" "${MODEL_ID}" cloud_upload cloud_run; } agent_pre_launch() { - cloud_run "source ~/.zshrc && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 &" + cloud_run "export PATH=\$HOME/.bun/bin:\$PATH && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 &" wait_for_openclaw_gateway cloud_run } agent_launch_cmd() { echo 'source ~/.zshrc && openclaw tui'; } diff --git a/daytona/lib/common.sh b/daytona/lib/common.sh index 5ba04a8e..0100fc9d 100644 --- a/daytona/lib/common.sh +++ b/daytona/lib/common.sh @@ -100,7 +100,7 @@ _is_snapshot_conflict() { _daytona_create_with_resources() { local name="${1}" local cpu="${DAYTONA_CPU:-2}" - local memory="${DAYTONA_MEMORY:-2048}" + local memory="${DAYTONA_MEMORY:-4096}" local disk="${DAYTONA_DISK:-5}" # Validate numeric env vars to prevent command injection diff --git a/digitalocean/lib/common.sh b/digitalocean/lib/common.sh index 46ea163b..5e5c3868 100755 --- a/digitalocean/lib/common.sh +++ b/digitalocean/lib/common.sh @@ -41,7 +41,7 @@ do_api() { test_do_token() { local response response=$(do_api GET "/account") - if [[ "$response" == *'"id"'* ]]; then + if [[ "$response" == *'"uuid"'* ]]; then log_info "API token validated" return 0 else @@ -165,7 +165,7 @@ _do_check_create_error() { # Create a DigitalOcean droplet with cloud-init create_server() { local name="$1" - local size="${DO_DROPLET_SIZE:-s-2vcpu-2gb}" + local size="${DO_DROPLET_SIZE:-s-2vcpu-4gb}" local region="${DO_REGION:-nyc3}" local image="ubuntu-24-04-x64" diff --git a/digitalocean/openclaw.sh b/digitalocean/openclaw.sh index 10c04393..53055562 100755 --- a/digitalocean/openclaw.sh +++ b/digitalocean/openclaw.sh @@ -16,7 +16,7 @@ echo "" AGENT_MODEL_PROMPT=1 AGENT_MODEL_DEFAULT="openrouter/auto" -agent_install() { install_agent "openclaw" "source ~/.bashrc && bun install -g openclaw" cloud_run; } +agent_install() { install_agent "openclaw" "export PATH=\$HOME/.bun/bin:\$PATH && bun install -g openclaw" cloud_run; } agent_env_vars() { generate_env_config \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ @@ -25,7 +25,7 @@ agent_env_vars() { } agent_configure() { setup_openclaw_config "${OPENROUTER_API_KEY}" "${MODEL_ID}" cloud_upload cloud_run; } agent_pre_launch() { - cloud_run "source ~/.zshrc && nohup openclaw gateway > /tmp/openclaw-gateway.log 2>&1 &" + cloud_run "source ~/.spawnrc 2>/dev/null; export PATH=\$HOME/.bun/bin:\$PATH && nohup openclaw gateway /tmp/openclaw-gateway.log 2>&1 & disown" wait_for_openclaw_gateway cloud_run } agent_launch_cmd() { echo 'source ~/.zshrc && openclaw tui'; } diff --git a/fly/lib/common.sh b/fly/lib/common.sh index 262e2398..7adb4a78 100644 --- a/fly/lib/common.sh +++ b/fly/lib/common.sh @@ -338,9 +338,13 @@ wait_for_cloud_init() { _fly_wait_for_ssh || return 1 log_step "Installing packages (this may take 1-2 minutes)..." - run_server "apt-get update -y && apt-get install -y curl unzip git zsh python3 pip" 600 || { + run_server "apt-get update -y && apt-get install -y curl unzip git zsh python3 python3-pip build-essential" 600 || { log_warn "Package install timed out or failed, retrying..." - run_server "apt-get install -y curl unzip git zsh python3 pip" 300 || true + run_server "apt-get install -y curl unzip git zsh python3 python3-pip build-essential" 300 || true + } + log_step "Installing Node.js..." + run_server "curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && apt-get install -y nodejs" 120 || { + log_warn "Node.js install failed, npm-based agents may not work" } log_step "Installing bun..." run_server "curl -fsSL https://bun.sh/install | bash" 120 || true @@ -401,9 +405,12 @@ upload_file() { # Start an interactive SSH session on the Fly.io machine interactive_session() { local cmd="$1" + # Wrap in bash -c with PATH prepended (same as run_server) so shell builtins + # like "source" work — fly ssh console -C execs directly, not via a shell. + local full_cmd="export PATH=\"\$HOME/.local/bin:\$HOME/.bun/bin:\$PATH\" && $cmd" # SECURITY: Properly escape command to prevent injection local escaped_cmd - escaped_cmd=$(printf '%q' "$cmd") + escaped_cmd=$(printf '%q' "$full_cmd") local session_exit=0 "$(_get_fly_cmd)" ssh console -a "$FLY_APP_NAME" -C "bash -c \"$escaped_cmd\"" || session_exit=$? SERVER_NAME="${FLY_APP_NAME:-}" SPAWN_RECONNECT_CMD="fly ssh console -a ${FLY_APP_NAME:-}" \ diff --git a/fly/openclaw.sh b/fly/openclaw.sh index 83a3f307..6009054f 100644 --- a/fly/openclaw.sh +++ b/fly/openclaw.sh @@ -16,8 +16,8 @@ AGENT_MODEL_PROMPT=1 AGENT_MODEL_DEFAULT="openrouter/auto" agent_install() { - cloud_run "curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && apt-get install -y nodejs" - install_agent "openclaw" "npm install -g --ignore-scripts openclaw@latest" cloud_run + # Node.js is installed in wait_for_cloud_init; bun install -g fails on Fly + install_agent "openclaw" "npm install -g openclaw@latest" cloud_run } agent_env_vars() { diff --git a/hetzner/lib/common.sh b/hetzner/lib/common.sh index 4fb3a4c2..42fc161b 100755 --- a/hetzner/lib/common.sh +++ b/hetzner/lib/common.sh @@ -211,7 +211,7 @@ _pick_server_type() { local location="$1" # Wrap the location-specific list function for interactive_pick _list_server_types_for_current_location() { _list_server_types_for_location "$location"; } - interactive_pick "HETZNER_SERVER_TYPE" "cpx11" "server types" _list_server_types_for_current_location "cpx11" + interactive_pick "HETZNER_SERVER_TYPE" "cx23" "server types" _list_server_types_for_current_location "cx23" unset -f _list_server_types_for_current_location } @@ -253,7 +253,7 @@ _hetzner_get_available_ids() { } # Search for a compatible fallback server type when the requested one is unavailable -# Tries same CPU family first, then any family with >= specs +# Tries: 1) same CPU family with >= specs, 2) any family with >= specs, 3) cheapest available # Prints the fallback type name on success; emits FALLBACK: info on stderr _hetzner_find_fallback_type() { local server_type="$1" types_response="$2" location="$3" available_ids="$4" @@ -269,12 +269,15 @@ _hetzner_find_fallback_type() { ids_json=$(printf '%s\n' "$available_ids" | jq -Rn '[inputs | tonumber]') local family candidates replacement - for family in "same" "any"; do + for family in "same" "any" "cheapest"; do local filter if [[ "$family" == "same" ]]; then filter="select(.cpu_type == \"${wanted_cpu}\" and .cores >= ${wanted_cores} and .memory >= ${wanted_memory})" - else + elif [[ "$family" == "any" ]]; then filter="select(.cores >= ${wanted_cores} and .memory >= ${wanted_memory})" + else + # Last resort: any available non-deprecated type (cheapest) + filter="." fi candidates=$(_hetzner_find_candidates "$types_response" "$location" "$ids_json" "$filter") @@ -282,6 +285,7 @@ _hetzner_find_fallback_type() { replacement=$(printf '%s\n' "$candidates" | head -1 | cut -d'|' -f2) local label="${wanted_cpu}" [[ "$family" == "any" ]] && label="any" + [[ "$family" == "cheapest" ]] && label="cheapest" printf 'FALLBACK:%s:%s:%s:%s\n' "$server_type" "$replacement" "$location" "$label" >&2 printf '%s' "$replacement" return 0 @@ -308,22 +312,30 @@ _validate_server_type_for_location() { local types_response types_response=$(hetzner_api GET "/server_types?per_page=50") - # Check if the requested type exists and is directly available + # Check if the requested type exists (including deprecated) for spec lookup local wanted_id wanted_id=$(printf '%s' "$types_response" | jq -r \ --arg name "$server_type" \ '.server_types[] | select(.name == $name and .deprecation == null) | .id') - if [[ -z "$wanted_id" ]]; then - printf 'ERROR:unknown_type\n' >&2 - return 1 - fi - - if printf '%s\n' "$available_ids" | grep -qx "$wanted_id"; then + if [[ -n "$wanted_id" ]] && printf '%s\n' "$available_ids" | grep -qx "$wanted_id"; then printf '%s' "$server_type" return 0 fi + # Type is unavailable (not in available list or deprecated) — find a fallback. + # If the type exists but is deprecated, we can still read its specs for fallback matching. + if [[ -z "$wanted_id" ]]; then + # Check if it exists at all (even deprecated) so we can read specs for fallback + wanted_id=$(printf '%s' "$types_response" | jq -r \ + --arg name "$server_type" \ + '.server_types[] | select(.name == $name) | .id') + if [[ -z "$wanted_id" ]]; then + printf 'ERROR:unknown_type\n' >&2 + return 1 + fi + fi + _hetzner_find_fallback_type "$server_type" "$types_response" "$location" "$available_ids" } diff --git a/oracle/lib/common.sh b/oracle/lib/common.sh index 662d1cf2..a9f14f63 100644 --- a/oracle/lib/common.sh +++ b/oracle/lib/common.sh @@ -394,7 +394,7 @@ _launch_oci_instance() { create_server() { local name="${1}" - local shape="${OCI_SHAPE:-VM.Standard.E2.1.Micro}" + local shape="${OCI_SHAPE:-VM.Standard.A1.Flex}" log_step "Creating OCI instance '${name}' (shape: ${shape})..." diff --git a/ovh/lib/common.sh b/ovh/lib/common.sh index 5e968412..640ba5b3 100644 --- a/ovh/lib/common.sh +++ b/ovh/lib/common.sh @@ -265,7 +265,7 @@ print(json.dumps(body)) # Create an OVH Public Cloud instance create_ovh_instance() { local name="$1" - local flavor="${OVH_FLAVOR:-d2-2}" + local flavor="${OVH_FLAVOR:-d2-4}" local region="${OVH_REGION:-GRA7}" # Validate env var inputs to prevent injection into Python code diff --git a/shared/common.sh b/shared/common.sh index aa99b275..912c0006 100644 --- a/shared/common.sh +++ b/shared/common.sh @@ -3120,10 +3120,10 @@ EOF # DEFAULT_VALUE - Default value if env var unset and list is empty or choice invalid # PROMPT_TEXT - Label shown above the menu (e.g., "locations", "server types") # LIST_CALLBACK - Function that outputs pipe-delimited lines (first field = ID) -# DEFAULT_ID - Optional: ID to pre-select as default (e.g., "cpx11") +# DEFAULT_ID - Optional: ID to pre-select as default (e.g., "cx23") # # LIST_CALLBACK must output pipe-delimited lines where the first field is the selectable ID. -# Example output: "fsn1|Falkenstein|DE" or "cpx11|2 vCPU|4 GB RAM|40 GB disk" +# Example output: "fsn1|Falkenstein|DE" or "cx23|2 vCPU|4 GB RAM|40 GB disk" # # Display a numbered list and read user selection # Pipe-delimited items: "id|label". Returns selected id via stdout. diff --git a/test/e2e.sh b/test/e2e.sh index 71fd5a8c..11c7e010 100644 --- a/test/e2e.sh +++ b/test/e2e.sh @@ -81,6 +81,111 @@ _get_token_env_var() { esac } +# --- Credential helpers --- + +# Try to load a token from the spawn config file into the env var. +# Returns 0 if token was loaded, 1 if not. +_load_token_from_config() { + local cloud="$1" + local token_var + token_var=$(_get_token_env_var "$cloud") + [[ -z "$token_var" ]] && return 1 + + # Already set — nothing to do + local current="${!token_var:-}" + [[ -n "$current" ]] && return 0 + + local config_file="${HOME}/.config/spawn/${cloud}.json" + [[ -f "$config_file" ]] || return 1 + + local saved + saved=$(python3 -c "import json, sys; data=json.load(open(sys.argv[1])); print(data.get('api_key','') or data.get('token',''))" "$config_file" 2>/dev/null) + if [[ -n "$saved" ]]; then + export "$token_var=$saved" + return 0 + fi + return 1 +} + +# Interactive credential collection — runs BEFORE non-interactive tests. +# For each token-based cloud, ensures the env var is set by: +# 1. Checking the env var +# 2. Loading from ~/.config/spawn/{cloud}.json +# 3. Prompting the user (Enter to skip) +_collect_credentials() { + local clouds="$1" + local collected="" + local skipped="" + + for cloud in $clouds; do + local token_var + token_var=$(_get_token_env_var "$cloud") + + # CLI-auth clouds (aws, gcp, oracle, sprite) — no token to collect + [[ -z "$token_var" ]] && continue + + # Already in env? + if [[ -n "${!token_var:-}" ]]; then + collected="${collected} ${cloud}" + continue + fi + + # Try config file + if _load_token_from_config "$cloud"; then + _e2e_log "Loaded ${token_var} from ~/.config/spawn/${cloud}.json" + collected="${collected} ${cloud}" + continue + fi + + # Fly: try CLI auth (fly auth token) + if [[ "$cloud" == "fly" ]] && _try_fly_cli_token; then + _e2e_log "Loaded FLY_API_TOKEN from fly CLI auth" + collected="${collected} ${cloud}" + continue + fi + + # No TTY? Can't prompt — skip + if ! echo -n "" > /dev/tty 2>/dev/null; then + skipped="${skipped} ${cloud}" + continue + fi + + # Interactive prompt + printf ' %s: paste %s (Enter to skip): ' "$cloud" "$token_var" + local token="" + read -r token /dev/null; then + fly_cmd="fly" + elif command -v flyctl &>/dev/null; then + fly_cmd="flyctl" + else + return 1 + fi + local token + token=$("$fly_cmd" auth token 2>/dev/null) || return 1 + if [[ -n "$token" ]]; then + export FLY_API_TOKEN="$token" + return 0 + fi + return 1 +} + # --- Credential check --- # Check if a cloud has credentials available (non-interactive) @@ -98,7 +203,7 @@ _cloud_has_credentials() { local) return 0 ;; esac - # Token-based clouds: check env var, then spawn config file + # Token-based clouds: check env var, then spawn config file, then CLI if [[ -n "$token_var" ]]; then local token_val="${!token_var:-}" if [[ -n "$token_val" ]]; then @@ -109,6 +214,10 @@ _cloud_has_credentials() { if [[ -f "$config_file" ]]; then return 0 fi + # Fly: also check CLI auth + if [[ "$cloud" == "fly" ]]; then + _try_fly_cli_token &>/dev/null && return 0 + fi fi return 1 } @@ -314,7 +423,7 @@ _setup_noninteractive_env() { case "$cloud" in hetzner) export HETZNER_LOCATION="${HETZNER_LOCATION:-fsn1}" - export HETZNER_SERVER_TYPE="${HETZNER_SERVER_TYPE:-cpx11}" + export HETZNER_SERVER_TYPE="${HETZNER_SERVER_TYPE:-cx23}" ;; fly) export FLY_REGION="${FLY_REGION:-iad}" @@ -491,60 +600,46 @@ _build_failure_context() { fi } -# Spawn one Claude agent per cloud to fix ALL failures on that cloud at once -auto_fix_cloud() { - local cloud="$1" - shift - local agents="$*" +# Spawn one Claude agent to fix a single failing combo +auto_fix_combo() { + local cloud="$1" agent="$2" if ! command -v claude &>/dev/null; then - _e2e_log "claude CLI not found — skipping auto-fix for ${cloud}" + _e2e_log "claude CLI not found — skipping auto-fix for ${cloud}/${agent}" return 1 fi - local agent_count=0 - local prompt="" - local files_list="" - - # Build combined prompt with all failures for this cloud - for agent in $agents; do - prompt="${prompt}$(_build_failure_context "$cloud" "$agent")" - prompt="${prompt}--- - -" - files_list="${files_list} ${cloud}/${agent}.sh" - agent_count=$((agent_count + 1)) - done + local prompt + prompt=$(_build_failure_context "$cloud" "$agent") local cloud_lib="" if [[ -f "${REPO_ROOT}/${cloud}/lib/common.sh" ]]; then cloud_lib=$(cat "${REPO_ROOT}/${cloud}/lib/common.sh") fi - _e2e_log "Spawning Claude agent for ${cloud} (${agent_count} failure(s): ${agents})..." + _e2e_log "Spawning Claude agent for ${cloud}/${agent}..." - claude -p "You are fixing E2E test failures for the **${cloud}** cloud provider. + claude -p "You are fixing an E2E test failure for **${cloud}/${agent}**. ## Cloud Library (${cloud}/lib/common.sh) \`\`\`bash ${cloud_lib} \`\`\` -## Failures to Fix +## Failure ${prompt} ## Instructions -Fix ALL ${agent_count} failing script(s):${files_list} +Fix the failing script: ${cloud}/${agent}.sh -For each script: 1. Read the error output to understand what went wrong 2. Compare with the reference script (working on another cloud) if available 3. Fix the issue — common problems: wrong install command, missing PATH, timeout in non-TTY 4. Run \`bash -n\` on every modified file -Only modify files under ${cloud}/. Do not modify lib/common.sh or shared/." 2>&1 | tee -a "${E2E_RESULTS_DIR}/autofix_${cloud}.log" || true +Only modify files under ${cloud}/. Do not modify lib/common.sh or shared/." 2>&1 | tee -a "${E2E_RESULTS_DIR}/autofix_${cloud}_${agent}.log" || true } # --- Timing history --- @@ -810,68 +905,48 @@ _build_slow_context() { fi } -# Spawn one Claude agent per cloud to optimize ALL slow combos on that cloud -optimize_slow_cloud() { - local cloud="$1" - shift - # Remaining args: "agent:elapsed:reasons" entries - local entries="$*" +# Spawn one Claude agent to optimize a single slow combo +optimize_slow_combo() { + local cloud="$1" agent="$2" elapsed="$3" reasons="$4" if ! command -v claude &>/dev/null; then - _e2e_log "claude CLI not found — skipping optimization for ${cloud}" + _e2e_log "claude CLI not found — skipping optimization for ${cloud}/${agent}" return 1 fi - local agent_count=0 - local prompt="" - local files_list="" - - for entry in $entries; do - local agent="${entry%%:*}" - local rest="${entry#*:}" - local elapsed="${rest%%:*}" - local reasons - reasons=$(printf '%s' "${rest#*:}" | tr '|' '\n') - - prompt="${prompt}$(_build_slow_context "$cloud" "$agent" "$elapsed" "$reasons")" - prompt="${prompt}--- - -" - files_list="${files_list} ${cloud}/${agent}.sh" - agent_count=$((agent_count + 1)) - done + local prompt + prompt=$(_build_slow_context "$cloud" "$agent" "$elapsed" "$reasons") local cloud_lib="" if [[ -f "${REPO_ROOT}/${cloud}/lib/common.sh" ]]; then cloud_lib=$(cat "${REPO_ROOT}/${cloud}/lib/common.sh") fi - _e2e_log "Spawning Claude agent for ${cloud} (${agent_count} slow combo(s))..." + _e2e_log "Spawning Claude agent for ${cloud}/${agent} (${elapsed}s)..." - claude -p "You are optimizing slow E2E tests for the **${cloud}** cloud provider. -All these scripts PASS but are too slow. + claude -p "You are optimizing a slow E2E test for **${cloud}/${agent}**. +The script PASSES but is too slow. ## Cloud Library (${cloud}/lib/common.sh) \`\`\`bash ${cloud_lib} \`\`\` -## Slow Scripts to Optimize +## Slow Script ${prompt} ## Instructions -Optimize ALL ${agent_count} slow script(s):${files_list} +Optimize the script: ${cloud}/${agent}.sh -For each script: 1. Compare timings with the fastest peer cloud for the same agent 2. Identify what makes it slow (heavy installer, compiling native deps, unnecessary steps) 3. Make it faster — use lighter install methods, skip unnecessary setup, parallelize where possible 4. Run \`bash -n\` on every modified file -5. Don't break anything — the scripts must still pass E2E +5. Don't break anything — the script must still pass E2E -Only modify files under ${cloud}/. Do not modify lib/common.sh or shared/." 2>&1 | tee -a "${E2E_RESULTS_DIR}/optimize_${cloud}.log" || true +Only modify files under ${cloud}/. Do not modify lib/common.sh or shared/." 2>&1 | tee -a "${E2E_RESULTS_DIR}/optimize_${cloud}_${agent}.log" || true } # --- Main --- @@ -975,6 +1050,16 @@ main() { # Testable clouds (excludes local, sprite which don't provision real servers the same way) local testable_clouds="fly hetzner digitalocean ovh aws daytona gcp oracle" + # --- Credential collection (interactive) --- + # Load tokens from config files and prompt for any missing ones + # BEFORE we go non-interactive. This lets the user provide tokens + # that aren't in env vars or config files. + echo "" + _e2e_log "━━━ Credential Collection ━━━" + echo "" + _collect_credentials "$testable_clouds" + echo "" + # Discover clouds with available credentials local available_clouds="" if [[ -n "$filter_cloud" ]]; then @@ -1200,34 +1285,19 @@ main() { done echo "" - # Group slow combos by cloud and spawn one agent per cloud in parallel - local opt_clouds="" + # Spawn one Claude agent per slow combo, all in parallel + local opt_pids="" for entry in $slow_combos; do local combo="${entry%%:*}" + local rest="${entry#*:}" + local elapsed="${rest%%:*}" + local reasons + reasons=$(printf '%s' "${rest#*:}" | tr '|' '\n') local cloud="${combo%%/*}" - case " $opt_clouds " in - *" $cloud "*) ;; # already listed - *) opt_clouds="${opt_clouds} ${cloud}" ;; - esac - done - - local opt_pids="" - for cloud in $opt_clouds; do - # Collect all slow entries for this cloud: "agent:elapsed:reasons ..." - local cloud_entries="" - for entry in $slow_combos; do - local combo="${entry%%:*}" - local entry_cloud="${combo%%/*}" - local agent="${combo##*/}" - if [[ "$entry_cloud" == "$cloud" ]]; then - local rest="${entry#*:}" - cloud_entries="${cloud_entries} ${agent}:${rest}" - fi - done - cloud_entries=$(printf '%s' "$cloud_entries" | sed 's/^ //') + local agent="${combo##*/}" ( - optimize_slow_cloud "$cloud" $cloud_entries + optimize_slow_combo "$cloud" "$agent" "$elapsed" "$reasons" ) & opt_pids="${opt_pids} $!" done @@ -1249,7 +1319,7 @@ main() { local cloud="${combo%%/*}" local agent="${combo##*/}" - run_e2e_test "$cloud" "$agent" + run_e2e_test "$cloud" "$agent" || true local result_file="${E2E_RESULTS_DIR}/${cloud}_${agent}.result" local timing_file="${E2E_RESULTS_DIR}/${cloud}_${agent}.timing" @@ -1267,37 +1337,20 @@ main() { done fi - # Auto-fix failures — one Claude agent per cloud, all in parallel + # Auto-fix failures — one Claude agent per combo, all in parallel if [[ "$total_fail" -gt 0 ]] && [[ "$E2E_AUTO_FIX" == "1" ]]; then echo "" _e2e_log "━━━ Auto-Fix Phase ━━━" echo "" - # Group failures by cloud - local fix_clouds="" + # Spawn one agent per failing combo in parallel + local fix_pids="" for combo in $failed_combos; do local cloud="${combo%%/*}" - case " $fix_clouds " in - *" $cloud "*) ;; - *) fix_clouds="${fix_clouds} ${cloud}" ;; - esac - done - - # Spawn one agent per cloud in parallel - local fix_pids="" - for cloud in $fix_clouds; do - local cloud_agents="" - for combo in $failed_combos; do - local c="${combo%%/*}" - local a="${combo##*/}" - if [[ "$c" == "$cloud" ]]; then - cloud_agents="${cloud_agents} ${a}" - fi - done - cloud_agents=$(printf '%s' "$cloud_agents" | sed 's/^ //') + local agent="${combo##*/}" ( - auto_fix_cloud "$cloud" $cloud_agents + auto_fix_combo "$cloud" "$agent" ) & fix_pids="${fix_pids} $!" done @@ -1319,7 +1372,7 @@ main() { local cloud="${combo%%/*}" local agent="${combo##*/}" - run_e2e_test "$cloud" "$agent" + run_e2e_test "$cloud" "$agent" || true local result_file="${E2E_RESULTS_DIR}/${cloud}_${agent}.result" local timing_file="${E2E_RESULTS_DIR}/${cloud}_${agent}.timing" diff --git a/test/fixtures/hetzner/_env.sh b/test/fixtures/hetzner/_env.sh index 44b41957..66fa9083 100644 --- a/test/fixtures/hetzner/_env.sh +++ b/test/fixtures/hetzner/_env.sh @@ -1,4 +1,4 @@ export HCLOUD_TOKEN="test-token-hetzner" export HETZNER_SERVER_NAME="test-srv" -export HETZNER_SERVER_TYPE="cpx11" +export HETZNER_SERVER_TYPE="cx22" export HETZNER_LOCATION="fsn1" diff --git a/test/fixtures/hetzner/datacenters.json b/test/fixtures/hetzner/datacenters.json index d63cca4b..2ad8bbcc 100644 --- a/test/fixtures/hetzner/datacenters.json +++ b/test/fixtures/hetzner/datacenters.json @@ -12,7 +12,7 @@ "city": "Falkenstein" }, "server_types": { - "available": [1, 3, 5, 7, 9, 11, 22, 23, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97], + "available": [1, 3, 5, 7, 9, 11, 22, 23, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 120], "available_for_migration": [1, 3, 5, 7, 9, 11, 22, 23, 33, 34, 35, 36], "supported": [1, 3, 5, 7, 9, 11, 22, 23, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97] } diff --git a/test/fixtures/hetzner/server_types.json b/test/fixtures/hetzner/server_types.json index 24dbc1ea..93cb04b5 100644 --- a/test/fixtures/hetzner/server_types.json +++ b/test/fixtures/hetzner/server_types.json @@ -2591,6 +2591,87 @@ ], "storage_type": "local" }, + { + "architecture": "x86", + "category": "cost_optimized", + "cores": 2, + "cpu_type": "shared", + "deprecated": false, + "deprecation": null, + "description": "CX 22", + "disk": 40, + "id": 120, + "locations": [ + { + "deprecation": null, + "id": 1, + "name": "fsn1" + }, + { + "deprecation": null, + "id": 2, + "name": "nbg1" + }, + { + "deprecation": null, + "id": 3, + "name": "hel1" + } + ], + "memory": 2, + "name": "cx22", + "prices": [ + { + "included_traffic": 21990232555520, + "location": "fsn1", + "price_hourly": { + "gross": "0.0048000000000000", + "net": "0.0048000000" + }, + "price_monthly": { + "gross": "2.9900000000000000", + "net": "2.9900000000" + }, + "price_per_tb_traffic": { + "gross": "1.2000000000000000", + "net": "1.2000000000" + } + }, + { + "included_traffic": 21990232555520, + "location": "hel1", + "price_hourly": { + "gross": "0.0048000000000000", + "net": "0.0048000000" + }, + "price_monthly": { + "gross": "2.9900000000000000", + "net": "2.9900000000" + }, + "price_per_tb_traffic": { + "gross": "1.2000000000000000", + "net": "1.2000000000" + } + }, + { + "included_traffic": 21990232555520, + "location": "nbg1", + "price_hourly": { + "gross": "0.0048000000000000", + "net": "0.0048000000" + }, + "price_monthly": { + "gross": "2.9900000000000000", + "net": "2.9900000000" + }, + "price_per_tb_traffic": { + "gross": "1.2000000000000000", + "net": "1.2000000000" + } + } + ], + "storage_type": "local" + }, { "architecture": "x86", "category": "cost_optimized",