From 83775f2a2774d8552df7d1de646b892af1cb6458 Mon Sep 17 00:00:00 2001 From: Ahmed Abushagur Date: Tue, 10 Feb 2026 14:32:13 -0800 Subject: [PATCH 01/13] feat: testing --- .claude/settings.local.json | 22 + aws-lightsail/claude.sh | 8 +- binarylane/claude.sh | 8 +- civo/claude.sh | 10 +- daytona/claude.sh | 8 +- digitalocean/claude.sh | 8 +- e2b/claude.sh | 8 +- gcp/claude.sh | 8 +- genesiscloud/claude.sh | 8 +- genesiscloud/lib/common.sh | 144 ++-- hetzner/claude.sh | 8 +- ionos/claude.sh | 8 +- kamatera/claude.sh | 10 +- linode/claude.sh | 8 +- modal/claude.sh | 8 +- oracle/claude.sh | 8 +- ovh/claude.sh | 8 +- test/fixtures/digitalocean/_metadata.json | 14 +- test/fixtures/digitalocean/regions.json | 25 +- test/fixtures/hetzner/_metadata.json | 14 +- test/fixtures/hetzner/create_server.json | 30 +- test/fixtures/hetzner/delete_server.json | 6 +- test/fixtures/hetzner/servers.json | 562 +-------------- test/fixtures/linode/_metadata.json | 14 +- test/fixtures/vultr/_metadata.json | 14 +- test/fixtures/vultr/account.json | 12 +- test/fixtures/vultr/plans.json | 13 +- test/record.sh | 232 +++---- test/run.sh | 787 +++++++++++++--------- vultr/claude.sh | 8 +- 30 files changed, 806 insertions(+), 1215 deletions(-) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000..d5f3891e3 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,22 @@ +{ + "permissions": { + "allow": [ + "Bash(done)", + "Bash(grep:*)", + "Bash(for cloud in sprite hetzner digitalocean vultr linode)", + "Bash(do)", + "Bash(echo:*)", + "Bash(if ! grep -q \"setup_shell_environment\\\\|inject_env_vars\" \"/Users/ahmed/Documents/GitHub/spawn/$cloud/lib/common.sh\")", + "Bash(then)", + "Bash(fi)", + "Bash(if ! grep -q \"setup_claude_code_config\\\\|setup_openclaw_config\" \"/Users/ahmed/Documents/GitHub/spawn/$cloud/lib/common.sh\")", + "Bash(if ! grep -q \"run_server\\\\|upload_file\\\\|interactive_session\" \"/Users/ahmed/Documents/GitHub/spawn/$cloud/lib/common.sh\")", + "Bash( ls -d /Users/ahmed/Documents/GitHub/spawn/*/)", + "Bash(xargs:*)", + "Read(//private/tmp/**)", + "Bash(wc:*)", + "Bash(ls:*)", + "Bash(bash:*)" + ] + } +} diff --git a/aws-lightsail/claude.sh b/aws-lightsail/claude.sh index 7b353b9ae..0c54a219f 100644 --- a/aws-lightsail/claude.sh +++ b/aws-lightsail/claude.sh @@ -30,9 +30,9 @@ verify_server_connectivity "${LIGHTSAIL_SERVER_IP}" wait_for_cloud_init "${LIGHTSAIL_SERVER_IP}" 60 # 5. Verify Claude Code is installed (fallback to manual install) -log_step "Verifying Claude Code installation..." +log_warn "Verifying Claude Code installation..." if ! run_server "${LIGHTSAIL_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_step "Claude Code not found, installing manually..." + log_warn "Claude Code not found, installing manually..." run_server "${LIGHTSAIL_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -46,7 +46,7 @@ else fi # 7. Inject environment variables into ~/.zshrc -log_step "Setting up environment variables..." +log_warn "Setting up environment variables..." inject_env_vars_ssh "${LIGHTSAIL_INSTANCE_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ @@ -67,7 +67,7 @@ log_info "Instance: ${SERVER_NAME} (IP: ${LIGHTSAIL_SERVER_IP})" echo "" # 9. Start Claude Code interactively -log_step "Starting Claude Code..." +log_warn "Starting Claude Code..." sleep 1 clear interactive_session "${LIGHTSAIL_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/binarylane/claude.sh b/binarylane/claude.sh index 6ab29ff66..fda858c31 100644 --- a/binarylane/claude.sh +++ b/binarylane/claude.sh @@ -20,9 +20,9 @@ create_server "${SERVER_NAME}" verify_server_connectivity "${BINARYLANE_SERVER_IP}" wait_for_cloud_init "${BINARYLANE_SERVER_IP}" 60 -log_step "Verifying Claude Code installation..." +log_warn "Verifying Claude Code installation..." if ! run_server "${BINARYLANE_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_step "Claude Code not found, installing manually..." + log_warn "Claude Code not found, installing manually..." run_server "${BINARYLANE_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -34,7 +34,7 @@ else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) fi -log_step "Setting up environment variables..." +log_warn "Setting up environment variables..." inject_env_vars_ssh "${BINARYLANE_SERVER_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ @@ -52,7 +52,7 @@ log_info "BinaryLane server setup completed successfully!" log_info "Server: ${SERVER_NAME} (ID: ${BINARYLANE_SERVER_ID}, IP: ${BINARYLANE_SERVER_IP})" echo "" -log_step "Starting Claude Code..." +log_warn "Starting Claude Code..." sleep 1 clear interactive_session "${BINARYLANE_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/civo/claude.sh b/civo/claude.sh index 2edd37092..a9dd9588c 100644 --- a/civo/claude.sh +++ b/civo/claude.sh @@ -19,13 +19,13 @@ SERVER_NAME=$(get_server_name) create_server "${SERVER_NAME}" verify_server_connectivity "${CIVO_SERVER_IP}" -log_step "Waiting for cloud-init to complete..." +log_warn "Waiting for cloud-init to complete..." # Civo uses init scripts, wait for completion marker generic_ssh_wait "root" "${CIVO_SERVER_IP}" "${SSH_OPTS} -o ConnectTimeout=5" "test -f /root/.cloud-init-complete" "cloud-init" 60 5 -log_step "Verifying Claude Code installation..." +log_warn "Verifying Claude Code installation..." if ! run_server "${CIVO_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_step "Claude Code not found, installing manually..." + log_warn "Claude Code not found, installing manually..." run_server "${CIVO_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -37,7 +37,7 @@ else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) fi -log_step "Setting up environment variables..." +log_warn "Setting up environment variables..." inject_env_vars_ssh "${CIVO_SERVER_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ @@ -55,7 +55,7 @@ log_info "Civo instance setup completed successfully!" log_info "Server: ${SERVER_NAME} (ID: ${CIVO_SERVER_ID}, IP: ${CIVO_SERVER_IP})" echo "" -log_step "Starting Claude Code..." +log_warn "Starting Claude Code..." sleep 1 clear interactive_session "${CIVO_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/daytona/claude.sh b/daytona/claude.sh index 0ceeb5ae5..bbff6a2d1 100644 --- a/daytona/claude.sh +++ b/daytona/claude.sh @@ -26,9 +26,9 @@ create_server "${SERVER_NAME}" wait_for_cloud_init # 4. Verify Claude Code is installed (fallback to manual install) -log_step "Verifying Claude Code installation..." +log_warn "Verifying Claude Code installation..." if ! run_server "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_step "Claude Code not found, installing manually..." + log_warn "Claude Code not found, installing manually..." run_server "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -42,7 +42,7 @@ else fi # 6. Inject environment variables into ~/.zshrc -log_step "Setting up environment variables..." +log_warn "Setting up environment variables..." inject_env_vars_local upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ @@ -63,7 +63,7 @@ log_info "Sandbox: ${SERVER_NAME} (ID: ${DAYTONA_SANDBOX_ID})" echo "" # 8. Start Claude Code interactively -log_step "Starting Claude Code..." +log_warn "Starting Claude Code..." sleep 1 clear interactive_session "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/digitalocean/claude.sh b/digitalocean/claude.sh index 480b46e02..f0322bf8a 100755 --- a/digitalocean/claude.sh +++ b/digitalocean/claude.sh @@ -31,9 +31,9 @@ verify_server_connectivity "${DO_SERVER_IP}" wait_for_cloud_init "${DO_SERVER_IP}" 60 # 5. Verify Claude Code is installed (fallback to manual install) -log_step "Verifying Claude Code installation..." +log_warn "Verifying Claude Code installation..." if ! run_server "${DO_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_step "Claude Code not found, installing manually..." + log_warn "Claude Code not found, installing manually..." run_server "${DO_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi @@ -54,7 +54,7 @@ else fi # 7. Inject environment variables into ~/.zshrc -log_step "Setting up environment variables..." +log_warn "Setting up environment variables..." inject_env_vars_ssh "${DO_SERVER_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ @@ -74,7 +74,7 @@ log_info "Droplet: ${DROPLET_NAME} (ID: ${DO_DROPLET_ID}, IP: ${DO_SERVER_IP})" echo "" # 9. Start Claude Code interactively -log_step "Starting Claude Code..." +log_warn "Starting Claude Code..." sleep 1 clear interactive_session "${DO_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/e2b/claude.sh b/e2b/claude.sh index a6436d7c8..0d2c41f1d 100755 --- a/e2b/claude.sh +++ b/e2b/claude.sh @@ -26,9 +26,9 @@ create_server "${SERVER_NAME}" wait_for_cloud_init # 4. Verify Claude Code is installed (fallback to manual install) -log_step "Verifying Claude Code installation..." +log_warn "Verifying Claude Code installation..." if ! run_server "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_step "Claude Code not found, installing manually..." + log_warn "Claude Code not found, installing manually..." run_server "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -42,7 +42,7 @@ else fi # 6. Inject environment variables into ~/.zshrc -log_step "Setting up environment variables..." +log_warn "Setting up environment variables..." inject_env_vars_local upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ @@ -63,7 +63,7 @@ log_info "Sandbox: ${SERVER_NAME} (ID: ${E2B_SANDBOX_ID})" echo "" # 8. Start Claude Code interactively -log_step "Starting Claude Code..." +log_warn "Starting Claude Code..." sleep 1 clear interactive_session "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/gcp/claude.sh b/gcp/claude.sh index 79012e143..a7ff8621c 100644 --- a/gcp/claude.sh +++ b/gcp/claude.sh @@ -33,9 +33,9 @@ verify_server_connectivity "${GCP_SERVER_IP}" wait_for_cloud_init "${GCP_SERVER_IP}" 60 # 5. Verify Claude Code is installed (fallback to manual install) -log_step "Verifying Claude Code installation..." +log_warn "Verifying Claude Code installation..." if ! run_server "${GCP_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_step "Claude Code not found, installing manually..." + log_warn "Claude Code not found, installing manually..." run_server "${GCP_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -49,7 +49,7 @@ else fi # 7. Inject environment variables into ~/.zshrc -log_step "Setting up environment variables..." +log_warn "Setting up environment variables..." inject_env_vars_ssh "${GCP_SERVER_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ @@ -70,7 +70,7 @@ log_info "Instance: ${GCP_INSTANCE_NAME_ACTUAL} (Zone: ${GCP_ZONE}, IP: ${GCP_SE echo "" # 9. Start Claude Code interactively -log_step "Starting Claude Code..." +log_warn "Starting Claude Code..." sleep 1 clear interactive_session "${GCP_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/genesiscloud/claude.sh b/genesiscloud/claude.sh index dd5ebbc07..3a480d411 100644 --- a/genesiscloud/claude.sh +++ b/genesiscloud/claude.sh @@ -20,9 +20,9 @@ create_server "${SERVER_NAME}" verify_server_connectivity "${GENESIS_SERVER_IP}" wait_for_cloud_init "${GENESIS_SERVER_IP}" 60 -log_step "Verifying Claude Code installation..." +log_warn "Verifying Claude Code installation..." if ! run_server "${GENESIS_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_step "Claude Code not found, installing manually..." + log_warn "Claude Code not found, installing manually..." run_server "${GENESIS_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -34,7 +34,7 @@ else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) fi -log_step "Setting up environment variables..." +log_warn "Setting up environment variables..." inject_env_vars_ssh "${GENESIS_SERVER_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ @@ -52,7 +52,7 @@ log_info "Genesis Cloud server setup completed successfully!" log_info "Server: ${SERVER_NAME} (ID: ${GENESIS_SERVER_ID}, IP: ${GENESIS_SERVER_IP})" echo "" -log_step "Starting Claude Code..." +log_warn "Starting Claude Code..." sleep 1 clear interactive_session "${GENESIS_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/genesiscloud/lib/common.sh b/genesiscloud/lib/common.sh index bf52d52dd..c6462bde0 100644 --- a/genesiscloud/lib/common.sh +++ b/genesiscloud/lib/common.sh @@ -63,7 +63,10 @@ ensure_genesis_token() { # Check if SSH key is registered with Genesis Cloud genesis_check_ssh_key() { - check_ssh_key_by_fingerprint genesis_api "/ssh-keys" "$1" + local fingerprint="$1" + local existing_keys + existing_keys=$(genesis_api GET "/ssh-keys") + echo "$existing_keys" | grep -q "$fingerprint" } # Register SSH key with Genesis Cloud @@ -98,52 +101,14 @@ ensure_ssh_key() { } get_server_name() { - get_validated_server_name "GENESIS_SERVER_NAME" "Enter instance name: " -} + local server_name + server_name=$(get_resource_name "GENESIS_SERVER_NAME" "Enter instance name: ") || return 1 -# Get all SSH key IDs from Genesis Cloud -_genesis_get_ssh_key_ids() { - local ssh_keys_response - ssh_keys_response=$(genesis_api GET "/ssh-keys") - echo "$ssh_keys_response" | python3 -c " -import json, sys -data = json.loads(sys.stdin.read()) -keys = data.get('ssh_keys', []) -ids = [k['id'] for k in keys] -print(json.dumps(ids)) -" -} - -# Build Genesis Cloud instance creation request body -# $1=name $2=instance_type $3=region $4=image $5=ssh_key_ids -_genesis_build_instance_body() { - local name="$1" instance_type="$2" region="$3" image="$4" ssh_key_ids="$5" - - local userdata - userdata=$(get_cloud_init_userdata) - - echo "$userdata" | python3 -c " -import json, sys -userdata = sys.stdin.read() -body = { - 'name': sys.argv[1], - 'type': sys.argv[2], - 'region': sys.argv[3], - 'image': sys.argv[4], - 'ssh_key_ids': json.loads(sys.argv[5]), - 'startup_script': userdata -} -print(json.dumps(body)) -" "$name" "$instance_type" "$region" "$image" "$ssh_key_ids" -} + if ! validate_server_name "$server_name"; then + return 1 + fi -# Poll Genesis Cloud API until instance is active, then extract IP -# Sets GENESIS_SERVER_IP on success -_genesis_wait_for_instance() { - local server_id="$1" - generic_wait_for_instance genesis_api "/instances/${server_id}" \ - "active" "d['instance']['status']" "d['instance']['public_ip']" \ - GENESIS_SERVER_IP "Instance" 60 + echo "$server_name" } create_server() { @@ -158,13 +123,37 @@ create_server() { # Image names may contain spaces (e.g. "Ubuntu 24.04") but must not contain quotes or shell metacharacters if [[ "$image" =~ [\'\"\`\$\;\\] ]]; then log_error "Invalid GENESIS_IMAGE: contains unsafe characters"; return 1; fi - log_step "Creating Genesis Cloud instance '$name' (type: $instance_type, region: $region)..." + log_warn "Creating Genesis Cloud instance '$name' (type: $instance_type, region: $region)..." + # Get all SSH key IDs + local ssh_keys_response + ssh_keys_response=$(genesis_api GET "/ssh-keys") local ssh_key_ids - ssh_key_ids=$(_genesis_get_ssh_key_ids) + ssh_key_ids=$(echo "$ssh_keys_response" | python3 -c " +import json, sys +data = json.loads(sys.stdin.read()) +keys = data.get('ssh_keys', []) +ids = [k['id'] for k in keys] +print(json.dumps(ids)) +") + + local userdata + userdata=$(get_cloud_init_userdata) local body - body=$(_genesis_build_instance_body "$name" "$instance_type" "$region" "$image" "$ssh_key_ids") + body=$(echo "$userdata" | python3 -c " +import json, sys +userdata = sys.stdin.read() +body = { + 'name': '$name', + 'type': '$instance_type', + 'region': '$region', + 'image': '$image', + 'ssh_key_ids': $ssh_key_ids, + 'startup_script': userdata +} +print(json.dumps(body)) +") local response response=$(genesis_api POST "/instances" "$body") @@ -175,9 +164,11 @@ create_server() { log_info "Instance created: ID=$GENESIS_SERVER_ID" else log_error "Failed to create Genesis Cloud instance" + local error_msg error_msg=$(echo "$response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('error',{}).get('message', d.get('message','Unknown error')))" 2>/dev/null || echo "$response") log_error "API Error: $error_msg" + log_warn "Common issues:" log_warn " - Insufficient account balance" log_warn " - Instance type unavailable in region (try different GENESIS_INSTANCE_TYPE or GENESIS_REGION)" @@ -186,18 +177,61 @@ create_server() { return 1 fi - _genesis_wait_for_instance "$GENESIS_SERVER_ID" + # Wait for instance to get an IP and become active + log_warn "Waiting for instance to become active..." + local max_attempts=60 + local attempt=1 + while [[ "$attempt" -le "$max_attempts" ]]; do + local status_response + status_response=$(genesis_api GET "/instances/$GENESIS_SERVER_ID") + local status + status=$(echo "$status_response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['instance']['status'])") + + if [[ "$status" == "active" ]]; then + GENESIS_SERVER_IP=$(echo "$status_response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['instance']['public_ip'])") + export GENESIS_SERVER_IP + log_info "Instance active: IP=$GENESIS_SERVER_IP" + return 0 + fi + + log_warn "Instance status: $status ($attempt/$max_attempts)" + sleep "${INSTANCE_STATUS_POLL_DELAY}" + attempt=$((attempt + 1)) + done + + log_error "Instance did not become active in time" + return 1 +} + +verify_server_connectivity() { + local ip="$1" + local max_attempts=${2:-30} + # SSH_OPTS is defined in shared/common.sh + # shellcheck disable=SC2154 + generic_ssh_wait "root" "$ip" "$SSH_OPTS -o ConnectTimeout=5" "echo ok" "SSH connectivity" "$max_attempts" 5 +} + +run_server() { + local ip="$1"; local cmd="$2" + # shellcheck disable=SC2086 + ssh $SSH_OPTS "root@$ip" "$cmd" } -# SSH operations — delegates to shared helpers (SSH_USER defaults to root) -verify_server_connectivity() { ssh_verify_connectivity "$@"; } -run_server() { ssh_run_server "$@"; } -upload_file() { ssh_upload_file "$@"; } -interactive_session() { ssh_interactive_session "$@"; } +upload_file() { + local ip="$1"; local local_path="$2"; local remote_path="$3" + # shellcheck disable=SC2086 + scp $SSH_OPTS "$local_path" "root@$ip:$remote_path" +} + +interactive_session() { + local ip="$1"; local cmd="$2" + # shellcheck disable=SC2086 + ssh -t $SSH_OPTS "root@$ip" "$cmd" +} destroy_server() { local server_id="$1" - log_step "Destroying instance $server_id..." + log_warn "Destroying instance $server_id..." genesis_api DELETE "/instances/$server_id" log_info "Instance $server_id destroyed" } diff --git a/hetzner/claude.sh b/hetzner/claude.sh index 78346bf2b..de7690721 100755 --- a/hetzner/claude.sh +++ b/hetzner/claude.sh @@ -28,9 +28,9 @@ verify_server_connectivity "${HETZNER_SERVER_IP}" wait_for_cloud_init "${HETZNER_SERVER_IP}" 60 # 5. Verify Claude Code is installed (fallback to manual install) -log_step "Verifying Claude Code installation..." +log_warn "Verifying Claude Code installation..." if ! run_server "${HETZNER_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_step "Claude Code not found, installing manually..." + log_warn "Claude Code not found, installing manually..." run_server "${HETZNER_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi @@ -50,7 +50,7 @@ else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) fi -log_step "Setting up environment variables..." +log_warn "Setting up environment variables..." inject_env_vars_ssh "${HETZNER_SERVER_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ @@ -70,7 +70,7 @@ log_info "Server: ${SERVER_NAME} (ID: ${HETZNER_SERVER_ID}, IP: ${HETZNER_SERVER echo "" # 9. Start Claude Code interactively -log_step "Starting Claude Code..." +log_warn "Starting Claude Code..." sleep 1 clear interactive_session "${HETZNER_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/ionos/claude.sh b/ionos/claude.sh index 8fa6f55be..0808c4dea 100644 --- a/ionos/claude.sh +++ b/ionos/claude.sh @@ -30,9 +30,9 @@ verify_server_connectivity "${LAMBDA_SERVER_IP}" wait_for_cloud_init "${LAMBDA_SERVER_IP}" # 5. Verify Claude Code is installed (fallback to manual install) -log_step "Verifying Claude Code installation..." +log_warn "Verifying Claude Code installation..." if ! run_server "${LAMBDA_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_step "Claude Code not found, installing manually..." + log_warn "Claude Code not found, installing manually..." run_server "${LAMBDA_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -46,7 +46,7 @@ else fi # 7. Inject environment variables into ~/.zshrc -log_step "Setting up environment variables..." +log_warn "Setting up environment variables..." inject_env_vars_ssh "${LAMBDA_INSTANCE_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ @@ -67,7 +67,7 @@ log_info "Instance: ${SERVER_NAME} (IP: ${LAMBDA_SERVER_IP})" echo "" # 9. Start Claude Code interactively -log_step "Starting Claude Code..." +log_warn "Starting Claude Code..." sleep 1 clear interactive_session "${LAMBDA_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/kamatera/claude.sh b/kamatera/claude.sh index 4769472f7..388cf3a6d 100644 --- a/kamatera/claude.sh +++ b/kamatera/claude.sh @@ -19,12 +19,12 @@ SERVER_NAME=$(get_server_name) create_server "${SERVER_NAME}" verify_server_connectivity "${KAMATERA_SERVER_IP}" -log_step "Waiting for init script to complete..." +log_warn "Waiting for init script to complete..." generic_ssh_wait "root" "${KAMATERA_SERVER_IP}" "${SSH_OPTS} -o ConnectTimeout=5" "test -f /root/.cloud-init-complete" "init script" 60 5 -log_step "Verifying Claude Code installation..." +log_warn "Verifying Claude Code installation..." if ! run_server "${KAMATERA_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_step "Claude Code not found, installing manually..." + log_warn "Claude Code not found, installing manually..." run_server "${KAMATERA_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -36,7 +36,7 @@ else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) fi -log_step "Setting up environment variables..." +log_warn "Setting up environment variables..." inject_env_vars_ssh "${KAMATERA_SERVER_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ @@ -54,7 +54,7 @@ log_info "Kamatera server setup completed successfully!" log_info "Server: ${SERVER_NAME} (IP: ${KAMATERA_SERVER_IP})" echo "" -log_step "Starting Claude Code..." +log_warn "Starting Claude Code..." sleep 1 clear interactive_session "${KAMATERA_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/linode/claude.sh b/linode/claude.sh index ea11c3526..1296547bd 100755 --- a/linode/claude.sh +++ b/linode/claude.sh @@ -12,16 +12,16 @@ SERVER_NAME=$(get_server_name) create_server "${SERVER_NAME}" verify_server_connectivity "${LINODE_SERVER_IP}" wait_for_cloud_init "${LINODE_SERVER_IP}" 60 -log_step "Verifying Claude Code installation..." +log_warn "Verifying Claude Code installation..." if ! run_server "${LINODE_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_step "Claude Code not found, installing manually..." + log_warn "Claude Code not found, installing manually..." run_server "${LINODE_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" echo "" if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then log_info "Using OpenRouter API key from environment" else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180); fi -log_step "Setting up environment variables..." +log_warn "Setting up environment variables..." inject_env_vars_ssh "${LINODE_SERVER_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ @@ -36,7 +36,7 @@ echo "" log_info "Linode setup completed successfully!" log_info "Server: ${SERVER_NAME} (ID: ${LINODE_SERVER_ID}, IP: ${LINODE_SERVER_IP})" echo "" -log_step "Starting Claude Code..." +log_warn "Starting Claude Code..." sleep 1 clear interactive_session "${LINODE_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/modal/claude.sh b/modal/claude.sh index 53f9c9640..c4313e490 100755 --- a/modal/claude.sh +++ b/modal/claude.sh @@ -32,9 +32,9 @@ fi wait_for_cloud_init # 4. Verify Claude Code is installed (fallback to manual install) -log_step "Verifying Claude Code installation..." +log_warn "Verifying Claude Code installation..." if ! run_server "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_step "Claude Code not found, installing manually..." + log_warn "Claude Code not found, installing manually..." run_server "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -48,7 +48,7 @@ else fi # 6. Inject environment variables into ~/.zshrc -log_step "Setting up environment variables..." +log_warn "Setting up environment variables..." inject_env_vars_local upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ @@ -68,7 +68,7 @@ log_info "Sandbox: ${SERVER_NAME} (ID: ${MODAL_SANDBOX_ID})" echo "" # 8. Start Claude Code interactively -log_step "Starting Claude Code..." +log_warn "Starting Claude Code..." sleep 1 clear interactive_session "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/oracle/claude.sh b/oracle/claude.sh index e8c37f600..207c64633 100644 --- a/oracle/claude.sh +++ b/oracle/claude.sh @@ -33,9 +33,9 @@ verify_server_connectivity "${OCI_SERVER_IP}" wait_for_cloud_init "${OCI_SERVER_IP}" 60 # 5. Verify Claude Code is installed (fallback to manual install) -log_step "Verifying Claude Code installation..." +log_warn "Verifying Claude Code installation..." if ! run_server "${OCI_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_step "Claude Code not found, installing manually..." + log_warn "Claude Code not found, installing manually..." run_server "${OCI_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -49,7 +49,7 @@ else fi # 7. Inject environment variables into ~/.zshrc -log_step "Setting up environment variables..." +log_warn "Setting up environment variables..." inject_env_vars_ssh "${OCI_SERVER_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ @@ -70,7 +70,7 @@ log_info "Instance: ${OCI_INSTANCE_NAME_ACTUAL} (IP: ${OCI_SERVER_IP})" echo "" # 9. Start Claude Code interactively -log_step "Starting Claude Code..." +log_warn "Starting Claude Code..." sleep 1 clear interactive_session "${OCI_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/ovh/claude.sh b/ovh/claude.sh index 71182c5bf..e3644842c 100644 --- a/ovh/claude.sh +++ b/ovh/claude.sh @@ -33,9 +33,9 @@ verify_server_connectivity "${OVH_SERVER_IP}" install_base_deps "${OVH_SERVER_IP}" # 7. Verify Claude Code is installed (fallback to manual install) -log_step "Verifying Claude Code installation..." +log_warn "Verifying Claude Code installation..." if ! run_ovh "${OVH_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_step "Claude Code not found, installing manually..." + log_warn "Claude Code not found, installing manually..." run_ovh "${OVH_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi @@ -55,7 +55,7 @@ else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) fi -log_step "Setting up environment variables..." +log_warn "Setting up environment variables..." inject_env_vars_ovh "${OVH_SERVER_IP}" \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ @@ -75,7 +75,7 @@ log_info "Instance: ${SERVER_NAME} (ID: ${OVH_INSTANCE_ID}, IP: ${OVH_SERVER_IP} echo "" # 10. Start Claude Code interactively -log_step "Starting Claude Code..." +log_warn "Starting Claude Code..." sleep 1 clear interactive_session "${OVH_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/test/fixtures/digitalocean/_metadata.json b/test/fixtures/digitalocean/_metadata.json index f6bbc7728..9b546f315 100644 --- a/test/fixtures/digitalocean/_metadata.json +++ b/test/fixtures/digitalocean/_metadata.json @@ -1,13 +1,11 @@ { "cloud": "digitalocean", - "recorded_at": "2026-02-11T03:07:38Z", + "recorded_at": "2026-02-10T21:55:29Z", "fixtures": { - "account": {"endpoint": "/account", "recorded_at": "2026-02-11T03:07:30Z"}, - "ssh_keys": {"endpoint": "/account/keys", "recorded_at": "2026-02-11T03:07:30Z"}, - "droplets": {"endpoint": "/droplets", "recorded_at": "2026-02-11T03:07:31Z"}, - "sizes": {"endpoint": "/sizes", "recorded_at": "2026-02-11T03:07:31Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-11T03:07:32Z"}, - "create_server": {"endpoint": "POST /droplets", "type": "live", "recorded_at": "2026-02-11T03:07:34Z"}, - "delete_server": {"endpoint": "DELETE /droplets/{id}", "type": "live", "recorded_at": "2026-02-11T03:07:38Z"} + "account": {"endpoint": "/account", "recorded_at": "2026-02-10T21:55:27Z"}, + "ssh_keys": {"endpoint": "/account/keys", "recorded_at": "2026-02-10T21:55:27Z"}, + "droplets": {"endpoint": "/droplets", "recorded_at": "2026-02-10T21:55:28Z"}, + "sizes": {"endpoint": "/sizes", "recorded_at": "2026-02-10T21:55:29Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-10T21:55:29Z"} } } diff --git a/test/fixtures/digitalocean/regions.json b/test/fixtures/digitalocean/regions.json index 3ef65964d..2ec9f844f 100644 --- a/test/fixtures/digitalocean/regions.json +++ b/test/fixtures/digitalocean/regions.json @@ -289,11 +289,9 @@ "c-16-intel", "m3-8vcpu-64gb-intel", "c2-16vcpu-32gb-intel", - "g-16vcpu-64gb", "so-8vcpu-64gb-intel", "so-8vcpu-64gb", "m6-8vcpu-64gb", - "gd-16vcpu-64gb", "so1_5-8vcpu-64gb-intel", "g-16vcpu-64gb-intel", "gd-16vcpu-64gb-intel", @@ -317,9 +315,7 @@ "gd-32vcpu-128gb-intel", "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", - "c-48-intel", "m3-24vcpu-192gb-intel", - "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", "so-24vcpu-192gb-intel", "so-24vcpu-192gb", @@ -329,7 +325,6 @@ "m3-32vcpu-256gb-intel", "g-48vcpu-192gb-intel", "gd-48vcpu-192gb-intel", - "c5-48vcpu-96gb-intel", "so1_5-24vcpu-192gb", "so-32vcpu-256gb-intel", "g5_5-48vcpu-192gb-intel", @@ -381,10 +376,12 @@ "s-2vcpu-4gb-amd", "s-2vcpu-4gb-intel", "s-2vcpu-4gb-120gb-intel", + "s-2vcpu-8gb-amd", "c-2", "c2-2vcpu-4gb", "s-2vcpu-8gb-160gb-intel", "s-4vcpu-8gb", + "s-4vcpu-8gb-amd", "s-4vcpu-8gb-intel", "c5-2vcpu-4gb", "g-2vcpu-8gb", @@ -392,6 +389,7 @@ "gd-2vcpu-8gb", "g-2vcpu-8gb-intel", "gd-2vcpu-8gb-intel", + "s-4vcpu-16gb-amd", "m-2vcpu-16gb", "c-4", "g6_5-2vcpu-8gb", @@ -403,6 +401,7 @@ "m3-2vcpu-16gb", "c-4-intel", "m3-2vcpu-16gb-intel", + "s-8vcpu-16gb-amd", "s-8vcpu-16gb-intel", "c2-4vcpu-8gb-intel", "c5-4vcpu-8gb", @@ -417,6 +416,7 @@ "gd-4vcpu-16gb-intel", "c5-4vcpu-8gb-intel", "so1_5-2vcpu-16gb", + "s-8vcpu-32gb-amd", "m-4vcpu-32gb", "c-8", "g6_5-4vcpu-16gb", @@ -1158,17 +1158,13 @@ "c5-16vcpu-32gb-intel", "so1_5-8vcpu-64gb", "m-16vcpu-128gb", - "c-32", "g6_5-16vcpu-64gb", "g5_5-16vcpu-64gb-intel", - "c2-32vcpu-64gb", "m-16vcpu-128gb-intel", "m3-16vcpu-128gb", "c-32-intel", "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", - "c5-32vcpu-64gb", - "c-48", "m-24vcpu-192gb", "g-32vcpu-128gb", "so-16vcpu-128gb-intel", @@ -1176,7 +1172,6 @@ "m6-16vcpu-128gb", "gd-32vcpu-128gb", "so1_5-16vcpu-128gb-intel", - "c2-48vcpu-96gb", "m-24vcpu-192gb-intel", "g-32vcpu-128gb-intel", "m3-24vcpu-192gb", @@ -1191,27 +1186,29 @@ "g6_5-32vcpu-128gb", "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", - "c5-48vcpu-96gb", "so-24vcpu-192gb-intel", "so-24vcpu-192gb", "m6-24vcpu-192gb", "m-32vcpu-256gb-intel", + "c-60-intel", "m3-32vcpu-256gb", "so1_5-24vcpu-192gb-intel", "m3-32vcpu-256gb-intel", "g6_5-40vcpu-160gb", "g-48vcpu-192gb-intel", + "c2-60vcpu-120gb-intel", "gd-48vcpu-192gb-intel", "c5-48vcpu-96gb-intel", "so1_5-24vcpu-192gb", "so-32vcpu-256gb-intel", - "so-32vcpu-256gb", - "m6-32vcpu-256gb", "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", + "g-60vcpu-240gb-intel", "m-48vcpu-384gb-intel", - "so1_5-32vcpu-256gb", + "gd-60vcpu-240gb-intel", + "c5-60vcpu-120gb-intel", "m3-48vcpu-384gb-intel", + "g5_5-60vcpu-240gb-intel", "so-48vcpu-384gb-intel" ], "slug": "fra1" diff --git a/test/fixtures/hetzner/_metadata.json b/test/fixtures/hetzner/_metadata.json index 2d8280f88..c5883a23a 100644 --- a/test/fixtures/hetzner/_metadata.json +++ b/test/fixtures/hetzner/_metadata.json @@ -1,12 +1,12 @@ { "cloud": "hetzner", - "recorded_at": "2026-02-11T03:07:29Z", + "recorded_at": "2026-02-10T21:55:26Z", "fixtures": { - "server_types": {"endpoint": "/server_types?per_page=50", "recorded_at": "2026-02-11T03:07:22Z"}, - "locations": {"endpoint": "/locations", "recorded_at": "2026-02-11T03:07:23Z"}, - "ssh_keys": {"endpoint": "/ssh_keys", "recorded_at": "2026-02-11T03:07:24Z"}, - "servers": {"endpoint": "/servers", "recorded_at": "2026-02-11T03:07:25Z"}, - "create_server": {"endpoint": "POST /servers", "type": "live", "recorded_at": "2026-02-11T03:07:26Z"}, - "delete_server": {"endpoint": "DELETE /servers/{id}", "type": "live", "recorded_at": "2026-02-11T03:07:29Z"} + "server_types": {"endpoint": "/server_types?per_page=50", "recorded_at": "2026-02-10T21:55:19Z"}, + "locations": {"endpoint": "/locations", "recorded_at": "2026-02-10T21:55:20Z"}, + "ssh_keys": {"endpoint": "/ssh_keys", "recorded_at": "2026-02-10T21:55:21Z"}, + "servers": {"endpoint": "/servers", "recorded_at": "2026-02-10T21:55:22Z"}, + "create_server": {"endpoint": "POST /servers", "type": "live", "recorded_at": "2026-02-10T21:55:24Z"}, + "delete_server": {"endpoint": "DELETE /servers/{id}", "type": "live", "recorded_at": "2026-02-10T21:55:26Z"} } } diff --git a/test/fixtures/hetzner/create_server.json b/test/fixtures/hetzner/create_server.json index d731f2840..74637d827 100644 --- a/test/fixtures/hetzner/create_server.json +++ b/test/fixtures/hetzner/create_server.json @@ -3,11 +3,11 @@ "command": "create_server", "error": null, "finished": null, - "id": 610873205655089, + "id": 610821665885201, "progress": 0, "resources": [ { - "id": 120704776, + "id": 120682780, "type": "server" }, { @@ -15,7 +15,7 @@ "type": "image" } ], - "started": "2026-02-11T03:07:26Z", + "started": "2026-02-10T21:55:23Z", "status": "running" }, "next_actions": [ @@ -23,23 +23,23 @@ "command": "start_server", "error": null, "finished": null, - "id": 610873205655090, - "parent_id": 610873205655089, + "id": 610821665885202, + "parent_id": 610821665885201, "progress": 0, "resources": [ { - "id": 120704776, + "id": 120682780, "type": "server" } ], - "started": "2026-02-11T03:07:26Z", + "started": "2026-02-10T21:55:23Z", "status": "running" } ], "root_password": null, "server": { "backup_window": null, - "created": "2026-02-11T03:07:26Z", + "created": "2026-02-10T21:55:23Z", "datacenter": { "description": "Nuremberg 1 virtual DC 3", "id": 2, @@ -166,7 +166,7 @@ ] } }, - "id": 120704776, + "id": 120682780, "image": { "architecture": "x86", "bound_to": null, @@ -205,7 +205,7 @@ "network_zone": "eu-central" }, "locked": false, - "name": "spawn-record-1770779245", + "name": "spawn-record-1770760522", "outgoing_traffic": 0, "placement_group": null, "primary_disk_size": 40, @@ -219,15 +219,15 @@ "floating_ips": [], "ipv4": { "blocked": false, - "dns_ptr": "static.236.114.98.91.clients.your-server.de", - "id": 117647239, - "ip": "91.98.114.236" + "dns_ptr": "static.197.210.245.188.clients.your-server.de", + "id": 117603379, + "ip": "188.245.210.197" }, "ipv6": { "blocked": false, "dns_ptr": [], - "id": 117647240, - "ip": "2a01:4f8:1c1e:e2da::/64" + "id": 117603380, + "ip": "2a01:4f8:1c1e:bc55::/64" } }, "rescue_enabled": false, diff --git a/test/fixtures/hetzner/delete_server.json b/test/fixtures/hetzner/delete_server.json index d65164ead..bbf825949 100644 --- a/test/fixtures/hetzner/delete_server.json +++ b/test/fixtures/hetzner/delete_server.json @@ -3,15 +3,15 @@ "command": "delete_server", "error": null, "finished": null, - "id": 610873205655095, + "id": 610821665885215, "progress": 0, "resources": [ { - "id": 120704776, + "id": 120682780, "type": "server" } ], - "started": "2026-02-11T03:07:29Z", + "started": "2026-02-10T21:55:26Z", "status": "running" } } diff --git a/test/fixtures/hetzner/servers.json b/test/fixtures/hetzner/servers.json index b30c403b7..dd6315039 100644 --- a/test/fixtures/hetzner/servers.json +++ b/test/fixtures/hetzner/servers.json @@ -6,7 +6,7 @@ "page": 1, "per_page": 25, "previous_page": null, - "total_entries": 3 + "total_entries": 1 } }, "servers": [ @@ -163,7 +163,7 @@ "type": "system" }, "included_traffic": 21990232555520, - "ingoing_traffic": 1481770000, + "ingoing_traffic": 0, "iso": null, "labels": {}, "load_balancers": [], @@ -179,7 +179,7 @@ }, "locked": false, "name": "claude", - "outgoing_traffic": 14700000, + "outgoing_traffic": 0, "placement_group": null, "primary_disk_size": 40, "private_net": [], @@ -287,562 +287,6 @@ }, "status": "running", "volumes": [] - }, - { - "backup_window": null, - "created": "2026-02-11T01:39:38Z", - "datacenter": { - "description": "Nuremberg 1 virtual DC 3", - "id": 2, - "location": { - "city": "Nuremberg", - "country": "DE", - "description": "Nuremberg DC Park 1", - "id": 2, - "latitude": 49.452102, - "longitude": 11.076665, - "name": "nbg1", - "network_zone": "eu-central" - }, - "name": "nbg1-dc3", - "server_types": { - "available": [ - 27, - 28, - 29, - 30, - 31, - 32, - 45, - 93, - 94, - 96, - 97, - 98, - 99, - 100, - 101, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ], - "available_for_migration": [ - 27, - 28, - 29, - 30, - 31, - 32, - 45, - 93, - 94, - 96, - 97, - 98, - 99, - 100, - 101, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ], - "supported": [ - 1, - 3, - 5, - 7, - 9, - 11, - 12, - 13, - 14, - 15, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 45, - 93, - 94, - 95, - 96, - 97, - 98, - 99, - 100, - 101, - 104, - 105, - 106, - 107, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ] - } - }, - "id": 120699126, - "image": { - "architecture": "x86", - "bound_to": null, - "created": "2024-04-25T13:26:27Z", - "created_from": null, - "deleted": null, - "deprecated": null, - "description": "Ubuntu 24.04", - "disk_size": 5, - "id": 161547269, - "image_size": null, - "labels": {}, - "name": "ubuntu-24.04", - "os_flavor": "ubuntu", - "os_version": "24.04", - "protection": { - "delete": false - }, - "rapid_deploy": true, - "status": "available", - "type": "system" - }, - "included_traffic": 21990232555520, - "ingoing_traffic": 0, - "iso": null, - "labels": {}, - "load_balancers": [], - "location": { - "city": "Nuremberg", - "country": "DE", - "description": "Nuremberg DC Park 1", - "id": 2, - "latitude": 49.452102, - "longitude": 11.076665, - "name": "nbg1", - "network_zone": "eu-central" - }, - "locked": false, - "name": "spawn-record-1770773977", - "outgoing_traffic": 0, - "placement_group": null, - "primary_disk_size": 40, - "private_net": [], - "protection": { - "delete": false, - "rebuild": false - }, - "public_net": { - "firewalls": [], - "floating_ips": [], - "ipv4": { - "blocked": false, - "dns_ptr": "static.197.210.245.188.clients.your-server.de", - "id": 117636450, - "ip": "188.245.210.197" - }, - "ipv6": { - "blocked": false, - "dns_ptr": [], - "id": 117636451, - "ip": "2a01:4f8:1c1e:bc55::/64" - } - }, - "rescue_enabled": false, - "server_type": { - "architecture": "x86", - "category": "cost_optimized", - "cores": 2, - "cpu_type": "shared", - "deprecated": false, - "deprecation": null, - "description": "CX 23", - "disk": 40, - "id": 114, - "locations": [ - { - "deprecation": null, - "id": 1, - "name": "fsn1" - }, - { - "deprecation": null, - "id": 2, - "name": "nbg1" - }, - { - "deprecation": null, - "id": 3, - "name": "hel1" - } - ], - "memory": 4, - "name": "cx23", - "prices": [ - { - "included_traffic": 21990232555520, - "location": "fsn1", - "price_hourly": { - "gross": "0.0056000000000000", - "net": "0.0056000000" - }, - "price_monthly": { - "gross": "3.4900000000000000", - "net": "3.4900000000" - }, - "price_per_tb_traffic": { - "gross": "1.2000000000000000", - "net": "1.2000000000" - } - }, - { - "included_traffic": 21990232555520, - "location": "hel1", - "price_hourly": { - "gross": "0.0056000000000000", - "net": "0.0056000000" - }, - "price_monthly": { - "gross": "3.4900000000000000", - "net": "3.4900000000" - }, - "price_per_tb_traffic": { - "gross": "1.2000000000000000", - "net": "1.2000000000" - } - }, - { - "included_traffic": 21990232555520, - "location": "nbg1", - "price_hourly": { - "gross": "0.0056000000000000", - "net": "0.0056000000" - }, - "price_monthly": { - "gross": "3.4900000000000000", - "net": "3.4900000000" - }, - "price_per_tb_traffic": { - "gross": "1.2000000000000000", - "net": "1.2000000000" - } - } - ], - "storage_type": "local" - }, - "status": "running", - "volumes": [] - }, - { - "backup_window": null, - "created": "2026-02-11T01:40:34Z", - "datacenter": { - "description": "Nuremberg 1 virtual DC 3", - "id": 2, - "location": { - "city": "Nuremberg", - "country": "DE", - "description": "Nuremberg DC Park 1", - "id": 2, - "latitude": 49.452102, - "longitude": 11.076665, - "name": "nbg1", - "network_zone": "eu-central" - }, - "name": "nbg1-dc3", - "server_types": { - "available": [ - 27, - 28, - 29, - 30, - 31, - 32, - 45, - 93, - 94, - 96, - 97, - 98, - 99, - 100, - 101, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ], - "available_for_migration": [ - 27, - 28, - 29, - 30, - 31, - 32, - 45, - 93, - 94, - 96, - 97, - 98, - 99, - 100, - 101, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ], - "supported": [ - 1, - 3, - 5, - 7, - 9, - 11, - 12, - 13, - 14, - 15, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 45, - 93, - 94, - 95, - 96, - 97, - 98, - 99, - 100, - 101, - 104, - 105, - 106, - 107, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ] - } - }, - "id": 120699168, - "image": { - "architecture": "x86", - "bound_to": null, - "created": "2024-04-25T13:26:27Z", - "created_from": null, - "deleted": null, - "deprecated": null, - "description": "Ubuntu 24.04", - "disk_size": 5, - "id": 161547269, - "image_size": null, - "labels": {}, - "name": "ubuntu-24.04", - "os_flavor": "ubuntu", - "os_version": "24.04", - "protection": { - "delete": false - }, - "rapid_deploy": true, - "status": "available", - "type": "system" - }, - "included_traffic": 21990232555520, - "ingoing_traffic": 0, - "iso": null, - "labels": {}, - "load_balancers": [], - "location": { - "city": "Nuremberg", - "country": "DE", - "description": "Nuremberg DC Park 1", - "id": 2, - "latitude": 49.452102, - "longitude": 11.076665, - "name": "nbg1", - "network_zone": "eu-central" - }, - "locked": false, - "name": "spawn-record-1770774032", - "outgoing_traffic": 0, - "placement_group": null, - "primary_disk_size": 40, - "private_net": [], - "protection": { - "delete": false, - "rebuild": false - }, - "public_net": { - "firewalls": [], - "floating_ips": [], - "ipv4": { - "blocked": false, - "dns_ptr": "static.36.112.98.91.clients.your-server.de", - "id": 117636536, - "ip": "91.98.112.36" - }, - "ipv6": { - "blocked": false, - "dns_ptr": [], - "id": 117636537, - "ip": "2a01:4f8:1c1c:cff1::/64" - } - }, - "rescue_enabled": false, - "server_type": { - "architecture": "x86", - "category": "cost_optimized", - "cores": 2, - "cpu_type": "shared", - "deprecated": false, - "deprecation": null, - "description": "CX 23", - "disk": 40, - "id": 114, - "locations": [ - { - "deprecation": null, - "id": 1, - "name": "fsn1" - }, - { - "deprecation": null, - "id": 2, - "name": "nbg1" - }, - { - "deprecation": null, - "id": 3, - "name": "hel1" - } - ], - "memory": 4, - "name": "cx23", - "prices": [ - { - "included_traffic": 21990232555520, - "location": "fsn1", - "price_hourly": { - "gross": "0.0056000000000000", - "net": "0.0056000000" - }, - "price_monthly": { - "gross": "3.4900000000000000", - "net": "3.4900000000" - }, - "price_per_tb_traffic": { - "gross": "1.2000000000000000", - "net": "1.2000000000" - } - }, - { - "included_traffic": 21990232555520, - "location": "hel1", - "price_hourly": { - "gross": "0.0056000000000000", - "net": "0.0056000000" - }, - "price_monthly": { - "gross": "3.4900000000000000", - "net": "3.4900000000" - }, - "price_per_tb_traffic": { - "gross": "1.2000000000000000", - "net": "1.2000000000" - } - }, - { - "included_traffic": 21990232555520, - "location": "nbg1", - "price_hourly": { - "gross": "0.0056000000000000", - "net": "0.0056000000" - }, - "price_monthly": { - "gross": "3.4900000000000000", - "net": "3.4900000000" - }, - "price_per_tb_traffic": { - "gross": "1.2000000000000000", - "net": "1.2000000000" - } - } - ], - "storage_type": "local" - }, - "status": "running", - "volumes": [] } ] } diff --git a/test/fixtures/linode/_metadata.json b/test/fixtures/linode/_metadata.json index b050bf247..029fa863d 100644 --- a/test/fixtures/linode/_metadata.json +++ b/test/fixtures/linode/_metadata.json @@ -1,13 +1,11 @@ { "cloud": "linode", - "recorded_at": "2026-02-11T03:01:14Z", + "recorded_at": "2026-02-10T21:55:36Z", "fixtures": { - "profile": {"endpoint": "/profile", "recorded_at": "2026-02-11T03:01:06Z"}, - "ssh_keys": {"endpoint": "/profile/sshkeys", "recorded_at": "2026-02-11T03:01:07Z"}, - "instances": {"endpoint": "/linode/instances", "recorded_at": "2026-02-11T03:01:07Z"}, - "types": {"endpoint": "/linode/types", "recorded_at": "2026-02-11T03:01:08Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-11T03:01:08Z"}, - "create_server": {"endpoint": "POST /linode/instances", "type": "live", "recorded_at": "2026-02-11T03:01:10Z"}, - "delete_server": {"endpoint": "DELETE /linode/instances/{id}", "type": "live", "recorded_at": "2026-02-11T03:01:14Z"} + "profile": {"endpoint": "/profile", "recorded_at": "2026-02-10T21:55:34Z"}, + "ssh_keys": {"endpoint": "/profile/sshkeys", "recorded_at": "2026-02-10T21:55:34Z"}, + "instances": {"endpoint": "/linode/instances", "recorded_at": "2026-02-10T21:55:35Z"}, + "types": {"endpoint": "/linode/types", "recorded_at": "2026-02-10T21:55:35Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-10T21:55:36Z"} } } diff --git a/test/fixtures/vultr/_metadata.json b/test/fixtures/vultr/_metadata.json index 523379dac..96a70de8c 100644 --- a/test/fixtures/vultr/_metadata.json +++ b/test/fixtures/vultr/_metadata.json @@ -1,13 +1,11 @@ { "cloud": "vultr", - "recorded_at": "2026-02-11T03:01:06Z", + "recorded_at": "2026-02-10T21:55:33Z", "fixtures": { - "account": {"endpoint": "/account", "recorded_at": "2026-02-11T02:59:20Z"}, - "ssh_keys": {"endpoint": "/ssh-keys", "recorded_at": "2026-02-11T02:59:21Z"}, - "instances": {"endpoint": "/instances", "recorded_at": "2026-02-11T02:59:21Z"}, - "plans": {"endpoint": "/plans", "recorded_at": "2026-02-11T02:59:24Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-11T02:59:25Z"}, - "create_server": {"endpoint": "POST /instances", "type": "live", "recorded_at": "2026-02-11T02:59:27Z"}, - "delete_server": {"endpoint": "DELETE /instances/{id}", "type": "live", "recorded_at": "2026-02-11T03:01:06Z"} + "account": {"endpoint": "/account", "recorded_at": "2026-02-10T21:55:30Z"}, + "ssh_keys": {"endpoint": "/ssh-keys", "recorded_at": "2026-02-10T21:55:31Z"}, + "instances": {"endpoint": "/instances", "recorded_at": "2026-02-10T21:55:31Z"}, + "plans": {"endpoint": "/plans", "recorded_at": "2026-02-10T21:55:33Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-10T21:55:33Z"} } } diff --git a/test/fixtures/vultr/account.json b/test/fixtures/vultr/account.json index d580fc601..f22b4a562 100644 --- a/test/fixtures/vultr/account.json +++ b/test/fixtures/vultr/account.json @@ -23,17 +23,17 @@ "address2": "", "balance": 0, "city": "San Jose", - "company_industry_id": "34", - "company_info": "tests please let me run it", - "company_name": "test", - "company_size_id": "3", + "company_industry_id": "0", + "company_info": "", + "company_name": "", + "company_size_id": 0, "company_url": "", "country": "US", "email": "ahmed@abushagur.com", - "interest": "DevOps", + "interest": "", "name": "Ahmed Abushagur", "org_name": "", - "pending_charges": 0.1, + "pending_charges": 0, "prepayment_remaining": "0.00", "state": "CA" } diff --git a/test/fixtures/vultr/plans.json b/test/fixtures/vultr/plans.json index 1e72f225f..d4382630c 100644 --- a/test/fixtures/vultr/plans.json +++ b/test/fixtures/vultr/plans.json @@ -495,14 +495,7 @@ "hourly_cost_preemptible": 0.438, "id": "vc2-16c-64gb", "invoice_type": "monthly", - "location_cost": { - "sao": { - "hourly_cost": 0.658, - "hourly_cost_preemptible": 0.658, - "monthly_cost": 480, - "monthly_cost_preemptible": 480 - } - }, + "location_cost": {}, "locations": [ "ewr", "ord", @@ -525,7 +518,6 @@ "sgp", "sto", "mex", - "sao", "mel", "bom", "jnb", @@ -5370,7 +5362,8 @@ "invoice_type": "hourly", "location_cost": {}, "locations": [ - "ewr" + "ewr", + "nrt" ], "monthly_cost": 315, "monthly_cost_preemptible": 315, diff --git a/test/record.sh b/test/record.sh index b60bec949..9f52bc90f 100644 --- a/test/record.sh +++ b/test/record.sh @@ -79,9 +79,7 @@ get_endpoints() { printf '%s\n' \ "regions:/regions" \ "instances:/instances" \ - "sshkeys:/sshkeys" \ - "networks:/networks" \ - "disk_images:/disk_images" + "ssh_keys:/sshkeys" ;; upcloud) printf '%s\n' \ @@ -167,23 +165,16 @@ try_load_config() { # OVH uses separate config with multiple fields if [[ "$cloud" == "ovh" ]]; then if [[ -f "$config_file" ]]; then - local ovh_vals - ovh_vals=$(python3 -c " + eval "$(python3 -c " import json, sys try: d = json.load(open(sys.argv[1])) - # Output tab-separated values in fixed order - print('\t'.join(d.get(k, '') for k in ['application_key', 'application_secret', 'consumer_key', 'project_id'])) -except: print('\t\t\t') -" "$config_file" 2>/dev/null) || true - if [[ -n "${ovh_vals:-}" ]]; then - local IFS=$'\t' - read -r ak as ck pid <<< "$ovh_vals" - [[ -n "${ak:-}" ]] && export OVH_APPLICATION_KEY="$ak" - [[ -n "${as:-}" ]] && export OVH_APPLICATION_SECRET="$as" - [[ -n "${ck:-}" ]] && export OVH_CONSUMER_KEY="$ck" - [[ -n "${pid:-}" ]] && export OVH_PROJECT_ID="$pid" - fi + for k, e in [('application_key','OVH_APPLICATION_KEY'), ('application_secret','OVH_APPLICATION_SECRET'), + ('consumer_key','OVH_CONSUMER_KEY'), ('project_id','OVH_PROJECT_ID')]: + v = d.get(k, '') + if v: print(f'export {e}=\"{v}\"') +except: pass +" "$config_file" 2>/dev/null)" || true fi return 0 fi @@ -233,29 +224,29 @@ save_config() { case "$cloud" in ovh) python3 -c " -import json, sys -d = {'application_key': sys.argv[1], 'application_secret': sys.argv[2], - 'consumer_key': sys.argv[3], 'project_id': sys.argv[4]} +import json +d = {'application_key': '${OVH_APPLICATION_KEY:-}', 'application_secret': '${OVH_APPLICATION_SECRET:-}', + 'consumer_key': '${OVH_CONSUMER_KEY:-}', 'project_id': '${OVH_PROJECT_ID:-}'} print(json.dumps(d, indent=2)) -" "${OVH_APPLICATION_KEY:-}" "${OVH_APPLICATION_SECRET:-}" "${OVH_CONSUMER_KEY:-}" "${OVH_PROJECT_ID:-}" > "$config_file" +" > "$config_file" ;; upcloud) python3 -c " -import json, sys -print(json.dumps({'username': sys.argv[1], 'password': sys.argv[2]}, indent=2)) -" "${UPCLOUD_USERNAME:-}" "${UPCLOUD_PASSWORD:-}" > "$config_file" +import json +print(json.dumps({'username': '${UPCLOUD_USERNAME:-}', 'password': '${UPCLOUD_PASSWORD:-}'}, indent=2)) +" > "$config_file" ;; kamatera) python3 -c " -import json, sys -print(json.dumps({'client_id': sys.argv[1], 'secret': sys.argv[2]}, indent=2)) -" "${KAMATERA_API_CLIENT_ID:-}" "${KAMATERA_API_SECRET:-}" > "$config_file" +import json +print(json.dumps({'client_id': '${KAMATERA_API_CLIENT_ID:-}', 'secret': '${KAMATERA_API_SECRET:-}'}, indent=2)) +" > "$config_file" ;; *) local env_var env_var=$(get_auth_env_var "$cloud") eval "local val=\"\${${env_var}:-}\"" - python3 -c "import json, sys; print(json.dumps({'api_key': sys.argv[1]}, indent=2))" "$val" > "$config_file" + python3 -c "import json; print(json.dumps({'api_key': '${val}'}, indent=2))" > "$config_file" ;; esac printf '%b\n' " ${GREEN}saved${NC} → ${config_file}" @@ -335,7 +326,7 @@ has_api_error() { echo "$response" | python3 -c " import json, sys d = json.loads(sys.stdin.read()) -cloud = sys.argv[1] +cloud = '$cloud' if cloud == 'hetzner': err = d.get('error') @@ -360,7 +351,7 @@ elif cloud == 'latitude': sys.exit(0 if 'error' in d or ('errors' in d and d['errors']) else 1) else: sys.exit(1) -" "$cloud" 2>/dev/null +" 2>/dev/null } # --- Pretty print JSON --- @@ -376,16 +367,13 @@ _record_live_cycle() { local cloud="$1" local fixture_dir="$2" - # Source cloud lib so API wrappers are available (dynamic scoping - # lets _live_* functions update caller's counters/metadata) - source "${REPO_ROOT}/${cloud}/lib/common.sh" 2>/dev/null || true - case "$cloud" in - hetzner) _live_hetzner "$fixture_dir" ;; - digitalocean) _live_digitalocean "$fixture_dir" ;; - vultr) _live_vultr "$fixture_dir" ;; - linode) _live_linode "$fixture_dir" ;; - civo) _live_civo "$fixture_dir" ;; + hetzner) + # Source cloud lib so API wrappers are available (dynamic scoping + # lets _live_hetzner update caller's counters/metadata) + source "${REPO_ROOT}/${cloud}/lib/common.sh" 2>/dev/null || true + _live_hetzner "$fixture_dir" + ;; *) return 0 ;; # No live cycle for this cloud yet esac } @@ -409,13 +397,6 @@ _save_live_fixture() { return 1 fi - # Check for API error responses (cloud var is available via dynamic scoping) - if has_api_error "$cloud" "$response"; then - printf '%b\n' " ${RED}fail${NC} ${fixture_name} — API error response" - cloud_errors=$((cloud_errors + 1)) - return 1 - fi - echo "$response" | pretty_json > "${fixture_dir}/${fixture_name}.json" printf '%b\n' " ${GREEN} ok${NC} ${fixture_name} (live)" @@ -660,79 +641,98 @@ _live_civo() { } # --- Record one cloud --- -# Check credentials and prompt if needed; returns 1 to skip this cloud -_record_ensure_credentials() { +record_cloud() { local cloud="$1" - if has_credentials "$cloud"; then - return 0 - fi - local env_var - env_var=$(get_auth_env_var "$cloud") - if [[ "$PROMPT_FOR_CREDS" == "true" ]]; then - printf '%b\n' "${CYAN}━━━ ${cloud} ━━━${NC}" - printf '%b\n' " ${YELLOW}missing${NC} ${env_var}" - if prompt_credentials "$cloud"; then + if ! has_credentials "$cloud"; then + local env_var + env_var=$(get_auth_env_var "$cloud") + if [[ "$PROMPT_FOR_CREDS" == "true" ]]; then + printf '%b\n' "${CYAN}━━━ ${cloud} ━━━${NC}" + printf '%b\n' " ${YELLOW}missing${NC} ${env_var}" + if ! prompt_credentials "$cloud"; then + printf '%b\n' " ${YELLOW}skip${NC} ${cloud}" + SKIPPED=$((SKIPPED + 1)) + return 0 + fi + else + printf '%b\n' " ${YELLOW}skip${NC} ${cloud} — ${env_var} not set" + SKIPPED=$((SKIPPED + 1)) return 0 fi - printf '%b\n' " ${YELLOW}skip${NC} ${cloud}" - else - printf '%b\n' " ${YELLOW}skip${NC} ${cloud} — ${env_var} not set" fi - SKIPPED=$((SKIPPED + 1)) - return 1 -} -# Record a single endpoint fixture; increments cloud_recorded/cloud_errors -# Usage: _record_endpoint CLOUD FIXTURE_DIR FIXTURE_NAME ENDPOINT -_record_endpoint() { - local cloud="$1" fixture_dir="$2" fixture_name="$3" endpoint="$4" + printf '%b\n' "${CYAN}━━━ Recording ${cloud} ━━━${NC}" - # Call API in a subshell that sources the cloud lib - local tmp_response - tmp_response=$(mktemp /tmp/spawn-record-XXXXXX) + # Create fixture directory + local fixture_dir="${FIXTURES_DIR}/${cloud}" + mkdir -p "$fixture_dir" - ( - source "${REPO_ROOT}/${cloud}/lib/common.sh" 2>/dev/null - call_api "$cloud" "$endpoint" 2>/dev/null - ) > "$tmp_response" 2>/dev/null || true + # Source the cloud's lib in a subshell to avoid namespace collisions + # Capture results via temp files + local endpoints + endpoints=$(get_endpoints "$cloud") - local response - response=$(cat "$tmp_response") - rm -f "$tmp_response" + local cloud_recorded=0 + local cloud_errors=0 + local metadata_entries="" - if [[ -z "$response" ]]; then - printf '%b\n' " ${RED}fail${NC} ${fixture_name} — empty response" - cloud_errors=$((cloud_errors + 1)) - return 0 - fi + while IFS=: read -r fixture_name endpoint; do + [[ -z "$fixture_name" ]] && continue - if ! echo "$response" | is_valid_json; then - printf '%b\n' " ${RED}fail${NC} ${fixture_name} — invalid JSON" - cloud_errors=$((cloud_errors + 1)) - return 0 - fi + local response="" + local record_ok=false - if has_api_error "$cloud" "$response"; then - printf '%b\n' " ${RED}fail${NC} ${fixture_name} — API error response" - cloud_errors=$((cloud_errors + 1)) - return 0 - fi + # Call API in a subshell that sources the cloud lib + local tmp_response + tmp_response=$(mktemp /tmp/spawn-record-XXXXXX) - echo "$response" | pretty_json > "${fixture_dir}/${fixture_name}.json" - printf '%b\n' " ${GREEN} ok${NC} ${fixture_name} → fixtures/${cloud}/${fixture_name}.json" - cloud_recorded=$((cloud_recorded + 1)) + ( + # Source cloud lib (this also sources shared/common.sh) + source "${REPO_ROOT}/${cloud}/lib/common.sh" 2>/dev/null + + # Suppress any interactive prompts by ensuring tokens are loaded + # The API call itself uses env vars directly + call_api "$cloud" "$endpoint" 2>/dev/null + ) > "$tmp_response" 2>/dev/null || true - local timestamp - timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - metadata_entries="${metadata_entries} \"${fixture_name}\": {\"endpoint\": \"${endpoint}\", \"recorded_at\": \"${timestamp}\"}, + response=$(cat "$tmp_response") + rm -f "$tmp_response" + + if [[ -z "$response" ]]; then + printf '%b\n' " ${RED}fail${NC} ${fixture_name} — empty response" + cloud_errors=$((cloud_errors + 1)) + continue + fi + + if ! echo "$response" | is_valid_json; then + printf '%b\n' " ${RED}fail${NC} ${fixture_name} — invalid JSON" + cloud_errors=$((cloud_errors + 1)) + continue + fi + + if has_api_error "$cloud" "$response"; then + printf '%b\n' " ${RED}fail${NC} ${fixture_name} — API error response" + cloud_errors=$((cloud_errors + 1)) + continue + fi + + # Save pretty-printed fixture + echo "$response" | pretty_json > "${fixture_dir}/${fixture_name}.json" + printf '%b\n' " ${GREEN} ok${NC} ${fixture_name} → fixtures/${cloud}/${fixture_name}.json" + cloud_recorded=$((cloud_recorded + 1)) + + # Build metadata entry + local timestamp + timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + metadata_entries="${metadata_entries} \"${fixture_name}\": {\"endpoint\": \"${endpoint}\", \"recorded_at\": \"${timestamp}\"}, " -} + done <<< "$endpoints" -# Write the _metadata.json file for a cloud's fixtures -_record_write_metadata() { - local cloud="$1" fixture_dir="$2" + # --- Live create+delete cycle for write endpoint fixtures --- + _record_live_cycle "$cloud" "$fixture_dir" cloud_recorded cloud_errors metadata_entries + # Write metadata local meta_timestamp meta_timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ") @@ -748,34 +748,6 @@ ${metadata_entries} } } METADATA_EOF -} - -record_cloud() { - local cloud="$1" - - _record_ensure_credentials "$cloud" || return 0 - - printf '%b\n' "${CYAN}━━━ Recording ${cloud} ━━━${NC}" - - local fixture_dir="${FIXTURES_DIR}/${cloud}" - mkdir -p "$fixture_dir" - - local endpoints - endpoints=$(get_endpoints "$cloud") - - local cloud_recorded=0 - local cloud_errors=0 - local metadata_entries="" - - while IFS=: read -r fixture_name endpoint; do - [[ -z "$fixture_name" ]] && continue - _record_endpoint "$cloud" "$fixture_dir" "$fixture_name" "$endpoint" - done <<< "$endpoints" - - # Live create+delete cycle for write endpoint fixtures - _record_live_cycle "$cloud" "$fixture_dir" cloud_recorded cloud_errors metadata_entries || true - - _record_write_metadata "$cloud" "$fixture_dir" RECORDED=$((RECORDED + cloud_recorded)) ERRORS=$((ERRORS + cloud_errors)) diff --git a/test/run.sh b/test/run.sh index 45443e739..578666cd7 100644 --- a/test/run.sh +++ b/test/run.sh @@ -15,7 +15,7 @@ # bash test/run.sh claude # test one script # bash test/run.sh --remote # test remote source (from GitHub) -set -eo pipefail +set -uo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" TEST_DIR=$(mktemp -d) @@ -50,18 +50,8 @@ echo "sprite $*" >> "${MOCK_LOG}" case "$1" in org) exit 0 ;; # auth check passes - list) - echo "existing-sprite" - # After create, also return the test sprite name so provisioning poll succeeds - if [[ -f "/tmp/sprite_mock_created_$$" ]] || [[ -f "/tmp/sprite_mock_created" ]]; then - echo "${SPRITE_NAME:-}" - fi - exit 0 - ;; - create) - touch "/tmp/sprite_mock_created_$$" "/tmp/sprite_mock_created" - exit 0 - ;; + list) echo "existing-sprite"; exit 0 ;; # list returns no match for test sprite + create) exit 0 ;; exec) # If there's a -file flag, just pretend to upload if [[ "$*" == *"-file"* ]]; then @@ -105,12 +95,12 @@ assert_contains() { local file="$1" pattern="$2" msg="$3" if grep -qE "${pattern}" "${file}" 2>/dev/null; then printf '%b\n' " ${GREEN}✓${NC} ${msg}" - PASSED=$((PASSED + 1)) + ((PASSED++)) else printf '%b\n' " ${RED}✗${NC} ${msg}" printf '%b\n' " expected pattern: ${pattern}" printf '%b\n' " in: ${file}" - FAILED=$((FAILED + 1)) + ((FAILED++)) fi } @@ -118,10 +108,10 @@ assert_not_contains() { local file="$1" pattern="$2" msg="$3" if ! grep -qE "${pattern}" "${file}" 2>/dev/null; then printf '%b\n' " ${GREEN}✓${NC} ${msg}" - PASSED=$((PASSED + 1)) + ((PASSED++)) else printf '%b\n' " ${RED}✗${NC} ${msg}" - FAILED=$((FAILED + 1)) + ((FAILED++)) fi } @@ -129,88 +119,48 @@ assert_exit_code() { local actual="$1" expected="$2" msg="$3" if [[ "${actual}" -eq "${expected}" ]]; then printf '%b\n' " ${GREEN}✓${NC} ${msg}" - PASSED=$((PASSED + 1)) + ((PASSED++)) else printf '%b\n' " ${RED}✗${NC} ${msg} (got exit code ${actual}, expected ${expected})" - FAILED=$((FAILED + 1)) + ((FAILED++)) fi } -# Assert that a value equals an expected string -# Usage: assert_equals ACTUAL EXPECTED MSG -assert_equals() { - local actual="$1" expected="$2" msg="$3" - if [[ "${actual}" == "${expected}" ]]; then - printf '%b\n' " ${GREEN}✓${NC} ${msg}" - PASSED=$((PASSED + 1)) - else - printf '%b\n' " ${RED}✗${NC} ${msg} (got '${actual}')" - FAILED=$((FAILED + 1)) - fi -} +# --- Test runner for a single script --- +run_script_test() { + local script_name="$1" + local script_path="${REPO_ROOT}/sprite/${script_name}.sh" + local output_file="${TEST_DIR}/${script_name}_output.log" -# Assert that a value contains a substring pattern (glob match) -# Usage: assert_match ACTUAL PATTERN MSG -# PATTERN uses glob syntax: *substring* for contains, prefix* for starts-with, etc. -assert_match() { - local actual="$1" pattern="$2" msg="$3" - # Use a case statement for glob matching (compatible with bash 3.x) - case "${actual}" in - ${pattern}) - printf '%b\n' " ${GREEN}✓${NC} ${msg}" - PASSED=$((PASSED + 1)) - ;; - *) - printf '%b\n' " ${RED}✗${NC} ${msg} (got '${actual}')" - FAILED=$((FAILED + 1)) - ;; - esac -} + echo "" + printf '%b\n' "${YELLOW}━━━ Testing ${script_name}.sh ━━━${NC}" -# Run a shared/common.sh function and assert it succeeds (exit 0) -assert_common_succeeds() { - local msg="$1" cmd="$2" - local result - result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && '"${cmd}" 2>/dev/null) - if [[ "${result}" == "valid" ]]; then - printf '%b\n' " ${GREEN}✓${NC} ${msg}" - PASSED=$((PASSED + 1)) - else - printf '%b\n' " ${RED}✗${NC} ${msg}" - FAILED=$((FAILED + 1)) - fi -} + # Reset mock log + : > "${MOCK_LOG}" -# Run a shared/common.sh function and assert it fails (exit non-zero) -assert_common_fails() { - local msg="$1" cmd="$2" - local rc=0 - bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && '"${cmd}" /dev/null 2>&1 || rc=$? - if [[ "${rc}" -ne 0 ]]; then - printf '%b\n' " ${GREEN}✓${NC} ${msg}" - PASSED=$((PASSED + 1)) - else - printf '%b\n' " ${RED}✗${NC} ${msg}" - FAILED=$((FAILED + 1)) - fi -} + # Run the script with mocked PATH and env vars (timeout 30s) + local exit_code=0 + MOCK_LOG="${MOCK_LOG}" \ + SPRITE_NAME="test-sprite-${script_name}" \ + OPENROUTER_API_KEY="sk-or-v1-0000000000000000000000000000000000000000000000000000000000000000" \ + PATH="${TEST_DIR}:${PATH}" \ + timeout 30 bash "${script_path}" > "${output_file}" 2>&1 || exit_code=$? -# --- Sprite command assertions --- -# Assert that a sprite script follows the standard command lifecycle: -# auth check -> list -> create -> exec -> env upload -> interactive launch -_assert_sprite_common_commands() { - local script_name="$1" + assert_exit_code "${exit_code}" 0 "Script exits successfully" + + # Common assertions for all scripts assert_contains "${MOCK_LOG}" "sprite org list" "Checks sprite authentication" assert_contains "${MOCK_LOG}" "sprite list" "Checks if sprite exists" assert_contains "${MOCK_LOG}" "sprite create.*test-sprite-${script_name}" "Creates sprite with correct name" assert_contains "${MOCK_LOG}" "sprite exec.*test-sprite-${script_name}" "Runs commands on sprite" + + # Check env var injection (temp file upload) assert_contains "${MOCK_LOG}" "sprite exec.*-file.*/tmp/env_config" "Uploads env config to sprite" + + # Check final interactive launch (flag order varies: -s NAME -tty or -tty -s NAME) assert_contains "${MOCK_LOG}" "sprite exec.*-tty.*" "Launches interactive session" -} -# Assert that a sprite script installs agent-specific components -_assert_agent_specific() { - local script_name="$1" + # Script-specific assertions case "${script_name}" in claude) assert_contains "${MOCK_LOG}" "sprite exec.*claude.*install" "Installs Claude Code" @@ -225,49 +175,26 @@ _assert_agent_specific() { assert_contains "${MOCK_LOG}" "sprite exec.*git.*nanoclaw" "Clones nanoclaw repo" assert_contains "${MOCK_LOG}" "sprite exec.*-file.*/tmp/nanoclaw_env" "Uploads nanoclaw .env" ;; + *) + # No agent-specific assertions for other agents + ;; esac -} -# Assert no temp files were leaked during script execution -_assert_no_temp_leaks() { + # Check no temp files leaked local leaked_temps leaked_temps=$(find /tmp -maxdepth 1 -name "tmp.*" -newer "${MOCK_LOG}" 2>/dev/null | wc -l) if [[ "${leaked_temps}" -eq 0 ]]; then printf '%b\n' " ${GREEN}✓${NC} No temp files leaked" - PASSED=$((PASSED + 1)) + ((PASSED++)) fi } -# --- Test runner for a single script --- -run_script_test() { - local script_name="$1" - local script_path="${REPO_ROOT}/sprite/${script_name}.sh" - local output_file="${TEST_DIR}/${script_name}_output.log" - +# --- Test common.sh sourcing --- +test_common_source() { echo "" - printf '%b\n' "${YELLOW}━━━ Testing ${script_name}.sh ━━━${NC}" - - # Reset mock state - : > "${MOCK_LOG}" - rm -f /tmp/sprite_mock_created_* /tmp/sprite_mock_created 2>/dev/null || true - - # Run the script with mocked PATH and env vars (timeout 30s) - local exit_code=0 - MOCK_LOG="${MOCK_LOG}" \ - SPRITE_NAME="test-sprite-${script_name}" \ - OPENROUTER_API_KEY="sk-or-v1-0000000000000000000000000000000000000000000000000000000000000000" \ - PATH="${TEST_DIR}:${PATH}" \ - timeout 30 bash "${script_path}" > "${output_file}" 2>&1 || exit_code=$? - - assert_exit_code "${exit_code}" 0 "Script exits successfully" - _assert_sprite_common_commands "${script_name}" - _assert_agent_specific "${script_name}" - _assert_no_temp_leaks -} + printf '%b\n' "${YELLOW}━━━ Testing common.sh ━━━${NC}" -# --- Test common.sh sourcing --- -_test_sprite_functions_and_syntax() { - # Source locally and check all functions exist + # Test 1: Source locally and check all functions exist local output output=$(bash -c ' source "'"${REPO_ROOT}"'/sprite/lib/common.sh" @@ -283,126 +210,167 @@ _test_sprite_functions_and_syntax() { local missing missing=$(echo "${output}" | grep "^MISSING:" || true) - assert_equals "${missing}" "" "All functions defined" - - # Syntax check - local rc=0 - bash -n "${REPO_ROOT}/sprite/lib/common.sh" 2>/dev/null || rc=$? - assert_exit_code "${rc}" 0 "Syntax valid" -} + if [[ -z "${missing}" ]]; then + printf '%b\n' " ${GREEN}✓${NC} All functions defined" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} Missing functions: ${missing}" + ((FAILED++)) + fi -_test_sprite_log_and_name() { - # log functions write to stderr, not stdout + # Test 2: log functions write to stderr, not stdout local stdout stderr stdout=$(timeout 5 bash -c 'source "'"${REPO_ROOT}"'/sprite/lib/common.sh" && log_info "test"' /dev/null) stderr=$(timeout 5 bash -c 'source "'"${REPO_ROOT}"'/sprite/lib/common.sh" && log_info "test"' &1 >/dev/null) - assert_equals "${stdout}" "" "Log functions write to stderr (no stdout)" - assert_match "${stderr}" "?*" "Log functions produce stderr output" + if [[ -z "${stdout}" && -n "${stderr}" ]]; then + printf '%b\n' " ${GREEN}✓${NC} Log functions write to stderr" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} Log functions should write to stderr only" + ((FAILED++)) + fi - # get_sprite_name uses SPRITE_NAME env var + # Test 3: get_sprite_name uses SPRITE_NAME env var local name name=$(timeout 5 bash -c 'SPRITE_NAME=from-env; source "'"${REPO_ROOT}"'/sprite/lib/common.sh" && get_sprite_name' 2>/dev/null) - assert_equals "${name}" "from-env" "get_sprite_name reads SPRITE_NAME env var" + if [[ "${name}" == "from-env" ]]; then + printf '%b\n' " ${GREEN}✓${NC} get_sprite_name reads SPRITE_NAME env var" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} get_sprite_name should return 'from-env', got '${name}'" + ((FAILED++)) + fi - # get_sprite_name fails gracefully without TTY or env var + # Test 4: get_sprite_name fails gracefully without TTY or env var local rc=0 timeout 5 bash -c 'SPRITE_NAME=""; source "'"${REPO_ROOT}"'/sprite/lib/common.sh" && get_sprite_name' /dev/null 2>&1 || rc=$? - assert_match "${rc}" "[1-9]*" "get_sprite_name fails without TTY or env var" -} - -_test_sprite_remote_source() { - if [[ "${REMOTE}" != true ]]; then - return 0 + if [[ "${rc}" -ne 0 ]]; then + printf '%b\n' " ${GREEN}✓${NC} get_sprite_name fails without TTY or env var" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} get_sprite_name should fail without input" + ((FAILED++)) fi - local remote_fns - remote_fns=$(bash -c ' - eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/sprite/lib/common.sh)" - type log_info &>/dev/null && echo "OK" || echo "FAIL" - ' 2>/dev/null) - assert_equals "${remote_fns}" "OK" "Remote source from GitHub works" -} -test_common_source() { - echo "" - printf '%b\n' "${YELLOW}━━━ Testing common.sh ━━━${NC}" + # Test 5: Syntax check + if bash -n "${REPO_ROOT}/sprite/lib/common.sh" 2>/dev/null; then + printf '%b\n' " ${GREEN}✓${NC} Syntax valid" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} Syntax errors" + ((FAILED++)) + fi - _test_sprite_functions_and_syntax - _test_sprite_log_and_name - _test_sprite_remote_source + # Test 6: Remote source (if --remote) + if [[ "${REMOTE}" == true ]]; then + local remote_fns + remote_fns=$(bash -c ' + source <(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/sprite/lib/common.sh) + type log_info &>/dev/null && echo "OK" || echo "FAIL" + ' 2>/dev/null) + if [[ "${remote_fns}" == "OK" ]]; then + printf '%b\n' " ${GREEN}✓${NC} Remote source from GitHub works" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} Remote source from GitHub failed" + ((FAILED++)) + fi + fi } # --- Test shared/common.sh functions --- -# --- shared/common.sh sub-tests (grouped by feature) --- - -_test_model_validation() { - assert_common_succeeds "validate_model_id accepts valid model IDs" \ - 'validate_model_id "anthropic/claude-3.5-sonnet" && echo "valid"' - assert_common_fails "validate_model_id rejects invalid characters" \ - 'validate_model_id "bad;model"' - assert_common_succeeds "validate_model_id accepts empty string" \ - 'validate_model_id "" && echo "valid"' - assert_common_succeeds "validate_model_id accepts openrouter/auto" \ - 'validate_model_id "openrouter/auto" && echo "valid"' - assert_common_succeeds "validate_model_id accepts model IDs with colons" \ - 'validate_model_id "provider/model:version" && echo "valid"' - - # Bulk test: all shell metacharacters must be rejected - # Note: backtick excluded due to shell escaping complexity - local dangerous_chars=('$' '&' '|' '>' '<' '(' ')' '{' '}' ';' '*' '?' '[' ']') - local rejected_count=0 - local rc - for char in "${dangerous_chars[@]}"; do - rc=0 - local test_str - test_str=$(printf 'bad%smodel' "${char}") - bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_model_id '"$(printf '%q' "${test_str}")" /dev/null 2>&1 || rc=$? - [[ "${rc}" -ne 0 ]] && rejected_count=$((rejected_count + 1)) - done - assert_equals "${rejected_count}" "${#dangerous_chars[@]}" \ - "validate_model_id rejects shell metacharacters (${rejected_count}/${#dangerous_chars[@]})" -} +test_shared_common() { + echo "" + printf '%b\n' "${YELLOW}━━━ Testing shared/common.sh ━━━${NC}" -_test_json_escape() { + # Test 1: validate_model_id accepts valid model IDs local result + result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_model_id "anthropic/claude-3.5-sonnet" && echo "valid"' 2>/dev/null) + if [[ "${result}" == "valid" ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_model_id accepts valid model IDs" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} validate_model_id should accept 'anthropic/claude-3.5-sonnet'" + ((FAILED++)) + fi + + # Test 2: validate_model_id rejects invalid characters + local rc=0 + bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_model_id "bad;model"' /dev/null 2>&1 || rc=$? + if [[ "${rc}" -ne 0 ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_model_id rejects invalid characters" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} validate_model_id should reject 'bad;model'" + ((FAILED++)) + fi + + # Test 3: validate_model_id accepts empty string + result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_model_id "" && echo "valid"' 2>/dev/null) + if [[ "${result}" == "valid" ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_model_id accepts empty string" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} validate_model_id should accept empty string" + ((FAILED++)) + fi + + # Test 4: json_escape handles special characters result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && json_escape "test\"quote"' 2>/dev/null) - # json_escape should produce escaped quotes (\\") in the output - assert_match "${result}" '*\\"*' "json_escape handles special characters" -} + if [[ "${result}" == *'\\"'* ]] || [[ "${result}" == *'\"'* ]]; then + printf '%b\n' " ${GREEN}✓${NC} json_escape handles special characters" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} json_escape should escape quotes" + ((FAILED++)) + fi -_test_ssh_key_utils() { - # generate_ssh_key_if_missing - creates key + # Test 5: generate_ssh_key_if_missing creates key if missing local test_key="${TEST_DIR}/test_id_ed25519" bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && generate_ssh_key_if_missing "'"${test_key}"'"' >/dev/null 2>&1 - local key_exists="no" - [[ -f "${test_key}" && -f "${test_key}.pub" ]] && key_exists="yes" - assert_equals "${key_exists}" "yes" "generate_ssh_key_if_missing creates key" + if [[ -f "${test_key}" && -f "${test_key}.pub" ]]; then + printf '%b\n' " ${GREEN}✓${NC} generate_ssh_key_if_missing creates key" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} generate_ssh_key_if_missing should create key at ${test_key}" + ((FAILED++)) + fi - # generate_ssh_key_if_missing - skips existing + # Test 6: generate_ssh_key_if_missing skips if key exists local mtime_before mtime_before=$(stat -c %Y "${test_key}" 2>/dev/null || stat -f %m "${test_key}" 2>/dev/null) sleep 1 bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && generate_ssh_key_if_missing "'"${test_key}"'"' >/dev/null 2>&1 local mtime_after mtime_after=$(stat -c %Y "${test_key}" 2>/dev/null || stat -f %m "${test_key}" 2>/dev/null) - assert_equals "${mtime_before}" "${mtime_after}" "generate_ssh_key_if_missing skips existing key" + if [[ "${mtime_before}" == "${mtime_after}" ]]; then + printf '%b\n' " ${GREEN}✓${NC} generate_ssh_key_if_missing skips existing key" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} generate_ssh_key_if_missing should not recreate existing key" + ((FAILED++)) + fi - # get_ssh_fingerprint - local result + # Test 7: get_ssh_fingerprint returns fingerprint result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && get_ssh_fingerprint "'"${test_key}.pub"'"' 2>/dev/null) - assert_match "${result}" "*:*" "get_ssh_fingerprint returns valid fingerprint" - - # extract_ssh_key_ids - local mock_json='{"ssh_keys":[{"id":123},{"id":456}]}' - result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && echo '"'${mock_json}'"' | extract_ssh_key_ids "$(cat)" "ssh_keys"' 2>/dev/null) - assert_match "${result}" "*123*456*" "extract_ssh_key_ids parses JSON correctly" -} + if [[ -n "${result}" && "${result}" =~ ^[a-f0-9:]+$ ]]; then + printf '%b\n' " ${GREEN}✓${NC} get_ssh_fingerprint returns valid fingerprint" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} get_ssh_fingerprint should return hex fingerprint, got '${result}'" + ((FAILED++)) + fi -_test_syntax_and_logging() { - local rc=0 - bash -n "${REPO_ROOT}/shared/common.sh" 2>/dev/null || rc=$? - assert_exit_code "${rc}" 0 "shared/common.sh syntax valid" + # Test 8: Syntax check for shared/common.sh + if bash -n "${REPO_ROOT}/shared/common.sh" 2>/dev/null; then + printf '%b\n' " ${GREEN}✓${NC} shared/common.sh syntax valid" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} shared/common.sh has syntax errors" + ((FAILED++)) + fi - local output missing + # Test 9: All logging functions exist in shared/common.sh output=$(bash -c ' source "'"${REPO_ROOT}"'/shared/common.sh" for fn in log_info log_warn log_error; do @@ -410,34 +378,62 @@ _test_syntax_and_logging() { done ' 2>/dev/null) missing=$(echo "${output}" | grep "^MISSING:" || true) - assert_equals "${missing}" "" "All logging functions exist in shared/common.sh" -} + if [[ -z "${missing}" ]]; then + printf '%b\n' " ${GREEN}✓${NC} All logging functions exist in shared/common.sh" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} Missing logging functions: ${missing}" + ((FAILED++)) + fi -_test_open_browser() { - # open_browser: termux - local result + # Test 10: extract_ssh_key_ids parses JSON correctly + local mock_json='{"ssh_keys":[{"id":123},{"id":456}]}' + result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && echo '"'${mock_json}'"' | extract_ssh_key_ids "$(cat)" "ssh_keys"' 2>/dev/null) + if [[ "${result}" == "[123, 456]" ]] || [[ "${result}" == "[123,456]" ]]; then + printf '%b\n' " ${GREEN}✓${NC} extract_ssh_key_ids parses JSON correctly" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} extract_ssh_key_ids should return [123, 456], got '${result}'" + ((FAILED++)) + fi + + # Test 11: open_browser detects termux-open-url result=$(bash -c ' source "'"${REPO_ROOT}"'/shared/common.sh" termux-open-url() { echo "termux: $*"; } export -f termux-open-url open_browser "https://example.com" ' 2>/dev/null) - assert_equals "${result}" "termux: https://example.com" "open_browser detects termux-open-url" + if [[ "${result}" == "termux: https://example.com" ]]; then + printf '%b\n' " ${GREEN}✓${NC} open_browser detects termux-open-url" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} open_browser should use termux-open-url, got '${result}'" + ((FAILED++)) + fi - # open_browser: macOS open + # Test 14: open_browser detects macOS open result=$(bash -c ' source "'"${REPO_ROOT}"'/shared/common.sh" open() { echo "macOS: $*"; } export -f open open_browser "https://example.com" ' 2>/dev/null) - assert_equals "${result}" "macOS: https://example.com" "open_browser detects macOS open" + if [[ "${result}" == "macOS: https://example.com" ]]; then + printf '%b\n' " ${GREEN}✓${NC} open_browser detects macOS open" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} open_browser should use macOS open, got '${result}'" + ((FAILED++)) + fi - # open_browser: fallback message + # Test 15: open_browser handles missing browsers gracefully local stderr_output stderr_output=$(bash -c ' + # Use minimal PATH to force fallback behavior PATH="/usr/bin:/bin" source "'"${REPO_ROOT}"'/shared/common.sh" + # Mock all browser detection to fail command() { if [[ "$2" == "termux-open-url" || "$2" == "open" || "$2" == "xdg-open" ]]; then return 1 @@ -447,118 +443,256 @@ _test_open_browser() { export -f command open_browser "https://example.com" ' 2>&1 >/dev/null) - assert_match "${stderr_output}" "*Please open: https://example.com*" \ - "open_browser shows fallback message when browsers unavailable" -} + if [[ "${stderr_output}" == *"Please open: https://example.com"* ]]; then + printf '%b\n' " ${GREEN}✓${NC} open_browser shows fallback message when browsers unavailable" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} open_browser should show fallback message, got '${stderr_output}'" + ((FAILED++)) + fi -_test_cloud_init() { - # get_cloud_init_userdata - local result + # Test 16: get_cloud_init_userdata returns valid YAML result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && get_cloud_init_userdata' 2>/dev/null) - assert_match "${result}" "*#cloud-config*" "get_cloud_init_userdata returns valid YAML" - assert_match "${result}" "*curl*" "get_cloud_init_userdata includes curl" - assert_match "${result}" "*git*" "get_cloud_init_userdata includes git" - assert_match "${result}" "*zsh*" "get_cloud_init_userdata includes zsh" - assert_match "${result}" "*bun.sh/install*" "get_cloud_init_userdata includes Bun installation" - assert_match "${result}" "*claude.ai/install*" "get_cloud_init_userdata includes Claude installation" - - # check_openrouter_connectivity -- accepts success or graceful failure - if command -v curl &> /dev/null; then - local connectivity_result - connectivity_result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && check_openrouter_connectivity && echo "reachable"' 2>/dev/null) - # Accept both "reachable" and empty (network unavailable) -- just shouldn't crash - assert_match "${connectivity_result:-ok}" "*" "check_openrouter_connectivity handles connectivity check" + if [[ "${result}" == *"#cloud-config"* ]] && [[ "${result}" == *"package_update"* ]]; then + printf '%b\n' " ${GREEN}✓${NC} get_cloud_init_userdata returns valid YAML" + ((PASSED++)) else - printf '%b\n' " ${YELLOW}⚠${NC} check_openrouter_connectivity test skipped (curl not available)" + printf '%b\n' " ${RED}✗${NC} get_cloud_init_userdata should return cloud-init YAML" + ((FAILED++)) + fi + + # Test 17: get_cloud_init_userdata includes required packages + if [[ "${result}" == *"curl"* ]] && [[ "${result}" == *"git"* ]] && [[ "${result}" == *"zsh"* ]]; then + printf '%b\n' " ${GREEN}✓${NC} get_cloud_init_userdata includes required packages" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} get_cloud_init_userdata should include curl, git, zsh" + ((FAILED++)) fi -} -_test_oauth_functions() { - local rc + # Test 18: get_cloud_init_userdata includes Bun and Claude installation + if [[ "${result}" == *"bun.sh/install"* ]] && [[ "${result}" == *"claude.ai/install"* ]]; then + printf '%b\n' " ${GREEN}✓${NC} get_cloud_init_userdata includes Bun and Claude installation" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} get_cloud_init_userdata should include Bun and Claude install" + ((FAILED++)) + fi - # wait_for_oauth_code - success + # Test 19: wait_for_oauth_code returns success when file exists local code_test_file="${TEST_DIR}/oauth_code_test" echo "test_code" > "${code_test_file}" rc=0 bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && wait_for_oauth_code "'"${code_test_file}"'" 1' >/dev/null 2>&1 || rc=$? - assert_exit_code "${rc}" 0 "wait_for_oauth_code returns success when file exists" + if [[ "${rc}" -eq 0 ]]; then + printf '%b\n' " ${GREEN}✓${NC} wait_for_oauth_code returns success when file exists" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} wait_for_oauth_code should return 0 when file exists" + ((FAILED++)) + fi - # wait_for_oauth_code - timeout + # Test 21: wait_for_oauth_code returns failure on timeout local missing_file="${TEST_DIR}/missing_oauth_code" rc=0 bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && wait_for_oauth_code "'"${missing_file}"'" 1' >/dev/null 2>&1 || rc=$? - assert_match "${rc}" "[1-9]*" "wait_for_oauth_code returns failure on timeout" + if [[ "${rc}" -ne 0 ]]; then + printf '%b\n' " ${GREEN}✓${NC} wait_for_oauth_code returns failure on timeout" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} wait_for_oauth_code should return non-zero on timeout" + ((FAILED++)) + fi - # cleanup_oauth_session + # Test 22: cleanup_oauth_session removes directory local cleanup_test_dir="${TEST_DIR}/oauth_cleanup_test" mkdir -p "${cleanup_test_dir}" bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && cleanup_oauth_session "" "'"${cleanup_test_dir}"'"' >/dev/null 2>&1 - local dir_removed="yes" - [[ -d "${cleanup_test_dir}" ]] && dir_removed="no" - assert_equals "${dir_removed}" "yes" "cleanup_oauth_session removes directory" -} + if [[ ! -d "${cleanup_test_dir}" ]]; then + printf '%b\n' " ${GREEN}✓${NC} cleanup_oauth_session removes directory" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} cleanup_oauth_session should remove directory" + ((FAILED++)) + fi -_test_ssh_wait() { - # generic_ssh_wait - success - local result + # Test 23: generic_ssh_wait succeeds when command passes result=$(bash -c ' source "'"${REPO_ROOT}"'/shared/common.sh" + # Mock ssh that always succeeds ssh() { return 0; } export -f ssh generic_ssh_wait "root" "1.2.3.4" "-o Test" "true" "test" 2 1 2>&1 echo $? ' 2>/dev/null | tail -1) - assert_equals "${result}" "0" "generic_ssh_wait succeeds when command passes" + if [[ "${result}" == "0" ]]; then + printf '%b\n' " ${GREEN}✓${NC} generic_ssh_wait succeeds when command passes" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} generic_ssh_wait should return 0 on success, got '${result}'" + ((FAILED++)) + fi - # generic_ssh_wait - failure + # Test 24: generic_ssh_wait fails after max attempts result=$(bash -c ' source "'"${REPO_ROOT}"'/shared/common.sh" + # Mock ssh that always fails ssh() { return 1; } export -f ssh generic_ssh_wait "root" "1.2.3.4" "-o Test" "false" "test" 2 1 2>&1 echo $? ' 2>/dev/null | tail -1) - assert_equals "${result}" "1" "generic_ssh_wait fails after max attempts" -} + if [[ "${result}" == "1" ]]; then + printf '%b\n' " ${GREEN}✓${NC} generic_ssh_wait fails after max attempts" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} generic_ssh_wait should return 1 after max attempts, got '${result}'" + ((FAILED++)) + fi + + # Test 25: safe_read fails when no TTY available + rc=0 + timeout 5 bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && safe_read "test: "' /dev/null 2>&1 || rc=$? + if [[ "${rc}" -ne 0 ]]; then + printf '%b\n' " ${GREEN}✓${NC} safe_read fails when no TTY available" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} safe_read should fail without TTY" + ((FAILED++)) + fi + + # Test 26: validate_model_id accepts openrouter/auto + result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_model_id "openrouter/auto" && echo "valid"' 2>/dev/null) + if [[ "${result}" == "valid" ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_model_id accepts openrouter/auto" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} validate_model_id should accept 'openrouter/auto'" + ((FAILED++)) + fi + + # Test 27: validate_model_id accepts model IDs with colons + result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_model_id "provider/model:version" && echo "valid"' 2>/dev/null) + if [[ "${result}" == "valid" ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_model_id accepts model IDs with colons" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} validate_model_id should accept colons in model IDs" + ((FAILED++)) + fi + + # Test 28: validate_model_id rejects shell metacharacters + # Note: backtick excluded due to shell escaping complexity + local dangerous_chars=('$' '&' '|' '>' '<' '(' ')' '{' '}' ';' '*' '?' '[' ']') + local rejected_count=0 + for char in "${dangerous_chars[@]}"; do + rc=0 + # Use printf %q to properly escape the character + local test_str + test_str=$(printf 'bad%smodel' "${char}") + bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_model_id '"$(printf '%q' "${test_str}")" /dev/null 2>&1 || rc=$? + [[ "${rc}" -ne 0 ]] && ((rejected_count++)) + done + if [[ "${rejected_count}" -eq "${#dangerous_chars[@]}" ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_model_id rejects shell metacharacters" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} validate_model_id should reject all shell metacharacters (${rejected_count}/${#dangerous_chars[@]})" + ((FAILED++)) + fi -_test_input_and_server_validation() { - # safe_read without TTY - assert_common_fails "safe_read fails when no TTY available" \ - 'safe_read "test: " /dev/null) + if [[ "${result}" == "valid" ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_server_name accepts valid names" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} validate_server_name should accept 'dev-server-01'" + ((FAILED++)) + fi - # validate_server_name - assert_common_succeeds "validate_server_name accepts valid names" \ - 'validate_server_name "dev-server-01" && echo "valid"' - assert_common_fails "validate_server_name rejects names too short" \ - 'validate_server_name "ab"' + # Test 30: validate_server_name rejects names too short + rc=0 + bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_server_name "ab"' /dev/null 2>&1 || rc=$? + if [[ "${rc}" -ne 0 ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_server_name rejects names too short" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} validate_server_name should reject names < 3 characters" + ((FAILED++)) + fi + # Test 31: validate_server_name rejects names too long + rc=0 local long_name long_name=$(printf 'a%.0s' {1..64}) - assert_common_fails "validate_server_name rejects names too long" \ - 'validate_server_name "'"${long_name}"'"' - assert_common_fails "validate_server_name rejects leading dash" \ - 'validate_server_name "-server"' - assert_common_fails "validate_server_name rejects trailing dash" \ - 'validate_server_name "server-"' - assert_common_fails "validate_server_name rejects invalid characters" \ - 'validate_server_name "server_01"' - assert_common_fails "validate_server_name rejects empty string" \ - 'validate_server_name ""' -} + bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_server_name "'"${long_name}"'"' /dev/null 2>&1 || rc=$? + if [[ "${rc}" -ne 0 ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_server_name rejects names too long" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} validate_server_name should reject names > 63 characters" + ((FAILED++)) + fi -test_shared_common() { - echo "" - printf '%b\n' "${YELLOW}━━━ Testing shared/common.sh ━━━${NC}" + # Test 32: validate_server_name rejects leading dash + rc=0 + bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_server_name "-server"' /dev/null 2>&1 || rc=$? + if [[ "${rc}" -ne 0 ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_server_name rejects leading dash" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} validate_server_name should reject names starting with dash" + ((FAILED++)) + fi + + # Test 33: validate_server_name rejects trailing dash + rc=0 + bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_server_name "server-"' /dev/null 2>&1 || rc=$? + if [[ "${rc}" -ne 0 ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_server_name rejects trailing dash" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} validate_server_name should reject names ending with dash" + ((FAILED++)) + fi + + # Test 34: validate_server_name rejects invalid characters + rc=0 + bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_server_name "server_01"' /dev/null 2>&1 || rc=$? + if [[ "${rc}" -ne 0 ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_server_name rejects invalid characters" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} validate_server_name should reject underscore and special characters" + ((FAILED++)) + fi + + # Test 35: validate_server_name rejects empty string + rc=0 + bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_server_name ""' /dev/null 2>&1 || rc=$? + if [[ "${rc}" -ne 0 ]]; then + printf '%b\n' " ${GREEN}✓${NC} validate_server_name rejects empty string" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} validate_server_name should reject empty string" + ((FAILED++)) + fi - _test_model_validation - _test_json_escape - _test_ssh_key_utils - _test_syntax_and_logging - _test_open_browser - _test_cloud_init - _test_oauth_functions - _test_ssh_wait - _test_input_and_server_validation + # Test 36: check_openrouter_connectivity succeeds with real network + # Only run if curl is available and we have network access + if command -v curl &> /dev/null; then + result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && check_openrouter_connectivity && echo "reachable"' 2>/dev/null) + if [[ "${result}" == "reachable" ]] || [[ -z "${result}" ]]; then + printf '%b\n' " ${GREEN}✓${NC} check_openrouter_connectivity handles connectivity check" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} check_openrouter_connectivity should return success or failure gracefully" + ((FAILED++)) + fi + else + printf '%b\n' " ${YELLOW}⚠${NC} check_openrouter_connectivity test skipped (curl not available)" + fi } # --- Test source detection in each script --- @@ -571,54 +705,68 @@ test_source_detection() { [[ -f "${script_path}" ]] || continue # Verify the source block checks for local file existence - assert_contains "${script_path}" 'if \[\[ -f "\$\{SCRIPT_DIR\}/lib/common.sh" \]\]' \ - "${script}.sh uses file-existence check for sourcing" + if grep -q 'if \[\[ -f "${SCRIPT_DIR}/lib/common.sh" \]\]' "${script_path}"; then + printf '%b\n' " ${GREEN}✓${NC} ${script}.sh uses file-existence check for sourcing" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} ${script}.sh missing file-existence source check" + ((FAILED++)) + fi # Verify syntax - local rc=0 - bash -n "${script_path}" 2>/dev/null || rc=$? - assert_exit_code "${rc}" 0 "${script}.sh syntax valid" + if bash -n "${script_path}" 2>/dev/null; then + printf '%b\n' " ${GREEN}✓${NC} ${script}.sh syntax valid" + ((PASSED++)) + else + printf '%b\n' " ${RED}✗${NC} ${script}.sh syntax error" + ((FAILED++)) + fi done } # --- Static analysis with shellcheck --- +run_shellcheck() { + echo "" + printf '%b\n' "${YELLOW}━━━ Running shellcheck (static analysis) ━━━${NC}" -# Discover all shell scripts in the repo: agent scripts, lib files, shared, and test harness. -# Populates the DISCOVERED_SCRIPTS array. -_discover_shell_scripts() { - DISCOVERED_SCRIPTS=() - local dir - for dir in "${REPO_ROOT}"/*/; do - local cloud - cloud=$(basename "${dir}") - case "${cloud}" in - cli|shared|test|node_modules|.git|.github|.claude|.docs) continue ;; - esac - local f - for f in "${dir}"*.sh; do - [[ -f "${f}" ]] && DISCOVERED_SCRIPTS+=("${f}") - done - [[ -f "${dir}lib/common.sh" ]] && DISCOVERED_SCRIPTS+=("${dir}lib/common.sh") - done - DISCOVERED_SCRIPTS+=("${REPO_ROOT}/shared/common.sh" "${REPO_ROOT}/test/run.sh") -} + # Check if shellcheck is available + if ! command -v shellcheck &> /dev/null; then + printf '%b\n' " ${YELLOW}⚠${NC} shellcheck not found (install with: apt install shellcheck / brew install shellcheck)" + printf '%b\n' " ${YELLOW}⚠${NC} Skipping static analysis" + return 0 + fi + + # Find all shell scripts + local all_scripts=( + "${REPO_ROOT}"/sprite/*.sh + "${REPO_ROOT}"/sprite/lib/common.sh + "${REPO_ROOT}"/shared/common.sh + "${REPO_ROOT}"/digitalocean/*.sh + "${REPO_ROOT}"/digitalocean/lib/common.sh + "${REPO_ROOT}"/hetzner/*.sh + "${REPO_ROOT}"/hetzner/lib/common.sh + "${REPO_ROOT}"/linode/*.sh + "${REPO_ROOT}"/linode/lib/common.sh + "${REPO_ROOT}"/vultr/*.sh + "${REPO_ROOT}"/vultr/lib/common.sh + "${REPO_ROOT}"/test/run.sh + ) -# Run shellcheck on each discovered script and report results. -_run_shellcheck_on_scripts() { local issue_count=0 local checked_count=0 - for script in "${DISCOVERED_SCRIPTS[@]}"; do + for script in "${all_scripts[@]}"; do [[ -f "${script}" ]] || continue - checked_count=$((checked_count + 1)) + ((checked_count++)) + # Run shellcheck with warning severity, exclude some noisy checks # SC1090: Can't follow non-constant source # SC2312: Consider invoking this command separately to avoid masking its return value local output output=$(shellcheck --severity=warning --exclude=SC1090,SC2312 "${script}" 2>&1) if [[ -n "${output}" ]]; then - issue_count=$((issue_count + 1)) + ((issue_count++)) printf '%b\n' " ${YELLOW}⚠${NC} $(basename "${script}"): found issues" echo "${output}" | sed 's/^/ /' fi @@ -626,26 +774,13 @@ _run_shellcheck_on_scripts() { if [[ "${issue_count}" -eq 0 ]]; then printf '%b\n' " ${GREEN}✓${NC} No issues found in ${checked_count} scripts" - PASSED=$((PASSED + 1)) + ((PASSED++)) else printf '%b\n' " ${YELLOW}⚠${NC} Found issues in ${issue_count}/${checked_count} scripts (advisory only)" + # Don't fail the build, just warn fi } -run_shellcheck() { - echo "" - printf '%b\n' "${YELLOW}━━━ Running shellcheck (static analysis) ━━━${NC}" - - if ! command -v shellcheck &> /dev/null; then - printf '%b\n' " ${YELLOW}⚠${NC} shellcheck not found (install with: apt install shellcheck / brew install shellcheck)" - printf '%b\n' " ${YELLOW}⚠${NC} Skipping static analysis" - return 0 - fi - - _discover_shell_scripts - _run_shellcheck_on_scripts -} - # --- Main --- echo "===============================" echo " Spawn Script Test Suite" diff --git a/vultr/claude.sh b/vultr/claude.sh index 076923c65..6af2fbb55 100755 --- a/vultr/claude.sh +++ b/vultr/claude.sh @@ -20,9 +20,9 @@ create_server "${SERVER_NAME}" verify_server_connectivity "${VULTR_SERVER_IP}" wait_for_cloud_init "${VULTR_SERVER_IP}" 60 -log_step "Verifying Claude Code installation..." +log_warn "Verifying Claude Code installation..." if ! run_server "${VULTR_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_step "Claude Code not found, installing manually..." + log_warn "Claude Code not found, installing manually..." run_server "${VULTR_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -34,7 +34,7 @@ else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) fi -log_step "Setting up environment variables..." +log_warn "Setting up environment variables..." inject_env_vars_ssh "${VULTR_SERVER_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ @@ -52,7 +52,7 @@ log_info "Vultr instance setup completed successfully!" log_info "Server: ${SERVER_NAME} (ID: ${VULTR_SERVER_ID}, IP: ${VULTR_SERVER_IP})" echo "" -log_step "Starting Claude Code..." +log_warn "Starting Claude Code..." sleep 1 clear interactive_session "${VULTR_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" From a370f18782bb02091ad261c456f5f390e924022c Mon Sep 17 00:00:00 2001 From: Ahmed Abushagur Date: Thu, 12 Feb 2026 13:59:25 -0800 Subject: [PATCH 02/13] feat: mock tests for PR checks --- .claude/skills/.DS_Store | Bin 0 -> 6148 bytes .github/workflows/test.yml | 22 ++++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 .claude/skills/.DS_Store create mode 100644 .github/workflows/test.yml diff --git a/.claude/skills/.DS_Store b/.claude/skills/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..043c3ef82781bee9261019786b876a3c9c367c4c GIT binary patch literal 6148 zcmeHKPfNov6n~jhJ4Dz)VaI^iiPMdtcqwy!0V{e?nX4tPIf)-U8I@$>jz zl8G`scu-W{gO^|O{v^#WA!z_WbjE{SfGPkusDz~o79R-tNf)GKErde%F@kN#Ab}8u z3(;)&jSSG*Z9)nGuG<*iuHO)C39cvk5}4qc-az2z>Bx_ik6BOtC<@c8(Rdc6rSkI1 zsIal7T%Dt?gjnhs)xuDjmQjtHe_x-al$a~d|BNb=;Fb)!(5Do$ixjYZ!fyz5- z91l{R>lueraVouPZ93gMXxC+Pzcs7N>A_y3E?dp^Y*ukLw{{LsdUw%%tR4)_0(YdA zb&GR&#L5?DJn@n^QZeo4d5`m+7?}ZPfEoC!4A}F`S^KLK;O}Mzn1OF&fX)YrO6XcF z4CX3>-3f&lonl!g+cV72$PCvQiXkD2$PO>Y2#drg+Y@JLXV97*pY>O zp$I)X+NBN$;Tq(U8DIw9Gq7ZuRl5I=KY#zfPvRCczzqB=21L2*bvrmEd$%r3j_z8C sdW}j#afQLx6s+j07-Q)wuAyo{yQB)DYq2nh78L#w&@^zt4E!hqFS+wpO8@`> literal 0 HcmV?d00001 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..6766f3c5f --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,22 @@ +name: Test + +on: + pull_request: + branches: [main] + +jobs: + mock-tests: + name: Mock Tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run mock tests + run: | + bash test/mock.sh 2>&1 | tee /tmp/mock-output.log + - name: Post summary + if: always() + run: | + echo '## Mock Test Results' >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + grep -E '(Results:|✓|✗|skip|━━━)' /tmp/mock-output.log | head -100 >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" From 9d4127462a8cd4e92e1dcd37ddf7920c0ecc18e7 Mon Sep 17 00:00:00 2001 From: Ahmed Abushagur Date: Thu, 12 Feb 2026 21:35:30 -0800 Subject: [PATCH 03/13] feat: qa on refactor/discovery bot --- .claude/skills/setup-agent-team/discovery.sh | 33 +- .claude/skills/setup-agent-team/qa-cycle.sh | 135 +- .github/workflows/test.yml | 8 +- CLAUDE.md | 23 + test/fixtures/civo/_metadata.json | 12 +- test/fixtures/civo/create_server.json | 24 +- test/fixtures/civo/delete_server.json | 4 + test/fixtures/civo/disk_images.json | 516 ++ test/fixtures/civo/networks.json | 17 + test/fixtures/civo/sshkeys.json | 9 + test/fixtures/digitalocean/_metadata.json | 14 +- test/fixtures/digitalocean/create_server.json | 10 +- test/fixtures/digitalocean/delete_server.json | 5 +- test/fixtures/digitalocean/regions.json | 108 +- test/fixtures/hetzner/_metadata.json | 14 +- test/fixtures/hetzner/create_server.json | 32 +- test/fixtures/hetzner/delete_server.json | 6 +- test/fixtures/hetzner/servers.json | 283 +- test/fixtures/lambda/_metadata.json | 9 +- test/fixtures/lambda/instance_types.json | 68 +- test/fixtures/latitude/_metadata.json | 9 + test/fixtures/latitude/plans.json | 4901 +++++++++++++++++ test/fixtures/latitude/regions.json | 285 + test/fixtures/latitude/ssh_keys.json | 4 + test/fixtures/linode/_metadata.json | 14 +- test/fixtures/linode/create_server.json | 14 +- test/fixtures/linode/regions.json | 109 +- test/fixtures/scaleway/_metadata.json | 6 +- test/fixtures/scaleway/images.json | 110 +- test/fixtures/vultr/_metadata.json | 9 +- test/fixtures/vultr/plans.json | 81 +- test/record.sh | 62 +- 32 files changed, 6329 insertions(+), 605 deletions(-) create mode 100644 test/fixtures/civo/delete_server.json create mode 100644 test/fixtures/civo/disk_images.json create mode 100644 test/fixtures/civo/networks.json create mode 100644 test/fixtures/civo/sshkeys.json create mode 100644 test/fixtures/latitude/_metadata.json create mode 100644 test/fixtures/latitude/plans.json create mode 100644 test/fixtures/latitude/regions.json create mode 100644 test/fixtures/latitude/ssh_keys.json diff --git a/.claude/skills/setup-agent-team/discovery.sh b/.claude/skills/setup-agent-team/discovery.sh index adede4f61..205a851a7 100755 --- a/.claude/skills/setup-agent-team/discovery.sh +++ b/.claude/skills/setup-agent-team/discovery.sh @@ -201,12 +201,25 @@ For each candidate, verify: - Has affordable CPU instances (pay-per-hour or pay-per-second pricing) - Is actually available (not waitlisted/invite-only) -**MANDATORY: Add new clouds to the test infrastructure.** When adding a new cloud, you MUST also update: -1. `test/record.sh` — add the cloud to `ALL_RECORDABLE_CLOUDS`, add a case in `get_endpoints()` with the cloud's GET endpoints, add a case in `get_auth_env_var()`, add a case in `call_api()`, and add a `_live_{cloud}()` function for create/delete fixture recording -2. `test/mock.sh` — add a URL-stripping case in the curl mock so the cloud's API base URL is recognized (look for the `case "$URL" in` block near line 133) -3. `test/record.sh` `has_api_error()` — add error detection for the cloud's API error format - -Without these, the new cloud will have no test coverage and the QA cycle will skip it entirely. +**MANDATORY: Add new clouds to the test infrastructure.** When adding a new cloud, you MUST also update ALL of these files: + +**`test/record.sh`** (fixture recording): +1. Add the cloud to `ALL_RECORDABLE_CLOUDS` +2. Add a case in `get_endpoints()` with the cloud's GET-only API endpoints +3. Add a case in `get_auth_env_var()` mapping the cloud to its auth env var +4. Add a case in `call_api()` to dispatch to the cloud's API function +5. Add a case in `has_api_error()` for the cloud's error response format +6. Add a `_live_{cloud}()` function for create/delete server fixture recording + +**`test/mock.sh`** (mock test infrastructure — ALL of these): +1. **URL stripping**: Add a case in `_strip_api_base()` to recognize the cloud's API base URL (e.g., `https://api.newcloud.com/v1*`). Without this, mock curl logs `UNHANDLED_URL` warnings. +2. **Body validation**: Add a case in `_validate_body()` with the cloud's server creation endpoint and required POST body fields (e.g., `newcloud) case "$EP_CLEAN" in /instances) _check_fields "name region type" ;; esac ;;`) +3. **Cloud API assertions**: Add a case in `assert_cloud_api_calls()` with the cloud's expected API calls (SSH key fetch endpoint + server create endpoint) +4. **Environment setup**: Add a case in `setup_env_for_cloud()` with the cloud's test env vars (API token, server name, plan, region, etc.) +5. **POST response**: If the cloud's create-server response format differs from the default, add a case in `_respond_post()` and `_synthetic_active_response()` +6. **State tracking**: If the cloud creates servers at a non-standard endpoint, add it to `_track_state()` + +Without ALL of these, the new cloud will have no test coverage, body validation will be missing, and the QA cycle will skip it entirely. ### Agent Scout (spawn 1, only if justified) Research new AI agents, BUT only add one if there's REAL community demand: @@ -474,9 +487,13 @@ providers — container platforms, budget VPS providers, or regional clouds with REST APIs. We need affordable instances for running agents that use remote API inference, NOT GPU clouds. Create lib/common.sh, add to manifest, implement 2-3 agents, add "missing" entries for the rest. -MANDATORY: When adding a new cloud, also add it to the test infrastructure: +MANDATORY: When adding a new cloud, also update the test infrastructure: 1. test/record.sh — add to ALL_RECORDABLE_CLOUDS, get_endpoints(), get_auth_env_var(), call_api(), has_api_error(), and add a _live_{cloud}() function -2. test/mock.sh — add a URL-stripping case in the curl mock (case "$URL" in block) +2. test/mock.sh — add ALL of these: + - _strip_api_base(): URL-stripping case for the cloud's API base URL + - _validate_body(): body validation case with server creation endpoint + required fields + - assert_cloud_api_calls(): expected API calls (SSH key fetch + server create) + - setup_env_for_cloud(): test env vars (API token, server name, plan, region) Only add a new AGENT if you find one with real community buzz: - 1000+ GitHub stars diff --git a/.claude/skills/setup-agent-team/qa-cycle.sh b/.claude/skills/setup-agent-team/qa-cycle.sh index 1e898ea03..e28a76a5e 100644 --- a/.claude/skills/setup-agent-team/qa-cycle.sh +++ b/.claude/skills/setup-agent-team/qa-cycle.sh @@ -228,8 +228,14 @@ check_timeout || exit 0 log "=== Phase 1: Record fixtures ===" RECORD_OUTPUT="/tmp/spawn-qa-record-output.txt" +RECORD_FAILURES_FILE="${REPO_ROOT}/.docs/qa-record-failures.json" rm -f "${RECORD_OUTPUT}" +# Initialize failure tracker if missing +if [[ ! -f "${RECORD_FAILURES_FILE}" ]]; then + printf '{}' > "${RECORD_FAILURES_FILE}" +fi + RECORD_EXIT=0 bash test/record.sh allsaved 2>&1 | tee -a "${LOG_FILE}" | tee "${RECORD_OUTPUT}" || RECORD_EXIT=$? @@ -408,6 +414,107 @@ Only modify ${cloud}/lib/common.sh and test/record.sh if the recording infrastru fi fi +# --- Track consecutive Phase 1 failures per cloud --- +# Parse the final recording output to determine which clouds failed +FINAL_RECORD_FAILED="" +FINAL_RECORD_SUCCEEDED="" +if [[ -f "${RECORD_OUTPUT}" ]]; then + _current_cloud="" + _cloud_had_error="" + while IFS= read -r line; do + clean=$(printf '%s' "$line" | sed 's/\x1b\[[0-9;]*m//g') + case "$clean" in + *"Recording "*" ━━━"*) + # Save previous cloud result + if [[ -n "${_current_cloud}" ]]; then + if [[ "${_cloud_had_error}" == "true" ]]; then + FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}" + else + FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}" + fi + fi + _current_cloud=$(printf '%s' "$clean" | sed 's/.*Recording //; s/ ━━━.*//') + _cloud_had_error="" + ;; + *"fail "*) + _cloud_had_error="true" + ;; + *"done "*) + # Cloud section ended + ;; + esac + done < "${RECORD_OUTPUT}" + # Handle last cloud + if [[ -n "${_current_cloud}" ]]; then + if [[ "${_cloud_had_error}" == "true" ]]; then + FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}" + else + FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}" + fi + fi +fi +FINAL_RECORD_FAILED=$(printf '%s' "${FINAL_RECORD_FAILED}" | sed 's/^ //') +FINAL_RECORD_SUCCEEDED=$(printf '%s' "${FINAL_RECORD_SUCCEEDED}" | sed 's/^ //') + +# Update the persistent failure tracker +if [[ -f "${RECORD_FAILURES_FILE}" ]]; then + python3 -c " +import json, sys + +tracker_path = sys.argv[1] +failed = sys.argv[2].split() if sys.argv[2] else [] +succeeded = sys.argv[3].split() if sys.argv[3] else [] + +try: + with open(tracker_path) as f: + tracker = json.load(f) +except (json.JSONDecodeError, FileNotFoundError): + tracker = {} + +# Increment consecutive failures for failed clouds +for cloud in failed: + tracker[cloud] = tracker.get(cloud, 0) + 1 + +# Reset counter for clouds that succeeded +for cloud in succeeded: + tracker[cloud] = 0 + +with open(tracker_path, 'w') as f: + json.dump(tracker, f, indent=2, sort_keys=True) + +# Output clouds that hit the threshold (3+ consecutive failures) +escalate = [c for c, count in tracker.items() if count >= 3] +if escalate: + print(' '.join(escalate)) +" "${RECORD_FAILURES_FILE}" "${FINAL_RECORD_FAILED}" "${FINAL_RECORD_SUCCEEDED}" > /tmp/spawn-qa-escalate.txt 2>/dev/null || true + + ESCALATE_CLOUDS=$(cat /tmp/spawn-qa-escalate.txt 2>/dev/null || true) + rm -f /tmp/spawn-qa-escalate.txt + + if [[ -n "${ESCALATE_CLOUDS}" ]]; then + for cloud in ${ESCALATE_CLOUDS}; do + consecutive=$(python3 -c "import json; print(json.load(open('${RECORD_FAILURES_FILE}')).get('${cloud}', 0))" 2>/dev/null || printf "3+") + log "Phase 1: ESCALATION — ${cloud} has failed ${consecutive} consecutive cycles" + + # Check if an issue already exists for this cloud + existing_issue=$(gh issue list --repo OpenRouterTeam/spawn --state open \ + --search "fixture recording failing ${cloud}" \ + --json number --jq '.[0].number' 2>/dev/null) || existing_issue="" + + if [[ -z "${existing_issue}" ]]; then + gh issue create --repo OpenRouterTeam/spawn \ + --title "QA: ${cloud} fixture recording has been failing for ${consecutive} consecutive cycles" \ + --body "$(printf 'The automated QA cycle has detected that fixture recording for **%s** has failed for **%s consecutive cycles**.\n\nThis likely indicates a persistent issue with the cloud provider'\''s API or our integration that requires manual investigation.\n\n## What to check\n- Has the %s API changed? (new auth requirements, endpoint changes, rate limits)\n- Are the API credentials still valid?\n- Check `%s/lib/common.sh` for outdated API calls\n- Run `bash test/record.sh %s` locally to reproduce\n\n## Auto-generated\nThis issue was created automatically by the QA cycle (`qa-cycle.sh`).\n\n-- qa/cycle' "${cloud}" "${consecutive}" "${cloud}" "${cloud}" "${cloud}")" \ + --label "bug" \ + 2>&1 | tee -a "${LOG_FILE}" || true + log "Phase 1: Created GitHub issue for ${cloud} persistent failure" + else + log "Phase 1: Issue #${existing_issue} already open for ${cloud}, skipping duplicate" + fi + done + fi +fi + rm -f "${RECORD_OUTPUT}" check_timeout || exit 0 @@ -448,6 +555,15 @@ else FAILED_CLOUDS=$(grep ':fail$' "${RESULTS_PHASE2}" | sed 's/:fail$//' | cut -d/ -f1 | sort -u || true) fi + # Capture full mock test output per-cloud for richer agent context + MOCK_OUTPUT_DIR="/tmp/spawn-qa-mock-output" + rm -rf "${MOCK_OUTPUT_DIR}" + mkdir -p "${MOCK_OUTPUT_DIR}" + for cloud in $FAILED_CLOUDS; do + log "Phase 3: Capturing full mock test output for ${cloud}..." + bash test/mock.sh "$cloud" > "${MOCK_OUTPUT_DIR}/${cloud}.log" 2>&1 || true + done + AGENT_PIDS="" for cloud in $FAILED_CLOUDS; do check_timeout || break @@ -456,25 +572,21 @@ else cloud_failures=$(printf '%s\n' $FAILURES | grep "^${cloud}/" || true) failing_scripts="" failing_agents="" - error_context="" for combo in $cloud_failures; do agent=$(printf '%s' "$combo" | cut -d/ -f2) script_path="${cloud}/${agent}.sh" failing_scripts="${failing_scripts} ${script_path}" failing_agents="${failing_agents} ${agent}" - if [[ -f "${LOG_FILE}" ]]; then - ctx=$(grep -A 10 "test ${script_path}" "${LOG_FILE}" | tail -10 || true) - if [[ -n "$ctx" ]]; then - error_context="${error_context} ---- ${script_path} --- -${ctx} -" - fi - fi done failing_scripts=$(printf '%s' "$failing_scripts" | sed 's/^ //') failing_agents=$(printf '%s' "$failing_agents" | sed 's/^ //') + # Use full mock test output as error context (not just 10 lines) + error_context="" + if [[ -f "${MOCK_OUTPUT_DIR}/${cloud}.log" ]]; then + error_context=$(cat "${MOCK_OUTPUT_DIR}/${cloud}.log") + fi + fail_count=$(printf '%s\n' $cloud_failures | wc -l | tr -d ' ') log "Phase 3: Spawning agent to fix ${fail_count} failing script(s) in ${cloud}" @@ -582,6 +694,9 @@ FIXEOF done git worktree prune 2>/dev/null || true + # Clean up per-cloud mock output + rm -rf "${MOCK_OUTPUT_DIR}" 2>/dev/null || true + log "Phase 3: Fix agents complete" fi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6766f3c5f..5051e9166 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,12 +4,18 @@ on: pull_request: branches: [main] +concurrency: + group: test-${{ github.head_ref }} + cancel-in-progress: true + jobs: mock-tests: name: Mock Tests runs-on: ubuntu-latest + timeout-minutes: 10 steps: - - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v4 - name: Run mock tests run: | bash test/mock.sh 2>&1 | tee /tmp/mock-output.log diff --git a/CLAUDE.md b/CLAUDE.md index c1804f03c..da3786daf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -244,6 +244,29 @@ macOS ships bash 3.2. All scripts MUST work on it: - Run tests with `bun test` - Use `import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test"` +### Mock Test Infrastructure (MANDATORY for new clouds) + +When adding a new cloud provider, you **MUST** update all of these in `test/mock.sh`: + +| Function | What to add | +|---|---| +| `_strip_api_base()` | URL pattern to strip the cloud's API base (e.g., `https://api.newcloud.com/v1*`) | +| `_validate_body()` | Server creation endpoint + required POST fields | +| `assert_cloud_api_calls()` | Expected API calls (SSH key fetch + server create endpoints) | +| `setup_env_for_cloud()` | Test env vars (API token, server name, plan, region) | + +And in `test/record.sh`: + +| Function | What to add | +|---|---| +| `ALL_RECORDABLE_CLOUDS` | Cloud name to the list | +| `get_endpoints()` | GET-only API endpoints for fixture recording | +| `get_auth_env_var()` | Auth env var name mapping | +| `call_api()` | API function dispatcher | +| `has_api_error()` | Error response detection for the cloud's API | + +Without these, the cloud has **no test coverage**, body validation is missing, mock tests log `UNHANDLED_URL` warnings, and the QA cycle skips the cloud entirely. Both `test/mock.sh` and `test/record.sh` run in parallel when testing/recording multiple clouds. + ## CLI Version Management **CRITICAL: Bump the version on every CLI change!** diff --git a/test/fixtures/civo/_metadata.json b/test/fixtures/civo/_metadata.json index 5a6bd8299..44ff8c273 100644 --- a/test/fixtures/civo/_metadata.json +++ b/test/fixtures/civo/_metadata.json @@ -1,9 +1,13 @@ { "cloud": "civo", - "recorded_at": "2026-02-11T02:07:07Z", + "recorded_at": "2026-02-13T05:00:40Z", "fixtures": { - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-11T02:07:02Z"}, - "instances": {"endpoint": "/instances", "recorded_at": "2026-02-11T02:07:03Z"}, - "ssh_keys": {"endpoint": "/sshkeys", "recorded_at": "2026-02-11T02:07:04Z"} + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T05:00:28Z"}, + "instances": {"endpoint": "/instances", "recorded_at": "2026-02-13T05:00:29Z"}, + "sshkeys": {"endpoint": "/sshkeys", "recorded_at": "2026-02-13T05:00:30Z"}, + "networks": {"endpoint": "/networks", "recorded_at": "2026-02-13T05:00:30Z"}, + "disk_images": {"endpoint": "/disk_images", "recorded_at": "2026-02-13T05:00:31Z"}, + "create_server": {"endpoint": "POST /instances", "type": "live", "recorded_at": "2026-02-13T05:00:34Z"}, + "delete_server": {"endpoint": "DELETE /instances/{id}", "type": "live", "recorded_at": "2026-02-13T05:00:40Z"} } } diff --git a/test/fixtures/civo/create_server.json b/test/fixtures/civo/create_server.json index 2d769384e..1c6deb428 100644 --- a/test/fixtures/civo/create_server.json +++ b/test/fixtures/civo/create_server.json @@ -1,18 +1,18 @@ { "account_id": "d2449447-ac9e-4a4e-90a8-bb1aae89cb9a", "attached_volumes": [], - "civostatsd_token": "22672e8b-d4f7-4970-bfec-e654593b700d", + "civostatsd_token": "17b20ca4-8cdd-4ceb-8cc4-33a86877461b", "cpu_cores": 1, - "created_at": "2026-02-11T03:01:19Z", + "created_at": "2026-02-13T05:00:34Z", "disk_gb": 25, - "firewall_id": "a12fd86a-a517-46de-90e7-3c9428b164ca", - "hostname": "spawn-record-1770778876", - "id": "d17c67d9-7636-471f-9219-654573e09e56", - "initial_password": "25TA6WIKKr", + "firewall_id": "11a86926-bfbb-4744-aaed-979a3ae85086", + "hostname": "spawn-record-1770958831", + "id": "59e311e5-8abf-44e6-be46-32c0bf6a2cfb", + "initial_password": "MNTA7Q9MW4", "initial_user": "root", - "name": "spawn-record-1770778876-daaf-a54d25", - "namespace_id": "cust-default-d2449447-75057246b722", - "network_id": "1773d9a7-fec1-4478-b45a-665e39528c79", + "name": "spawn-record-1770958831-5a39-8c2237", + "namespace_id": "cust-default-d2449447-75059009d0bd", + "network_id": "aee434ed-e2b8-4e5d-8eb2-97ef3d3062b3", "notes": "", "placement_rule": { "affinity_rules": null, @@ -21,11 +21,11 @@ "ram_mb": 1024, "reverse_dns": "", "script": "", - "size": "g4s.xsmall", - "source_id": "ubuntu-noble", + "size": "g3.xsmall", + "source_id": "1.35.0-k3s1", "source_type": "diskimage", "status": "BUILDING", "tags": [], - "template_id": "d2c2c728-512d-4087-906a-8486fcf4a1c7", + "template_id": "7864fa48-5ce3-4758-a1c1-3b2eeba8c80d", "volume_type": "standard" } diff --git a/test/fixtures/civo/delete_server.json b/test/fixtures/civo/delete_server.json new file mode 100644 index 000000000..cfe5fb36c --- /dev/null +++ b/test/fixtures/civo/delete_server.json @@ -0,0 +1,4 @@ +{ + "id": "59e311e5-8abf-44e6-be46-32c0bf6a2cfb", + "result": "success" +} diff --git a/test/fixtures/civo/disk_images.json b/test/fixtures/civo/disk_images.json new file mode 100644 index 000000000..08af03882 --- /dev/null +++ b/test/fixtures/civo/disk_images.json @@ -0,0 +1,516 @@ +[ + { + "created_at": "2026-01-26T21:36:35Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": true, + "id": "7864fa48-5ce3-4758-a1c1-3b2eeba8c80d", + "initial_user": "root", + "label": "1.35.0-k3s1", + "name": "1.35.0-k3s1", + "state": "available", + "version": "1.35.0-k3s1" + }, + { + "created_at": "2026-01-13T19:07:34Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "7617e6db-0746-4765-88fd-68d9b870f916", + "initial_user": "root", + "label": "1.34.2-k3s1", + "name": "1.34.2-k3s1", + "state": "available", + "version": "1.34.2-k3s1" + }, + { + "created_at": "2025-12-09T07:38:31Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "49603b06-7e3c-460b-9f74-68f9b4620d57", + "initial_user": "root", + "label": "1.34.1-k3s1", + "name": "1.34.1-k3s1", + "state": "available", + "version": "1.34.1-k3s1" + }, + { + "created_at": "2026-01-13T19:07:34Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "1135e5db-fee9-4bd0-953c-42cca32c4501", + "initial_user": "root", + "label": "1.33.6-k3s1", + "name": "1.33.6-k3s1", + "state": "available", + "version": "1.33.6-k3s1" + }, + { + "created_at": "2025-11-11T08:02:43Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "3fcf95fd-852c-4205-a18d-e60dd38f3899", + "initial_user": "root", + "label": "1.33.5-k3s1", + "name": "1.33.5-k3s1", + "state": "available", + "version": "1.33.5-k3s1" + }, + { + "created_at": "2025-09-22T03:12:45Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "308d0cac-afdb-4551-b30f-8ccf53439b79", + "initial_user": "root", + "label": "1.32.5-k3s1", + "name": "1.32.5-k3s1", + "state": "available", + "version": "1.32.5-k3s1" + }, + { + "created_at": "2025-05-26T16:40:14Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "3a8b3616-635a-467c-9890-77932c706982", + "initial_user": "root", + "label": "1.31.6-k3s1", + "name": "1.31.6-k3s1", + "state": "available", + "version": "1.31.6-k3s1" + }, + { + "created_at": "2024-10-23T14:11:08Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "3a12c8dc-d36e-43b2-97d2-e4e26579a776", + "initial_user": "root", + "label": "1.30.5-k3s1", + "name": "1.30.5-k3s1", + "state": "available", + "version": "1.30.5-k3s1" + }, + { + "created_at": "2024-10-23T14:11:08Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "bed09b42-e286-495e-842a-3c73be307973", + "initial_user": "root", + "label": "1.29.8-k3s1", + "name": "1.29.8-k3s1", + "state": "available", + "version": "1.29.8-k3s1" + }, + { + "created_at": "2024-03-06T12:40:59Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "929df383-695e-4a18-a69e-d98fd819814c", + "initial_user": "root", + "label": "1.29.2-k3s1", + "name": "1.29.2-k3s1", + "state": "available", + "version": "1.29.2-k3s1" + }, + { + "created_at": "2024-03-06T12:40:59Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "898a54bf-4201-4e6f-9d86-d1f86d80b9b6", + "initial_user": "root", + "label": "1.28.7-k3s1", + "name": "1.28.7-k3s1", + "state": "available", + "version": "1.28.7-k3s1" + }, + { + "created_at": "2023-10-03T14:14:38Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "4009800d-d5fe-48bf-b916-19b389e9e72f", + "initial_user": "root", + "label": "1.28.2-k3s1", + "name": "1.28.2-k3s1", + "state": "available", + "version": "1.28.2-k3s1" + }, + { + "created_at": "2024-03-06T12:40:59Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "20fe1539-2ecd-4b48-8158-ab795aac638b", + "initial_user": "root", + "label": "1.27.11-k3s1", + "name": "1.27.11-k3s1", + "state": "available", + "version": "1.27.11-k3s1" + }, + { + "created_at": "2023-05-30T16:45:42Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "267033f4-5ca2-4a59-8d33-6e4d2f890157", + "initial_user": "root", + "label": "1.27.1-k3s1", + "name": "1.27.1-k3s1", + "state": "available", + "version": "1.27.1-k3s1" + }, + { + "created_at": "2024-03-06T12:40:59Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "62357d07-8777-4193-9607-d20ec8fed56c", + "initial_user": "root", + "label": "1.26.14-k3s1", + "name": "1.26.14-k3s1", + "state": "available", + "version": "1.26.14-k3s1" + }, + { + "created_at": "2023-05-30T16:45:41Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "1fc7ae3f-f73d-4812-8088-2935a7be974c", + "initial_user": "root", + "label": "1.26.4-k3s1", + "name": "1.26.4-k3s1", + "state": "available", + "version": "1.26.4-k3s1" + }, + { + "created_at": "2023-05-30T16:45:41Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "5a3566f9-e06d-4c15-961b-ced7f3525b75", + "initial_user": "root", + "label": "1.25.9-k3s1", + "name": "1.25.9-k3s1", + "state": "available", + "version": "1.25.9-k3s1" + }, + { + "created_at": "2022-10-17T10:43:20Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "bb142004-b730-4049-bc37-29b97285da61", + "initial_user": "root", + "label": "1.25.0-k3s1", + "name": "1.25.0-k3s1", + "state": "available", + "version": "1.25.0-k3s1" + }, + { + "created_at": "2023-05-30T16:45:41Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "6674b2d3-f10e-4ce3-8d94-0933bc4286d7", + "initial_user": "root", + "label": "1.24.13-k3s1", + "name": "1.24.13-k3s1", + "state": "available", + "version": "1.24.13-k3s1" + }, + { + "created_at": "2022-10-17T10:43:20Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "662c91fe-8c0a-4a35-b24c-64bd50b3864e", + "initial_user": "root", + "label": "1.24.4-k3s1", + "name": "1.24.4-k3s1", + "state": "available", + "version": "1.24.4-k3s1" + }, + { + "created_at": "2022-10-27T11:51:30Z", + "description": "civo-k3s", + "distribution": "civo-k3s", + "distribution_default": false, + "id": "3770ea3c-81b3-484e-8bce-9f7a01abbf0b", + "initial_user": "root", + "label": "1.23.10-k3s1", + "name": "1.23.10-k3s1", + "state": "available", + "version": "1.23.10-k3s1" + }, + { + "created_at": "2022-10-17T10:43:20Z", + "description": "civo-k3s-alpine", + "distribution": "civo-k3s-alpine", + "distribution_default": false, + "id": "84962d39-a07c-4216-b502-4570270dfa2f", + "initial_user": "root", + "label": "1.23.6-k3s1", + "name": "1.23.6-k3s1", + "state": "available", + "version": "1.23.6-k3s1" + }, + { + "created_at": "2022-07-11T10:33:17Z", + "description": "civo-k3s", + "distribution": "civo-k3s", + "distribution_default": false, + "id": "da4037c7-a1e1-48e0-a74a-e7adec30f97f", + "initial_user": "root", + "label": "1.22.11-k3s1", + "name": "1.22.11-k3s1", + "state": "available", + "version": "1.22.11-k3s1" + }, + { + "created_at": "2022-01-27T11:06:34Z", + "description": "civo-k3s", + "distribution": "civo-k3s", + "distribution_default": false, + "id": "6b35b340-6562-4f2f-a7be-a40966694453", + "initial_user": "root", + "label": "1.22.2-k3s1", + "name": "1.22.2-k3s1", + "state": "available", + "version": "1.22.2-k3s1" + }, + { + "created_at": "2022-01-06T16:01:33Z", + "description": "civo-k3s", + "distribution": "civo-k3s", + "distribution_default": false, + "id": "a66ca672-6d87-4e78-95ca-796c054145e9", + "initial_user": "root", + "label": "1.21.2-k3s1", + "name": "1.21.2-k3s1", + "state": "available", + "version": "1.21.2-k3s1" + }, + { + "created_at": "2025-12-10T12:51:06Z", + "description": "debian", + "distribution": "debian", + "distribution_default": true, + "id": "ea377a00-f0a7-4d1f-8ee4-f79cf124d299", + "initial_user": "admin", + "label": "trixie", + "name": "debian-13", + "state": "available", + "version": "13" + }, + { + "created_at": "2025-03-05T16:05:34Z", + "description": "debian", + "distribution": "debian", + "distribution_default": false, + "id": "727b9409-866c-460e-90c6-31cbf105c64a", + "initial_user": "admin", + "label": "bookworm", + "name": "debian-12", + "state": "available", + "version": "12" + }, + { + "created_at": "2022-07-21T14:12:03Z", + "description": "debian", + "distribution": "debian", + "distribution_default": false, + "id": "f398bcd6-c27e-4d56-a6df-d5aa63bb4083", + "initial_user": "admin", + "label": "bullseye", + "name": "debian-11", + "state": "available", + "version": "11" + }, + { + "created_at": "2021-03-17T16:52:12Z", + "description": "debian", + "distribution": "debian", + "distribution_default": false, + "id": "4204229c-510c-4ba4-ab07-522e2aaa2cf8", + "initial_user": "admin", + "label": "buster", + "name": "debian-10", + "state": "available", + "version": "10" + }, + { + "created_at": "2021-03-17T16:52:14Z", + "description": "debian", + "distribution": "debian", + "distribution_default": false, + "id": "cddce6c9-f84e-4e4f-ab8d-7a33cab85158", + "initial_user": "admin", + "label": "stretch", + "name": "debian-9", + "state": "available", + "version": "9" + }, + { + "created_at": "2025-07-30T07:53:34Z", + "description": "rocky", + "distribution": "rocky", + "distribution_default": true, + "id": "34b21eb5-80ee-4c5f-86e8-70f12a2485fc", + "initial_user": "rocky", + "label": "10", + "name": "rocky-10", + "state": "available", + "version": "10" + }, + { + "created_at": "2023-02-17T09:34:38Z", + "description": "rocky", + "distribution": "rocky", + "distribution_default": false, + "id": "4a7b536b-c893-41cf-b5e1-19f47933267d", + "initial_user": "rocky", + "label": "9.1", + "name": "rocky-9-1", + "state": "available", + "version": "9.1" + }, + { + "created_at": "2025-07-30T07:53:34Z", + "description": "rocky", + "distribution": "rocky", + "distribution_default": false, + "id": "54480094-4607-4185-95cc-d30253fce9eb", + "initial_user": "rocky", + "label": "9", + "name": "rocky-9", + "state": "available", + "version": "9" + }, + { + "created_at": "2023-09-15T08:56:58Z", + "description": "talos", + "distribution": "talos", + "distribution_default": true, + "id": "64053a48-b20d-4345-860c-cfa0a688816d", + "label": "v1.5.0", + "name": "talos-v1.5.0", + "state": "available", + "version": "1.27.0" + }, + { + "created_at": "2023-02-02T06:30:42Z", + "description": "talos", + "distribution": "talos", + "distribution_default": false, + "id": "7bb21130-840a-484f-a049-dcbbb4fd87cf", + "label": "v1.2.8", + "name": "talos-v1.2.8", + "state": "available", + "version": "1.25.5" + }, + { + "created_at": "2024-10-23T14:11:08Z", + "description": "ubuntu", + "distribution": "ubuntu", + "distribution_default": true, + "id": "9fb7b840-6cde-448a-b570-6418430c6ac8", + "initial_user": "ubuntu", + "label": "noble", + "name": "ubuntu-noble", + "state": "available", + "version": "24.04" + }, + { + "created_at": "2022-05-18T21:29:33Z", + "description": "ubuntu", + "distribution": "ubuntu", + "distribution_default": false, + "id": "9f953761-8b20-4623-8dff-e0c845201966", + "initial_user": "ubuntu", + "label": "jammy", + "name": "ubuntu-jammy", + "state": "available", + "version": "22.04" + }, + { + "created_at": "2021-03-17T16:52:13Z", + "description": "ubuntu", + "distribution": "ubuntu", + "distribution_default": false, + "id": "8eb48e20-e5db-49fe-9cdf-cc8f381c61c6", + "initial_user": "ubuntu", + "label": "focal", + "name": "ubuntu-focal", + "state": "available", + "version": "20.04" + }, + { + "created_at": "2025-07-30T07:53:56Z", + "description": "ubuntu", + "distribution": "ubuntu", + "distribution_default": false, + "id": "14f97f88-3963-462a-b517-e3697e263147", + "initial_user": "ubuntu", + "label": "cuda12-9", + "name": "ubuntu-cuda12-9", + "state": "available", + "version": "24.04-cuda12-9" + }, + { + "created_at": "2025-07-30T07:53:56Z", + "description": "ubuntu", + "distribution": "ubuntu", + "distribution_default": false, + "id": "a6dd00aa-96fe-4b00-b6bd-f5c42dab33a9", + "initial_user": "ubuntu", + "label": "cuda12-8", + "name": "ubuntu-cuda12-8", + "state": "available", + "version": "24.04-cuda12-8" + }, + { + "created_at": "2025-05-26T16:40:14Z", + "description": "ubuntu", + "distribution": "ubuntu", + "distribution_default": false, + "id": "c7ce39db-2054-4276-8b95-3b1d35e4ef01", + "initial_user": "ubuntu", + "label": "cuda12-6", + "name": "ubuntu-cuda12-6", + "state": "available", + "version": "24.04-cuda12-6" + }, + { + "created_at": "2024-11-21T09:15:26Z", + "description": "ubuntu", + "distribution": "ubuntu", + "distribution_default": false, + "id": "a9cf6632-d127-4516-9ce4-e2998e1bb2eb", + "initial_user": "ubuntu", + "label": "cuda12-2", + "name": "ubuntu-cuda12-2", + "state": "available", + "version": "22.04-cuda12-2" + }, + { + "created_at": "2024-11-21T09:15:26Z", + "description": "ubuntu", + "distribution": "ubuntu", + "distribution_default": false, + "id": "ac2aa741-b547-4b77-b3e8-fb40b73c6266", + "initial_user": "ubuntu", + "label": "cuda11-8", + "name": "ubuntu-cuda11-8", + "state": "available", + "version": "22.04-cuda11-8" + } +] diff --git a/test/fixtures/civo/networks.json b/test/fixtures/civo/networks.json new file mode 100644 index 000000000..81685b43d --- /dev/null +++ b/test/fixtures/civo/networks.json @@ -0,0 +1,17 @@ +[ + { + "cidr": "192.168.1.0/24", + "default": true, + "free_ip_count": 254, + "gateway_ipv4": "192.168.1.1", + "id": "aee434ed-e2b8-4e5d-8eb2-97ef3d3062b3", + "ipv4_enabled": true, + "label": "default", + "name": "cust-default-d2449447-75059009d0bd", + "nameservers_v4": [ + "8.8.8.8", + "1.1.1.1" + ], + "status": "Active" + } +] diff --git a/test/fixtures/civo/sshkeys.json b/test/fixtures/civo/sshkeys.json new file mode 100644 index 000000000..95332b180 --- /dev/null +++ b/test/fixtures/civo/sshkeys.json @@ -0,0 +1,9 @@ +[ + { + "created_at": "2026-02-12T00:26:45Z", + "fingerprint": "SHA256:Ko5JS3tB2xRw5C+jWzdDsRoscXqt1fWV1w0VPThbjrA", + "id": "feb0a45d-7c21-44fa-adc2-c24a88b2f5d5", + "name": "spawn-MacBookPro-1770856003", + "public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHmcVdzydp72a/B69nmENZvCvjuk7xGpKdi5CvhkmNsv ahmed" + } +] diff --git a/test/fixtures/digitalocean/_metadata.json b/test/fixtures/digitalocean/_metadata.json index 9b546f315..af1429efa 100644 --- a/test/fixtures/digitalocean/_metadata.json +++ b/test/fixtures/digitalocean/_metadata.json @@ -1,11 +1,13 @@ { "cloud": "digitalocean", - "recorded_at": "2026-02-10T21:55:29Z", + "recorded_at": "2026-02-13T05:00:37Z", "fixtures": { - "account": {"endpoint": "/account", "recorded_at": "2026-02-10T21:55:27Z"}, - "ssh_keys": {"endpoint": "/account/keys", "recorded_at": "2026-02-10T21:55:27Z"}, - "droplets": {"endpoint": "/droplets", "recorded_at": "2026-02-10T21:55:28Z"}, - "sizes": {"endpoint": "/sizes", "recorded_at": "2026-02-10T21:55:29Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-10T21:55:29Z"} + "account": {"endpoint": "/account", "recorded_at": "2026-02-13T05:00:28Z"}, + "ssh_keys": {"endpoint": "/account/keys", "recorded_at": "2026-02-13T05:00:29Z"}, + "droplets": {"endpoint": "/droplets", "recorded_at": "2026-02-13T05:00:29Z"}, + "sizes": {"endpoint": "/sizes", "recorded_at": "2026-02-13T05:00:29Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T05:00:30Z"}, + "create_server": {"endpoint": "POST /droplets", "type": "live", "recorded_at": "2026-02-13T05:00:33Z"}, + "delete_server": {"endpoint": "DELETE /droplets/{id}", "type": "live", "recorded_at": "2026-02-13T05:00:37Z"} } } diff --git a/test/fixtures/digitalocean/create_server.json b/test/fixtures/digitalocean/create_server.json index ede0fc5b1..a4d5c605e 100644 --- a/test/fixtures/digitalocean/create_server.json +++ b/test/fixtures/digitalocean/create_server.json @@ -1,7 +1,7 @@ { "droplet": { "backup_ids": [], - "created_at": "2026-02-11T03:07:33Z", + "created_at": "2026-02-13T05:00:32Z", "disk": 10, "disk_info": [ { @@ -15,7 +15,7 @@ "features": [ "droplet_agent" ], - "id": 550842125, + "id": 551370564, "image": { "created_at": "2025-08-08T15:11:27Z", "description": "Ubuntu 24.04 (LTS) x64", @@ -51,7 +51,7 @@ "kernel": null, "locked": false, "memory": 512, - "name": "spawn-record-1770779252", + "name": "spawn-record-1770958830", "networks": { "v4": [], "v6": [] @@ -276,8 +276,8 @@ "links": { "actions": [ { - "href": "/v2/actions/3038067722", - "id": 3038067722, + "href": "/v2/actions/3041635779", + "id": 3041635779, "rel": "create" } ] diff --git a/test/fixtures/digitalocean/delete_server.json b/test/fixtures/digitalocean/delete_server.json index 0967ef424..3a7eeca98 100644 --- a/test/fixtures/digitalocean/delete_server.json +++ b/test/fixtures/digitalocean/delete_server.json @@ -1 +1,4 @@ -{} +{ + "http_code": 204, + "status": "deleted" +} diff --git a/test/fixtures/digitalocean/regions.json b/test/fixtures/digitalocean/regions.json index 2ec9f844f..f6566eea2 100644 --- a/test/fixtures/digitalocean/regions.json +++ b/test/fixtures/digitalocean/regions.json @@ -282,32 +282,42 @@ "c5-8vcpu-16gb-intel", "so1_5-4vcpu-32gb", "m-8vcpu-64gb", + "c-16", "g6_5-8vcpu-32gb", "g5_5-8vcpu-32gb-intel", + "c2-16vcpu-32gb", "m-8vcpu-64gb-intel", "m3-8vcpu-64gb", "c-16-intel", "m3-8vcpu-64gb-intel", "c2-16vcpu-32gb-intel", + "c5-16vcpu-32gb", + "g-16vcpu-64gb", "so-8vcpu-64gb-intel", "so-8vcpu-64gb", "m6-8vcpu-64gb", + "gd-16vcpu-64gb", "so1_5-8vcpu-64gb-intel", "g-16vcpu-64gb-intel", "gd-16vcpu-64gb-intel", "c5-16vcpu-32gb-intel", "so1_5-8vcpu-64gb", "m-16vcpu-128gb", + "c-32", + "g6_5-16vcpu-64gb", "g5_5-16vcpu-64gb-intel", + "c2-32vcpu-64gb", "m-16vcpu-128gb-intel", "m3-16vcpu-128gb", "c-32-intel", "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", "m-24vcpu-192gb", + "g-32vcpu-128gb", "so-16vcpu-128gb-intel", "so-16vcpu-128gb", "m6-16vcpu-128gb", + "gd-32vcpu-128gb", "so1_5-16vcpu-128gb-intel", "m-24vcpu-192gb-intel", "g-32vcpu-128gb-intel", @@ -315,18 +325,26 @@ "gd-32vcpu-128gb-intel", "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", + "c-48-intel", "m3-24vcpu-192gb-intel", + "m-32vcpu-256gb", + "g6_5-32vcpu-128gb", + "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", "so-24vcpu-192gb-intel", "so-24vcpu-192gb", "m6-24vcpu-192gb", "m-32vcpu-256gb-intel", + "m3-32vcpu-256gb", "so1_5-24vcpu-192gb-intel", "m3-32vcpu-256gb-intel", "g-48vcpu-192gb-intel", "gd-48vcpu-192gb-intel", + "c5-48vcpu-96gb-intel", "so1_5-24vcpu-192gb", "so-32vcpu-256gb-intel", + "so-32vcpu-256gb", + "m6-32vcpu-256gb", "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", "gpu-h100x1-80gb", @@ -389,7 +407,6 @@ "gd-2vcpu-8gb", "g-2vcpu-8gb-intel", "gd-2vcpu-8gb-intel", - "s-4vcpu-16gb-amd", "m-2vcpu-16gb", "c-4", "g6_5-2vcpu-8gb", @@ -401,7 +418,6 @@ "m3-2vcpu-16gb", "c-4-intel", "m3-2vcpu-16gb-intel", - "s-8vcpu-16gb-amd", "s-8vcpu-16gb-intel", "c2-4vcpu-8gb-intel", "c5-4vcpu-8gb", @@ -416,7 +432,6 @@ "gd-4vcpu-16gb-intel", "c5-4vcpu-8gb-intel", "so1_5-2vcpu-16gb", - "s-8vcpu-32gb-amd", "m-4vcpu-32gb", "c-8", "g6_5-4vcpu-16gb", @@ -471,6 +486,7 @@ "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", "c5-32vcpu-64gb", + "c-48", "m-24vcpu-192gb", "g-32vcpu-128gb", "so-16vcpu-128gb-intel", @@ -943,7 +959,6 @@ "gd-4vcpu-16gb-intel", "c5-4vcpu-8gb-intel", "so1_5-2vcpu-16gb", - "s-8vcpu-32gb-amd", "m-4vcpu-32gb", "c-8", "g6_5-4vcpu-16gb", @@ -1011,36 +1026,18 @@ "m3-24vcpu-192gb", "g-40vcpu-160gb", "gd-32vcpu-128gb-intel", - "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", - "c-48-intel", "m3-24vcpu-192gb-intel", "gd-40vcpu-160gb", "g6_5-32vcpu-128gb", - "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", "so-24vcpu-192gb-intel", - "so-24vcpu-192gb", - "m6-24vcpu-192gb", "m-32vcpu-256gb-intel", - "c-60-intel", "so1_5-24vcpu-192gb-intel", "m3-32vcpu-256gb-intel", - "g-48vcpu-192gb-intel", - "c2-60vcpu-120gb-intel", - "gd-48vcpu-192gb-intel", - "c5-48vcpu-96gb-intel", "so-32vcpu-256gb-intel", - "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", - "g-60vcpu-240gb-intel", - "m-48vcpu-384gb-intel", - "gd-60vcpu-240gb-intel", - "c5-60vcpu-120gb-intel", - "gpu-h100x1-80gb", - "m3-48vcpu-384gb-intel", - "g5_5-60vcpu-240gb-intel", - "so-48vcpu-384gb-intel" + "gpu-h100x1-80gb" ], "slug": "ams3" }, @@ -1158,13 +1155,16 @@ "c5-16vcpu-32gb-intel", "so1_5-8vcpu-64gb", "m-16vcpu-128gb", + "c-32", "g6_5-16vcpu-64gb", "g5_5-16vcpu-64gb-intel", + "c2-32vcpu-64gb", "m-16vcpu-128gb-intel", "m3-16vcpu-128gb", "c-32-intel", "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", + "c5-32vcpu-64gb", "m-24vcpu-192gb", "g-32vcpu-128gb", "so-16vcpu-128gb-intel", @@ -1335,26 +1335,19 @@ "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", "m-24vcpu-192gb", - "g-32vcpu-128gb", "so-16vcpu-128gb-intel", "so-16vcpu-128gb", "m6-16vcpu-128gb", - "gd-32vcpu-128gb", "so1_5-16vcpu-128gb-intel", - "gpu-l40sx1-48gb", "gpu-6000adax1-48gb", "m-24vcpu-192gb-intel", "g-32vcpu-128gb-intel", "m3-24vcpu-192gb", - "g-40vcpu-160gb", "gd-32vcpu-128gb-intel", "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", "c-48-intel", "m3-24vcpu-192gb-intel", - "m-32vcpu-256gb", - "gd-40vcpu-160gb", - "g6_5-32vcpu-128gb", "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", "so-24vcpu-192gb-intel", @@ -1362,18 +1355,14 @@ "m6-24vcpu-192gb", "m-32vcpu-256gb-intel", "c-60-intel", - "m3-32vcpu-256gb", "so1_5-24vcpu-192gb-intel", "m3-32vcpu-256gb-intel", - "g6_5-40vcpu-160gb", "g-48vcpu-192gb-intel", "c2-60vcpu-120gb-intel", "gd-48vcpu-192gb-intel", "c5-48vcpu-96gb-intel", "so1_5-24vcpu-192gb", "so-32vcpu-256gb-intel", - "so-32vcpu-256gb", - "m6-32vcpu-256gb", "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", "g-60vcpu-240gb-intel", @@ -1381,7 +1370,6 @@ "gd-60vcpu-240gb-intel", "c5-60vcpu-120gb-intel", "gpu-h100x1-80gb", - "so1_5-32vcpu-256gb", "m3-48vcpu-384gb-intel", "g5_5-60vcpu-240gb-intel", "so-48vcpu-384gb-intel" @@ -1463,78 +1451,40 @@ "so1_5-4vcpu-32gb-intel", "g-8vcpu-32gb-intel", "gd-8vcpu-32gb-intel", - "c5-8vcpu-16gb-intel", "so1_5-4vcpu-32gb", "m-8vcpu-64gb", "c-16", "g6_5-8vcpu-32gb", - "g5_5-8vcpu-32gb-intel", "c2-16vcpu-32gb", "m-8vcpu-64gb-intel", "m3-8vcpu-64gb", "c-16-intel", - "m3-8vcpu-64gb-intel", "c2-16vcpu-32gb-intel", "c5-16vcpu-32gb", "g-16vcpu-64gb", - "so-8vcpu-64gb-intel", "so-8vcpu-64gb", "m6-8vcpu-64gb", "gd-16vcpu-64gb", - "so1_5-8vcpu-64gb-intel", "g-16vcpu-64gb-intel", "gd-16vcpu-64gb-intel", - "c5-16vcpu-32gb-intel", "so1_5-8vcpu-64gb", "m-16vcpu-128gb", "c-32", "g6_5-16vcpu-64gb", - "g5_5-16vcpu-64gb-intel", "c2-32vcpu-64gb", "m-16vcpu-128gb-intel", "m3-16vcpu-128gb", - "c-32-intel", - "m3-16vcpu-128gb-intel", - "c2-32vcpu-64gb-intel", "c-48", "m-24vcpu-192gb", "g-32vcpu-128gb", - "so-16vcpu-128gb-intel", "so-16vcpu-128gb", "m6-16vcpu-128gb", "gd-32vcpu-128gb", - "so1_5-16vcpu-128gb-intel", "c2-48vcpu-96gb", - "m-24vcpu-192gb-intel", "g-32vcpu-128gb-intel", "g-40vcpu-160gb", - "gd-32vcpu-128gb-intel", - "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", - "c-48-intel", - "m3-24vcpu-192gb-intel", - "gd-40vcpu-160gb", - "c2-48vcpu-96gb-intel", - "g5_5-32vcpu-128gb-intel", - "so-24vcpu-192gb-intel", - "m-32vcpu-256gb-intel", - "c-60-intel", - "so1_5-24vcpu-192gb-intel", - "m3-32vcpu-256gb-intel", - "g-48vcpu-192gb-intel", - "c2-60vcpu-120gb-intel", - "gd-48vcpu-192gb-intel", - "c5-48vcpu-96gb-intel", - "so-32vcpu-256gb-intel", - "g5_5-48vcpu-192gb-intel", - "so1_5-32vcpu-256gb-intel", - "g-60vcpu-240gb-intel", - "m-48vcpu-384gb-intel", - "gd-60vcpu-240gb-intel", - "c5-60vcpu-120gb-intel", - "m3-48vcpu-384gb-intel", - "g5_5-60vcpu-240gb-intel", - "so-48vcpu-384gb-intel" + "gd-40vcpu-160gb" ], "slug": "sfo2" }, @@ -1853,40 +1803,30 @@ "gd-32vcpu-128gb-intel", "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", - "c-48-intel", "m3-24vcpu-192gb-intel", "m-32vcpu-256gb", "gd-40vcpu-160gb", "g6_5-32vcpu-128gb", - "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", - "c5-48vcpu-96gb", "so-24vcpu-192gb-intel", "so-24vcpu-192gb", "m6-24vcpu-192gb", "m-32vcpu-256gb-intel", - "c-60-intel", "m3-32vcpu-256gb", "so1_5-24vcpu-192gb-intel", "m3-32vcpu-256gb-intel", "g6_5-40vcpu-160gb", "g-48vcpu-192gb-intel", - "c2-60vcpu-120gb-intel", "gd-48vcpu-192gb-intel", - "c5-48vcpu-96gb-intel", "so1_5-24vcpu-192gb", "so-32vcpu-256gb-intel", "so-32vcpu-256gb", "m6-32vcpu-256gb", "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", - "g-60vcpu-240gb-intel", "m-48vcpu-384gb-intel", - "gd-60vcpu-240gb-intel", - "c5-60vcpu-120gb-intel", "so1_5-32vcpu-256gb", "m3-48vcpu-384gb-intel", - "g5_5-60vcpu-240gb-intel", "so-48vcpu-384gb-intel" ], "slug": "sfo3" diff --git a/test/fixtures/hetzner/_metadata.json b/test/fixtures/hetzner/_metadata.json index c5883a23a..87157b730 100644 --- a/test/fixtures/hetzner/_metadata.json +++ b/test/fixtures/hetzner/_metadata.json @@ -1,12 +1,12 @@ { "cloud": "hetzner", - "recorded_at": "2026-02-10T21:55:26Z", + "recorded_at": "2026-02-13T05:00:36Z", "fixtures": { - "server_types": {"endpoint": "/server_types?per_page=50", "recorded_at": "2026-02-10T21:55:19Z"}, - "locations": {"endpoint": "/locations", "recorded_at": "2026-02-10T21:55:20Z"}, - "ssh_keys": {"endpoint": "/ssh_keys", "recorded_at": "2026-02-10T21:55:21Z"}, - "servers": {"endpoint": "/servers", "recorded_at": "2026-02-10T21:55:22Z"}, - "create_server": {"endpoint": "POST /servers", "type": "live", "recorded_at": "2026-02-10T21:55:24Z"}, - "delete_server": {"endpoint": "DELETE /servers/{id}", "type": "live", "recorded_at": "2026-02-10T21:55:26Z"} + "server_types": {"endpoint": "/server_types?per_page=50", "recorded_at": "2026-02-13T05:00:29Z"}, + "locations": {"endpoint": "/locations", "recorded_at": "2026-02-13T05:00:29Z"}, + "ssh_keys": {"endpoint": "/ssh_keys", "recorded_at": "2026-02-13T05:00:30Z"}, + "servers": {"endpoint": "/servers", "recorded_at": "2026-02-13T05:00:31Z"}, + "create_server": {"endpoint": "POST /servers", "type": "live", "recorded_at": "2026-02-13T05:00:33Z"}, + "delete_server": {"endpoint": "DELETE /servers/{id}", "type": "live", "recorded_at": "2026-02-13T05:00:36Z"} } } diff --git a/test/fixtures/hetzner/create_server.json b/test/fixtures/hetzner/create_server.json index 74637d827..d0cfc9fce 100644 --- a/test/fixtures/hetzner/create_server.json +++ b/test/fixtures/hetzner/create_server.json @@ -3,11 +3,11 @@ "command": "create_server", "error": null, "finished": null, - "id": 610821665885201, + "id": 611302701715360, "progress": 0, "resources": [ { - "id": 120682780, + "id": 120889518, "type": "server" }, { @@ -15,7 +15,7 @@ "type": "image" } ], - "started": "2026-02-10T21:55:23Z", + "started": "2026-02-13T05:00:33Z", "status": "running" }, "next_actions": [ @@ -23,23 +23,23 @@ "command": "start_server", "error": null, "finished": null, - "id": 610821665885202, - "parent_id": 610821665885201, + "id": 611302701715361, + "parent_id": 611302701715360, "progress": 0, "resources": [ { - "id": 120682780, + "id": 120889518, "type": "server" } ], - "started": "2026-02-10T21:55:23Z", + "started": "2026-02-13T05:00:33Z", "status": "running" } ], "root_password": null, "server": { "backup_window": null, - "created": "2026-02-10T21:55:23Z", + "created": "2026-02-13T05:00:33Z", "datacenter": { "description": "Nuremberg 1 virtual DC 3", "id": 2, @@ -65,6 +65,7 @@ 45, 93, 94, + 95, 96, 97, 98, @@ -91,6 +92,7 @@ 45, 93, 94, + 95, 96, 97, 98, @@ -166,7 +168,7 @@ ] } }, - "id": 120682780, + "id": 120889518, "image": { "architecture": "x86", "bound_to": null, @@ -205,7 +207,7 @@ "network_zone": "eu-central" }, "locked": false, - "name": "spawn-record-1770760522", + "name": "spawn-record-1770958831", "outgoing_traffic": 0, "placement_group": null, "primary_disk_size": 40, @@ -219,15 +221,15 @@ "floating_ips": [], "ipv4": { "blocked": false, - "dns_ptr": "static.197.210.245.188.clients.your-server.de", - "id": 117603379, - "ip": "188.245.210.197" + "dns_ptr": "static.81.176.245.188.clients.your-server.de", + "id": 117975188, + "ip": "188.245.176.81" }, "ipv6": { "blocked": false, "dns_ptr": [], - "id": 117603380, - "ip": "2a01:4f8:1c1e:bc55::/64" + "id": 117975189, + "ip": "2a01:4f8:1c1a:f748::/64" } }, "rescue_enabled": false, diff --git a/test/fixtures/hetzner/delete_server.json b/test/fixtures/hetzner/delete_server.json index bbf825949..ee03f36a6 100644 --- a/test/fixtures/hetzner/delete_server.json +++ b/test/fixtures/hetzner/delete_server.json @@ -3,15 +3,15 @@ "command": "delete_server", "error": null, "finished": null, - "id": 610821665885215, + "id": 611302701715371, "progress": 0, "resources": [ { - "id": 120682780, + "id": 120889518, "type": "server" } ], - "started": "2026-02-10T21:55:26Z", + "started": "2026-02-13T05:00:36Z", "status": "running" } } diff --git a/test/fixtures/hetzner/servers.json b/test/fixtures/hetzner/servers.json index dd6315039..8ff3b8b96 100644 --- a/test/fixtures/hetzner/servers.json +++ b/test/fixtures/hetzner/servers.json @@ -6,287 +6,8 @@ "page": 1, "per_page": 25, "previous_page": null, - "total_entries": 1 + "total_entries": 0 } }, - "servers": [ - { - "backup_window": null, - "created": "2026-02-10T21:32:27Z", - "datacenter": { - "description": "Nuremberg 1 virtual DC 3", - "id": 2, - "location": { - "city": "Nuremberg", - "country": "DE", - "description": "Nuremberg DC Park 1", - "id": 2, - "latitude": 49.452102, - "longitude": 11.076665, - "name": "nbg1", - "network_zone": "eu-central" - }, - "name": "nbg1-dc3", - "server_types": { - "available": [ - 27, - 28, - 29, - 30, - 31, - 32, - 45, - 93, - 94, - 96, - 97, - 98, - 99, - 100, - 101, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ], - "available_for_migration": [ - 27, - 28, - 29, - 30, - 31, - 32, - 45, - 93, - 94, - 96, - 97, - 98, - 99, - 100, - 101, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ], - "supported": [ - 1, - 3, - 5, - 7, - 9, - 11, - 12, - 13, - 14, - 15, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 45, - 93, - 94, - 95, - 96, - 97, - 98, - 99, - 100, - 101, - 104, - 105, - 106, - 107, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ] - } - }, - "id": 120681182, - "image": { - "architecture": "x86", - "bound_to": null, - "created": "2024-04-25T13:26:27Z", - "created_from": null, - "deleted": null, - "deprecated": null, - "description": "Ubuntu 24.04", - "disk_size": 5, - "id": 161547269, - "image_size": null, - "labels": {}, - "name": "ubuntu-24.04", - "os_flavor": "ubuntu", - "os_version": "24.04", - "protection": { - "delete": false - }, - "rapid_deploy": true, - "status": "available", - "type": "system" - }, - "included_traffic": 21990232555520, - "ingoing_traffic": 0, - "iso": null, - "labels": {}, - "load_balancers": [], - "location": { - "city": "Nuremberg", - "country": "DE", - "description": "Nuremberg DC Park 1", - "id": 2, - "latitude": 49.452102, - "longitude": 11.076665, - "name": "nbg1", - "network_zone": "eu-central" - }, - "locked": false, - "name": "claude", - "outgoing_traffic": 0, - "placement_group": null, - "primary_disk_size": 40, - "private_net": [], - "protection": { - "delete": false, - "rebuild": false - }, - "public_net": { - "firewalls": [], - "floating_ips": [], - "ipv4": { - "blocked": false, - "dns_ptr": "static.157.65.225.46.clients.your-server.de", - "id": 117600167, - "ip": "46.225.65.157" - }, - "ipv6": { - "blocked": false, - "dns_ptr": [], - "id": 117600168, - "ip": "2a01:4f8:1c19:e41b::/64" - } - }, - "rescue_enabled": false, - "server_type": { - "architecture": "x86", - "category": "cost_optimized", - "cores": 2, - "cpu_type": "shared", - "deprecated": false, - "deprecation": null, - "description": "CX 23", - "disk": 40, - "id": 114, - "locations": [ - { - "deprecation": null, - "id": 1, - "name": "fsn1" - }, - { - "deprecation": null, - "id": 2, - "name": "nbg1" - }, - { - "deprecation": null, - "id": 3, - "name": "hel1" - } - ], - "memory": 4, - "name": "cx23", - "prices": [ - { - "included_traffic": 21990232555520, - "location": "fsn1", - "price_hourly": { - "gross": "0.0056000000000000", - "net": "0.0056000000" - }, - "price_monthly": { - "gross": "3.4900000000000000", - "net": "3.4900000000" - }, - "price_per_tb_traffic": { - "gross": "1.2000000000000000", - "net": "1.2000000000" - } - }, - { - "included_traffic": 21990232555520, - "location": "hel1", - "price_hourly": { - "gross": "0.0056000000000000", - "net": "0.0056000000" - }, - "price_monthly": { - "gross": "3.4900000000000000", - "net": "3.4900000000" - }, - "price_per_tb_traffic": { - "gross": "1.2000000000000000", - "net": "1.2000000000" - } - }, - { - "included_traffic": 21990232555520, - "location": "nbg1", - "price_hourly": { - "gross": "0.0056000000000000", - "net": "0.0056000000" - }, - "price_monthly": { - "gross": "3.4900000000000000", - "net": "3.4900000000" - }, - "price_per_tb_traffic": { - "gross": "1.2000000000000000", - "net": "1.2000000000" - } - } - ], - "storage_type": "local" - }, - "status": "running", - "volumes": [] - } - ] + "servers": [] } diff --git a/test/fixtures/lambda/_metadata.json b/test/fixtures/lambda/_metadata.json index 6e08753ae..d691ce397 100644 --- a/test/fixtures/lambda/_metadata.json +++ b/test/fixtures/lambda/_metadata.json @@ -1,10 +1,9 @@ { "cloud": "lambda", - "recorded_at": "2026-02-11T00:54:13Z", + "recorded_at": "2026-02-13T05:00:29Z", "fixtures": { - "instances": {"endpoint": "/instances", "recorded_at": "2026-02-11T00:54:12Z"}, - "ssh_keys": {"endpoint": "/ssh-keys", "recorded_at": "2026-02-11T00:54:12Z"}, - "instance_types": {"endpoint": "/instance-types", "recorded_at": "2026-02-11T00:54:12Z"}, - "create_server": {"endpoint": "POST /instance-operations/launch", "type": "live", "recorded_at": "2026-02-11T00:54:13Z"} + "instances": {"endpoint": "/instances", "recorded_at": "2026-02-13T05:00:28Z"}, + "ssh_keys": {"endpoint": "/ssh-keys", "recorded_at": "2026-02-13T05:00:28Z"}, + "instance_types": {"endpoint": "/instance-types", "recorded_at": "2026-02-13T05:00:29Z"} } } diff --git a/test/fixtures/lambda/instance_types.json b/test/fixtures/lambda/instance_types.json index f448e670e..7a88a4c96 100644 --- a/test/fixtures/lambda/instance_types.json +++ b/test/fixtures/lambda/instance_types.json @@ -67,7 +67,16 @@ "vcpus": 30 } }, - "regions_with_capacity_available": [] + "regions_with_capacity_available": [ + { + "description": "Virginia, USA", + "name": "us-east-1" + }, + { + "description": "Arizona, USA", + "name": "us-west-2" + } + ] }, "gpu_1x_a6000": { "instance_type": { @@ -97,7 +106,12 @@ "vcpus": 26 } }, - "regions_with_capacity_available": [] + "regions_with_capacity_available": [ + { + "description": "Sydney, Australia", + "name": "australia-east-1" + } + ] }, "gpu_1x_gh200": { "instance_type": { @@ -112,7 +126,12 @@ "vcpus": 64 } }, - "regions_with_capacity_available": [] + "regions_with_capacity_available": [ + { + "description": "Washington DC, USA", + "name": "us-east-3" + } + ] }, "gpu_1x_h100_pcie": { "instance_type": { @@ -127,7 +146,12 @@ "vcpus": 26 } }, - "regions_with_capacity_available": [] + "regions_with_capacity_available": [ + { + "description": "Utah, USA", + "name": "us-west-3" + } + ] }, "gpu_1x_h100_sxm5": { "instance_type": { @@ -142,7 +166,12 @@ "vcpus": 26 } }, - "regions_with_capacity_available": [] + "regions_with_capacity_available": [ + { + "description": "Central Texas, USA", + "name": "us-south-3" + } + ] }, "gpu_1x_rtx6000": { "instance_type": { @@ -217,7 +246,12 @@ "vcpus": 52 } }, - "regions_with_capacity_available": [] + "regions_with_capacity_available": [ + { + "description": "Central Texas, USA", + "name": "us-south-3" + } + ] }, "gpu_4x_a100": { "instance_type": { @@ -304,10 +338,6 @@ { "description": "Arizona, USA", "name": "us-west-2" - }, - { - "description": "Germany", - "name": "europe-central-1" } ] }, @@ -324,7 +354,16 @@ "vcpus": 240 } }, - "regions_with_capacity_available": [] + "regions_with_capacity_available": [ + { + "description": "Illinois, USA", + "name": "us-midwest-1" + }, + { + "description": "Sydney, Australia", + "name": "australia-east-1" + } + ] }, "gpu_8x_b200_sxm6": { "instance_type": { @@ -354,7 +393,12 @@ "vcpus": 208 } }, - "regions_with_capacity_available": [] + "regions_with_capacity_available": [ + { + "description": "North Texas, USA", + "name": "us-south-2" + } + ] }, "gpu_8x_v100": { "instance_type": { diff --git a/test/fixtures/latitude/_metadata.json b/test/fixtures/latitude/_metadata.json new file mode 100644 index 000000000..4ea9d87d3 --- /dev/null +++ b/test/fixtures/latitude/_metadata.json @@ -0,0 +1,9 @@ +{ + "cloud": "latitude", + "recorded_at": "2026-02-13T05:00:30Z", + "fixtures": { + "ssh_keys": {"endpoint": "/ssh_keys", "recorded_at": "2026-02-13T05:00:28Z"}, + "plans": {"endpoint": "/plans", "recorded_at": "2026-02-13T05:00:30Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T05:00:30Z"} + } +} diff --git a/test/fixtures/latitude/plans.json b/test/fixtures/latitude/plans.json new file mode 100644 index 000000000..296a1f71e --- /dev/null +++ b/test/fixtures/latitude/plans.json @@ -0,0 +1,4901 @@ +{ + "data": [ + { + "attributes": { + "features": [ + "ssh", + "user_data" + ], + "name": "c2.small.x86", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + }, + "USD": { + "hour": 0.18, + "month": 92.0, + "year": 773.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + }, + "USD": { + "hour": 0.18, + "month": 92.0, + "year": 773.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + }, + "USD": { + "hour": 0.18, + "month": 92.0, + "year": 773.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "SAN3" + ], + "in_stock": [ + "SAN3" + ] + }, + "name": "Chile", + "pricing": { + "BRL": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + }, + "USD": { + "hour": 0.18, + "month": 92.0, + "year": 773.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [ + "TYO3" + ] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + }, + "USD": { + "hour": 0.18, + "month": 92.0, + "year": 773.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "MEX2" + ], + "in_stock": [] + }, + "name": "Mexico", + "pricing": { + "BRL": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + }, + "USD": { + "hour": 0.18, + "month": 92.0, + "year": 773.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [ + "LON" + ] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 0.9, + "month": 460.0, + "year": 3864.0 + }, + "USD": { + "hour": 0.18, + "month": 92.0, + "year": 773.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "BUE" + ], + "in_stock": [] + }, + "name": "Argentina", + "pricing": { + "BRL": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + }, + "USD": { + "hour": 0.18, + "month": 92.0, + "year": 773.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "BGT" + ], + "in_stock": [] + }, + "name": "Colombia", + "pricing": { + "BRL": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + }, + "USD": { + "hour": 0.18, + "month": 92.0, + "year": 773.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [ + "FRA", + "FRA2" + ] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + }, + "USD": { + "hour": 0.18, + "month": 92.0, + "year": 773.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + }, + "USD": { + "hour": 0.18, + "month": 92.0, + "year": 773.0 + } + }, + "stock_level": "unavailable" + } + ], + "slug": "c2-small-x86", + "specs": { + "cpu": { + "clock": 3.6, + "cores": 4, + "count": 1, + "type": "E-2234" + }, + "drives": [ + { + "count": 1, + "size": "500 GB", + "type": "SSD" + } + ], + "gpu": {}, + "memory": { + "total": 32 + }, + "nics": [ + { + "count": 1, + "type": "1 Gbps" + } + ] + } + }, + "id": "plan_2X6KG5mA5yPBM", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "c2.medium.x86", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 1.75, + "month": 894.0, + "year": 7510.0 + }, + "USD": { + "hour": 0.35, + "month": 179.0, + "year": 1504.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "MIA2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 1.75, + "month": 894.0, + "year": 7510.0 + }, + "USD": { + "hour": 0.35, + "month": 179.0, + "year": 1504.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 1.75, + "month": 894.0, + "year": 7510.0 + }, + "USD": { + "hour": 0.35, + "month": 179.0, + "year": 1504.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "SAN3" + ], + "in_stock": [ + "SAN3" + ] + }, + "name": "Chile", + "pricing": { + "BRL": { + "hour": 1.75, + "month": 894.0, + "year": 7510.0 + }, + "USD": { + "hour": 0.35, + "month": 179.0, + "year": 1504.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 1.75, + "month": 894.0, + "year": 7510.0 + }, + "USD": { + "hour": 0.35, + "month": 179.0, + "year": 1504.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "MEX2" + ], + "in_stock": [] + }, + "name": "Mexico", + "pricing": { + "BRL": { + "hour": 1.75, + "month": 894.0, + "year": 7510.0 + }, + "USD": { + "hour": 0.35, + "month": 179.0, + "year": 1504.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [ + "LON" + ] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 1.75, + "month": 894.0, + "year": 7510.0 + }, + "USD": { + "hour": 0.35, + "month": 179.0, + "year": 1504.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [ + "FRA" + ] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 1.75, + "month": 894.0, + "year": 7510.0 + }, + "USD": { + "hour": 0.35, + "month": 179.0, + "year": 1504.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 1.73, + "month": 1200.0, + "year": 10608.0 + }, + "USD": { + "hour": 0.35, + "month": 243.0, + "year": 2146.0 + } + }, + "stock_level": "unavailable" + } + ], + "slug": "c2-medium-x86", + "specs": { + "cpu": { + "clock": 3.4, + "cores": 8, + "count": 1, + "type": "E-2278G" + }, + "drives": [ + { + "count": 2, + "size": "500 GB", + "type": "SSD" + } + ], + "gpu": {}, + "memory": { + "total": 64 + }, + "nics": [ + { + "count": 1, + "type": "10 Gbps" + } + ] + } + }, + "id": "plan_GdmkONWoNjL1b", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "c2.large.x86", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 1.95, + "month": 996.0, + "year": 8366.0 + }, + "USD": { + "hour": 0.39, + "month": 199.0, + "year": 1672.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "CHI" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 1.95, + "month": 996.0, + "year": 8366.0 + }, + "USD": { + "hour": 0.39, + "month": 199.0, + "year": 1672.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 1.95, + "month": 996.0, + "year": 8366.0 + }, + "USD": { + "hour": 0.39, + "month": 199.0, + "year": 1672.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAN3" + ], + "in_stock": [] + }, + "name": "Chile", + "pricing": { + "BRL": { + "hour": 1.95, + "month": 996.0, + "year": 8366.0 + }, + "USD": { + "hour": 0.39, + "month": 199.0, + "year": 1672.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 1.95, + "month": 996.0, + "year": 8366.0 + }, + "USD": { + "hour": 0.39, + "month": 199.0, + "year": 1672.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "MEX2" + ], + "in_stock": [] + }, + "name": "Mexico", + "pricing": { + "BRL": { + "hour": 1.95, + "month": 996.0, + "year": 8366.0 + }, + "USD": { + "hour": 0.39, + "month": 199.0, + "year": 1672.0 + } + }, + "stock_level": "unavailable" + } + ], + "slug": "c2-large-x86", + "specs": { + "cpu": { + "clock": 2.2, + "cores": 20, + "count": 2, + "type": "Silver 4210" + }, + "drives": [ + { + "count": 2, + "size": "1 TB", + "type": "SSD" + } + ], + "gpu": {}, + "memory": { + "total": 128 + }, + "nics": [ + { + "count": 1, + "type": "10 Gbps" + } + ] + } + }, + "id": "plan_zdexA0q8alQVv", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "user_data" + ], + "name": "c1.tiny.x86", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 0.54, + "month": 276.0, + "year": 2318.0 + }, + "USD": { + "hour": 0.09, + "month": 46.0, + "year": 386.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "MIA2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 0.54, + "month": 276.0, + "year": 2318.0 + }, + "USD": { + "hour": 0.09, + "month": 46.0, + "year": 386.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 0.54, + "month": 276.0, + "year": 2318.0 + }, + "USD": { + "hour": 0.09, + "month": 46.0, + "year": 386.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAN3" + ], + "in_stock": [] + }, + "name": "Chile", + "pricing": { + "BRL": { + "hour": 0.54, + "month": 276.0, + "year": 2318.0 + }, + "USD": { + "hour": 0.09, + "month": 46.0, + "year": 386.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 0.54, + "month": 276.0, + "year": 2318.0 + }, + "USD": { + "hour": 0.09, + "month": 46.0, + "year": 386.0 + } + }, + "stock_level": "unavailable" + } + ], + "slug": "c1-tiny-x86", + "specs": { + "cpu": { + "clock": 3.5, + "cores": 4, + "count": 1, + "type": "E-2134" + }, + "drives": [ + { + "count": 1, + "size": "500 GB", + "type": "SSD" + } + ], + "gpu": {}, + "memory": { + "total": 16 + }, + "nics": [ + { + "count": 1, + "type": "1 Gbps" + } + ] + } + }, + "id": "plan_D6B9VaLRN7vrk", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "s3.large.x86", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 6.35, + "month": 3245.0, + "year": 27258.0 + }, + "USD": { + "hour": 1.27, + "month": 649.0, + "year": 5452.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 6.35, + "month": 3245.0, + "year": 27258.0 + }, + "USD": { + "hour": 1.27, + "month": 649.0, + "year": 5452.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 6.35, + "month": 3245.0, + "year": 27258.0 + }, + "USD": { + "hour": 1.27, + "month": 649.0, + "year": 5452.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAN3" + ], + "in_stock": [] + }, + "name": "Chile", + "pricing": { + "BRL": { + "hour": 6.35, + "month": 3245.0, + "year": 27258.0 + }, + "USD": { + "hour": 1.27, + "month": 649.0, + "year": 5452.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 6.35, + "month": 3245.0, + "year": 27258.0 + }, + "USD": { + "hour": 1.27, + "month": 649.0, + "year": 5452.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "MEX2" + ], + "in_stock": [] + }, + "name": "Mexico", + "pricing": { + "BRL": { + "hour": 6.35, + "month": 3245.0, + "year": 27258.0 + }, + "USD": { + "hour": 1.27, + "month": 956.0, + "year": 8030.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [ + "LON" + ] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 6.35, + "month": 3245.0, + "year": 27258.0 + }, + "USD": { + "hour": 1.27, + "month": 649.0, + "year": 5452.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "BUE" + ], + "in_stock": [] + }, + "name": "Argentina", + "pricing": { + "BRL": { + "hour": 6.35, + "month": 3245.0, + "year": 27258.0 + }, + "USD": { + "hour": 1.27, + "month": 649.0, + "year": 5452.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 6.35, + "month": 3245.0, + "year": 27258.0 + }, + "USD": { + "hour": 1.27, + "month": 649.0, + "year": 5452.0 + } + }, + "stock_level": "unavailable" + } + ], + "slug": "s3-large-x86", + "specs": { + "cpu": { + "clock": 2.85, + "cores": 24, + "count": 1, + "type": "AMD 7443P" + }, + "drives": [ + { + "count": 2, + "size": "3.8 TB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 512 + }, + "nics": [ + { + "count": 1, + "type": "10Gbps" + } + ] + } + }, + "id": "plan_y3ZXaDKW5p2ml", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "user_data" + ], + "name": "c3.small.x86", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 1.15, + "month": 588.0, + "year": 4939.0 + }, + "USD": { + "hour": 0.23, + "month": 118.0, + "year": 991.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 1.15, + "month": 588.0, + "year": 4939.0 + }, + "USD": { + "hour": 0.23, + "month": 118.0, + "year": 991.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAN3" + ], + "in_stock": [] + }, + "name": "Chile", + "pricing": { + "BRL": { + "hour": 1.15, + "month": 588.0, + "year": 4939.0 + }, + "USD": { + "hour": 0.23, + "month": 118.0, + "year": 991.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 1.15, + "month": 588.0, + "year": 4939.0 + }, + "USD": { + "hour": 0.23, + "month": 118.0, + "year": 991.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "MEX2" + ], + "in_stock": [ + "MEX2" + ] + }, + "name": "Mexico", + "pricing": { + "BRL": { + "hour": 1.3, + "month": 664.0, + "year": 5578.0 + }, + "USD": { + "hour": 0.23, + "month": 118.0, + "year": 991.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "BUE" + ], + "in_stock": [] + }, + "name": "Argentina", + "pricing": { + "BRL": { + "hour": 1.15, + "month": 588.0, + "year": 4939.0 + }, + "USD": { + "hour": 0.23, + "month": 118.0, + "year": 991.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "BGT" + ], + "in_stock": [] + }, + "name": "Colombia", + "pricing": { + "BRL": { + "hour": 1.15, + "month": 588.0, + "year": 4939.0 + }, + "USD": { + "hour": 0.23, + "month": 118.0, + "year": 991.0 + } + }, + "stock_level": "unavailable" + } + ], + "slug": "c3-small-x86", + "specs": { + "cpu": { + "clock": 3.5, + "cores": 6, + "count": 1, + "type": "E-2386G" + }, + "drives": [ + { + "count": 1, + "size": "800 GB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 32 + }, + "nics": [ + { + "count": 1, + "type": "10 Gbps" + } + ] + } + }, + "id": "plan_gXQvNe93NzpbP", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "m3.large.x86", + "regions": [ + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "NYC", + "ASH" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 9.2, + "month": 4701.0, + "year": 39488.0 + }, + "USD": { + "hour": 1.84, + "month": 940.0, + "year": 7896.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 11.0, + "month": 5621.0, + "year": 47216.0 + }, + "USD": { + "hour": 2.2, + "month": 1124.0, + "year": 9442.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 9.2, + "month": 4701.0, + "year": 39488.0 + }, + "USD": { + "hour": 1.84, + "month": 940.0, + "year": 7896.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 9.2, + "month": 4701.0, + "year": 39488.0 + }, + "USD": { + "hour": 1.84, + "month": 940.0, + "year": 7896.0 + } + }, + "stock_level": "unavailable" + } + ], + "slug": "m3-large-x86", + "specs": { + "cpu": { + "clock": 2.8, + "cores": 32, + "count": 1, + "type": "AMD 7543P" + }, + "drives": [ + { + "count": 2, + "size": "3.8TB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 1024 + }, + "nics": [ + { + "count": 1, + "type": "2 x 10 Gbps" + } + ] + } + }, + "id": "plan_jv6m5JADNLPeE", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "c3.large.x86", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 5.95, + "month": 3040.0, + "year": 25536.0 + }, + "USD": { + "hour": 1.19, + "month": 608.0, + "year": 5107.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "LAX", + "NYC", + "ASH", + "MIA2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 4.85, + "month": 2478.0, + "year": 20815.0 + }, + "USD": { + "hour": 0.97, + "month": 496.0, + "year": 4166.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [ + "SYD2" + ] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 6.72, + "month": 3434.0, + "year": 28846.0 + }, + "USD": { + "hour": 1.19, + "month": 608.0, + "year": 5107.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 5.95, + "month": 3040.0, + "year": 25536.0 + }, + "USD": { + "hour": 1.19, + "month": 608.0, + "year": 5107.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "MEX2" + ], + "in_stock": [ + "MEX2" + ] + }, + "name": "Mexico", + "pricing": { + "BRL": { + "hour": 5.95, + "month": 3040.0, + "year": 25536.0 + }, + "USD": { + "hour": 1.19, + "month": 608.0, + "year": 5107.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [ + "LON" + ] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 4.85, + "month": 2478.0, + "year": 20815.0 + }, + "USD": { + "hour": 0.97, + "month": 496.0, + "year": 4166.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "BUE" + ], + "in_stock": [] + }, + "name": "Argentina", + "pricing": { + "BRL": { + "hour": 5.95, + "month": 3040.0, + "year": 25536.0 + }, + "USD": { + "hour": 1.19, + "month": 608.0, + "year": 5107.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [ + "FRA" + ] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 4.85, + "month": 2478.0, + "year": 20815.0 + }, + "USD": { + "hour": 0.97, + "month": 496.0, + "year": 4166.0 + } + }, + "stock_level": "low" + } + ], + "slug": "c3-large-x86", + "specs": { + "cpu": { + "clock": 2.85, + "cores": 24, + "count": 1, + "type": "AMD 7443P" + }, + "drives": [ + { + "count": 2, + "size": "1.9 TB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 256 + }, + "nics": [ + { + "count": 1, + "type": "2 x 10 Gbps" + } + ] + } + }, + "id": "plan_M3Beabem5LnbE", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "s2.small.x86", + "regions": [ + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "NYC", + "CHI", + "ASH", + "MIA2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 1.64, + "month": 838.0, + "year": 7039.0 + }, + "USD": { + "hour": 0.29, + "month": 148.0, + "year": 1243.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 1.64, + "month": 838.0, + "year": 7039.0 + }, + "USD": { + "hour": 0.29, + "month": 148.0, + "year": 1243.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [ + "TYO3" + ] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 1.64, + "month": 838.0, + "year": 7039.0 + }, + "USD": { + "hour": 0.29, + "month": 148.0, + "year": 1243.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [ + "LON2" + ] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 1.64, + "month": 838.0, + "year": 7039.0 + }, + "USD": { + "hour": 0.29, + "month": 148.0, + "year": 1243.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [ + "FRA" + ] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 1.64, + "month": 838.0, + "year": 7039.0 + }, + "USD": { + "hour": 0.29, + "month": 148.0, + "year": 1243.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [ + "SGP" + ] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 1.64, + "month": 838.0, + "year": 7039.0 + }, + "USD": { + "hour": 0.29, + "month": 148.0, + "year": 1243.0 + } + }, + "stock_level": "low" + } + ], + "slug": "s2-small-x86", + "specs": { + "cpu": { + "clock": 3.7, + "cores": 6, + "count": 1, + "type": "E-2176G" + }, + "drives": [ + { + "count": 2, + "size": "500 GB", + "type": "SSD" + }, + { + "count": 2, + "size": "1 TB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 32 + }, + "nics": [ + { + "count": 1, + "type": "1 Gbps" + } + ] + } + }, + "id": "plan_dexA0qn8NlQVv", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "g3.h100.medium", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 45.03, + "month": 23010.0, + "year": 193284.0 + }, + "USD": { + "hour": 7.97, + "month": 4073.0, + "year": 34213.0 + } + }, + "stock_level": "unavailable" + } + ], + "slug": "g3-h100-medium", + "specs": { + "cpu": { + "clock": 3.25, + "cores": 64, + "count": 2, + "type": "AMD 9354" + }, + "drives": [ + { + "count": 2, + "size": "3.8TB", + "type": "NVME" + } + ], + "gpu": { + "count": 4, + "interconnect": "Paired-NVLink, PCIe based", + "type": "NVIDIA H100 80GB NVLink", + "vram_per_gpu": 80 + }, + "memory": { + "total": 768 + }, + "nics": [ + { + "count": 2, + "type": "10 Gbps" + } + ] + } + }, + "id": "plan_3xMV0xWKamQp7", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "g3.h100.small", + "regions": [ + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu24_ml_in_a_box" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 13.62, + "month": 6960.0, + "year": 58464.0 + }, + "USD": { + "hour": 2.41, + "month": 1232.0, + "year": 10349.0 + } + }, + "stock_level": "high" + } + ], + "slug": "g3-h100-small", + "specs": { + "cpu": { + "clock": 3.0, + "cores": 16, + "count": 1, + "type": "AMD 9124" + }, + "drives": [ + { + "count": 2, + "size": "3.8TB", + "type": "NVME" + } + ], + "gpu": { + "count": 1, + "interconnect": "Paired-NVLink, PCIe based", + "type": "NVIDIA H100 80GB", + "vram_per_gpu": 80 + }, + "memory": { + "total": 192 + }, + "nics": [ + { + "count": 2, + "type": "10 Gbps" + } + ] + } + }, + "id": "plan_lnow0Ovwa9x1q", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "m4.metal.small", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 2.88, + "month": 1472.0, + "year": 12365.0 + }, + "USD": { + "hour": 0.51, + "month": 261.0, + "year": 2192.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "NYC", + "CHI", + "MIA2", + "LAX2", + "SJC2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 2.09, + "month": 1068.0, + "year": 8971.0 + }, + "USD": { + "hour": 0.37, + "month": 189.0, + "year": 1588.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [ + "SYD2" + ] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 2.88, + "month": 1472.0, + "year": 12365.0 + }, + "USD": { + "hour": 0.51, + "month": 261.0, + "year": 2192.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "SAN3" + ], + "in_stock": [ + "SAN3" + ] + }, + "name": "Chile", + "pricing": { + "BRL": { + "hour": 2.88, + "month": 1472.0, + "year": 12365.0 + }, + "USD": { + "hour": 0.51, + "month": 261.0, + "year": 2192.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 2.49, + "month": 1272.0, + "year": 10685.0 + }, + "USD": { + "hour": 0.44, + "month": 225.0, + "year": 1890.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "MEX2" + ], + "in_stock": [ + "MEX2" + ] + }, + "name": "Mexico", + "pricing": { + "BRL": { + "hour": 2.88, + "month": 1472.0, + "year": 12365.0 + }, + "USD": { + "hour": 0.51, + "month": 261.0, + "year": 2192.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 2.09, + "month": 1068.0, + "year": 8971.0 + }, + "USD": { + "hour": 0.37, + "month": 189.0, + "year": 1588.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "BUE" + ], + "in_stock": [] + }, + "name": "Argentina", + "pricing": { + "BRL": { + "hour": 2.55, + "month": 1303.0, + "year": 10945.0 + }, + "USD": { + "hour": 0.51, + "month": 261.0, + "year": 2192.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "BGT" + ], + "in_stock": [ + "BGT" + ] + }, + "name": "Colombia", + "pricing": { + "BRL": { + "hour": 2.88, + "month": 1472.0, + "year": 12365.0 + }, + "USD": { + "hour": 0.51, + "month": 261.0, + "year": 2192.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 2.09, + "month": 1068.0, + "year": 8971.0 + }, + "USD": { + "hour": 0.37, + "month": 189.0, + "year": 1588.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [ + "SGP" + ] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 2.2, + "month": 1124.0, + "year": 9442.0 + }, + "USD": { + "hour": 0.44, + "month": 225.0, + "year": 1890.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "AMS" + ], + "in_stock": [ + "AMS" + ] + }, + "name": "Netherlands", + "pricing": { + "BRL": { + "hour": 2.09, + "month": 1068.0, + "year": 8971.0 + }, + "USD": { + "hour": 0.37, + "month": 189.0, + "year": 1588.0 + } + }, + "stock_level": "high" + } + ], + "slug": "m4-metal-small", + "specs": { + "cpu": { + "clock": 3.8, + "cores": 6, + "count": 1, + "type": "AMD 4244P" + }, + "drives": [ + { + "count": 2, + "size": "960GB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 64 + }, + "nics": [ + { + "count": 1, + "type": "2 x 10Gbps" + } + ] + } + }, + "id": "plan_8mop5gXvajxvn", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data", + "sev" + ], + "name": "m4.metal.medium", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 5.65, + "month": 2887.0, + "year": 24251.0 + }, + "USD": { + "hour": 1.13, + "month": 577.0, + "year": 4847.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 4.45, + "month": 2274.0, + "year": 19102.0 + }, + "USD": { + "hour": 0.89, + "month": 455.0, + "year": 3822.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 5.65, + "month": 2887.0, + "year": 24251.0 + }, + "USD": { + "hour": 1.13, + "month": 577.0, + "year": 4847.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAN3" + ], + "in_stock": [] + }, + "name": "Chile", + "pricing": { + "BRL": { + "hour": 5.65, + "month": 2887.0, + "year": 24251.0 + }, + "USD": { + "hour": 1.13, + "month": 577.0, + "year": 4847.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 5.1, + "month": 2606.0, + "year": 21890.0 + }, + "USD": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "MEX2" + ], + "in_stock": [ + "MEX2" + ] + }, + "name": "Mexico", + "pricing": { + "BRL": { + "hour": 5.65, + "month": 2887.0, + "year": 24251.0 + }, + "USD": { + "hour": 1.13, + "month": 577.0, + "year": 4847.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [ + "LON", + "LON2" + ] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 4.45, + "month": 2274.0, + "year": 19102.0 + }, + "USD": { + "hour": 0.89, + "month": 455.0, + "year": 3822.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "BUE" + ], + "in_stock": [] + }, + "name": "Argentina", + "pricing": { + "BRL": { + "hour": 5.65, + "month": 2887.0, + "year": 24251.0 + }, + "USD": { + "hour": 1.13, + "month": 577.0, + "year": 4847.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "BGT" + ], + "in_stock": [ + "BGT" + ] + }, + "name": "Colombia", + "pricing": { + "BRL": { + "hour": 5.65, + "month": 2887.0, + "year": 24251.0 + }, + "USD": { + "hour": 1.13, + "month": 577.0, + "year": 4847.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [ + "FRA", + "FRA2" + ] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 4.45, + "month": 2274.0, + "year": 19102.0 + }, + "USD": { + "hour": 0.89, + "month": 455.0, + "year": 3822.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 5.1, + "month": 2606.0, + "year": 21890.0 + }, + "USD": { + "hour": 1.02, + "month": 521.0, + "year": 4376.0 + } + }, + "stock_level": "unavailable" + } + ], + "slug": "m4-metal-medium", + "specs": { + "cpu": { + "clock": 3.0, + "cores": 16, + "count": 1, + "type": "AMD 9124" + }, + "drives": [ + { + "count": 2, + "size": "480GB", + "type": "NVME" + }, + { + "count": 2, + "size": "1.9TB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 128 + }, + "nics": [ + { + "count": 1, + "type": "2 x 10Gbps" + } + ] + } + }, + "id": "plan_zZOo0l3eaKQYb", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data", + "sev" + ], + "name": "m4.metal.large", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 7.91, + "month": 4042.0, + "year": 33953.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "SJC2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 7.91, + "month": 4042.0, + "year": 33953.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 7.91, + "month": 4042.0, + "year": 33953.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 7.91, + "month": 4042.0, + "year": 33953.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "MEX2" + ], + "in_stock": [ + "MEX2" + ] + }, + "name": "Mexico", + "pricing": { + "BRL": { + "hour": 7.91, + "month": 4042.0, + "year": 33953.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [ + "LON", + "LON2" + ] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 7.91, + "month": 4042.0, + "year": 33953.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "BUE" + ], + "in_stock": [] + }, + "name": "Argentina", + "pricing": { + "BRL": { + "hour": 7.91, + "month": 4042.0, + "year": 33953.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [ + "FRA", + "FRA2" + ] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 7.91, + "month": 4042.0, + "year": 33953.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 7.91, + "month": 4042.0, + "year": 33953.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "AMS" + ], + "in_stock": [ + "AMS" + ] + }, + "name": "Netherlands", + "pricing": { + "BRL": { + "hour": 7.91, + "month": 4042.0, + "year": 33953.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "low" + } + ], + "slug": "m4-metal-large", + "specs": { + "cpu": { + "clock": 2.9, + "cores": 24, + "count": 1, + "type": "AMD 9254" + }, + "drives": [ + { + "count": 2, + "size": "480GB", + "type": "NVME" + }, + { + "count": 2, + "size": "3.8TB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 384 + }, + "nics": [ + { + "count": 1, + "type": "2 x 10Gbps" + } + ] + } + }, + "id": "plan_g3PYaRB3ayozM", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data", + "sev" + ], + "name": "rs4.metal.large", + "regions": [ + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [ + "SAO2" + ] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 15.93, + "month": 8140.0, + "year": 68376.0 + }, + "USD": { + "hour": 2.82, + "month": 1441.0, + "year": 12104.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 11.7, + "month": 5979.0, + "year": 50224.0 + }, + "USD": { + "hour": 2.07, + "month": 1058.0, + "year": 8887.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 15.93, + "month": 8140.0, + "year": 68376.0 + }, + "USD": { + "hour": 2.82, + "month": 1441.0, + "year": 12104.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [ + "TYO3", + "TYO4" + ] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 12.71, + "month": 6495.0, + "year": 54558.0 + }, + "USD": { + "hour": 2.25, + "month": 1150.0, + "year": 9660.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [ + "LON2" + ] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 11.7, + "month": 5979.0, + "year": 50224.0 + }, + "USD": { + "hour": 2.07, + "month": 1058.0, + "year": 8887.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [ + "FRA", + "FRA2" + ] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 11.7, + "month": 5979.0, + "year": 50224.0 + }, + "USD": { + "hour": 2.07, + "month": 1058.0, + "year": 8887.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [ + "SGP" + ] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 12.71, + "month": 6495.0, + "year": 54558.0 + }, + "USD": { + "hour": 2.25, + "month": 1150.0, + "year": 9660.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "AMS" + ], + "in_stock": [ + "AMS" + ] + }, + "name": "Netherlands", + "pricing": { + "BRL": { + "hour": 11.7, + "month": 5979.0, + "year": 50224.0 + }, + "USD": { + "hour": 2.07, + "month": 1058.0, + "year": 8887.0 + } + }, + "stock_level": "high" + } + ], + "slug": "rs4-metal-large", + "specs": { + "cpu": { + "clock": 3.25, + "cores": 32, + "count": 1, + "type": "AMD 9354P" + }, + "drives": [ + { + "count": 2, + "size": "480GB", + "type": "NVME" + }, + { + "count": 2, + "size": "8TB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 768 + }, + "nics": [ + { + "count": 1, + "type": "2 x 100Gbps" + } + ] + } + }, + "id": "plan_mJyo0vmLaDkw9", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data", + "sev" + ], + "name": "rs4.metal.xlarge", + "regions": [ + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 19.89, + "month": 10164.0, + "year": 85378.0 + }, + "USD": { + "hour": 3.52, + "month": 1799.0, + "year": 15112.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [ + "TYO3", + "TYO4" + ] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 21.64, + "month": 11058.0, + "year": 92887.0 + }, + "USD": { + "hour": 3.83, + "month": 1957.0, + "year": 16439.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [ + "LON2" + ] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 19.89, + "month": 10164.0, + "year": 85378.0 + }, + "USD": { + "hour": 3.52, + "month": 1799.0, + "year": 15112.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [ + "FRA2" + ] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 19.89, + "month": 10164.0, + "year": 85378.0 + }, + "USD": { + "hour": 3.52, + "month": 1799.0, + "year": 15112.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [ + "SGP" + ] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 21.64, + "month": 11058.0, + "year": 92887.0 + }, + "USD": { + "hour": 3.83, + "month": 1957.0, + "year": 16439.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "AMS" + ], + "in_stock": [ + "AMS" + ] + }, + "name": "Netherlands", + "pricing": { + "BRL": { + "hour": 19.89, + "month": 10164.0, + "year": 85378.0 + }, + "USD": { + "hour": 3.52, + "month": 1799.0, + "year": 15112.0 + } + }, + "stock_level": "low" + } + ], + "slug": "rs4-metal-xlarge", + "specs": { + "cpu": { + "clock": 3.1, + "cores": 64, + "count": 1, + "type": "AMD 9554P" + }, + "drives": [ + { + "count": 2, + "size": "480GB", + "type": "NVME" + }, + { + "count": 4, + "size": "8TB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 1536 + }, + "nics": [ + { + "count": 1, + "type": "2 x 100Gbps" + } + ] + } + }, + "id": "plan_ewOM5GAb0p8ER", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "f4.metal.medium", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 8.81, + "month": 4502.0, + "year": 37817.0 + }, + "USD": { + "hour": 1.56, + "month": 797.0, + "year": 6695.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts", + "ipxe" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 5.45, + "month": 2785.0, + "year": 23394.0 + }, + "USD": { + "hour": 1.09, + "month": 557.0, + "year": 4679.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [ + "SYD2" + ] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 8.81, + "month": 4502.0, + "year": 37817.0 + }, + "USD": { + "hour": 1.56, + "month": 797.0, + "year": 6695.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [ + "TYO3", + "TYO4" + ] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 7.0, + "month": 3577.0, + "year": 30047.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [ + "LON2" + ] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 5.45, + "month": 2785.0, + "year": 23394.0 + }, + "USD": { + "hour": 1.09, + "month": 557.0, + "year": 4679.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "BUE" + ], + "in_stock": [ + "BUE" + ] + }, + "name": "Argentina", + "pricing": { + "BRL": { + "hour": 7.8, + "month": 3986.0, + "year": 33482.0 + }, + "USD": { + "hour": 1.56, + "month": 797.0, + "year": 6695.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [ + "FRA2" + ] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 5.45, + "month": 2785.0, + "year": 23394.0 + }, + "USD": { + "hour": 1.09, + "month": 557.0, + "year": 4679.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [ + "SGP" + ] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 7.0, + "month": 3577.0, + "year": 30047.0 + }, + "USD": { + "hour": 1.4, + "month": 715.0, + "year": 6006.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "AMS" + ], + "in_stock": [ + "AMS" + ] + }, + "name": "Netherlands", + "pricing": { + "BRL": { + "hour": 6.16, + "month": 3148.0, + "year": 26443.0 + }, + "USD": { + "hour": 1.09, + "month": 557.0, + "year": 4679.0 + } + }, + "stock_level": "high" + } + ], + "slug": "f4-metal-medium", + "specs": { + "cpu": { + "clock": 4.5, + "cores": 16, + "count": 1, + "type": "AMD 4564P" + }, + "drives": [ + { + "count": 2, + "size": "480GB", + "type": "NVME" + }, + { + "count": 2, + "size": "1.9TB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 192 + }, + "nics": [ + { + "count": 1, + "type": "2 x 10Gbps" + } + ] + } + }, + "id": "plan_w8MVaone523gj", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data", + "sev" + ], + "name": "f4.metal.large", + "regions": [ + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [ + "SAO2" + ] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 17.52, + "month": 8953.0, + "year": 75205.0 + }, + "USD": { + "hour": 3.1, + "month": 1584.0, + "year": 13306.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "NYC", + "CHI", + "ASH", + "LAX2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 10.85, + "month": 5544.0, + "year": 46570.0 + }, + "USD": { + "hour": 2.17, + "month": 1109.0, + "year": 9316.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [ + "TYO4" + ] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 12.5, + "month": 6388.0, + "year": 53659.0 + }, + "USD": { + "hour": 2.5, + "month": 1277.0, + "year": 10727.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 10.85, + "month": 5544.0, + "year": 46570.0 + }, + "USD": { + "hour": 2.17, + "month": 1109.0, + "year": 9316.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 10.85, + "month": 5544.0, + "year": 46570.0 + }, + "USD": { + "hour": 2.17, + "month": 1109.0, + "year": 9316.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [ + "SGP" + ] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 12.5, + "month": 6388.0, + "year": 53659.0 + }, + "USD": { + "hour": 2.5, + "month": 1277.0, + "year": 10727.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "AMS" + ], + "in_stock": [ + "AMS" + ] + }, + "name": "Netherlands", + "pricing": { + "BRL": { + "hour": 12.26, + "month": 6265.0, + "year": 52626.0 + }, + "USD": { + "hour": 2.17, + "month": 1109.0, + "year": 9316.0 + } + }, + "stock_level": "low" + } + ], + "slug": "f4-metal-large", + "specs": { + "cpu": { + "clock": 4.1, + "cores": 24, + "count": 1, + "type": "AMD 9275F" + }, + "drives": [ + { + "count": 2, + "size": "480GB", + "type": "NVME" + }, + { + "count": 2, + "size": "3.8TB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 768 + }, + "nics": [ + { + "count": 1, + "type": "2 x 100Gbps" + } + ] + } + }, + "id": "plan_7vRENk3gNdPyk", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "f4.metal.small", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "SAO", + "SAO2" + ], + "in_stock": [] + }, + "name": "Brazil", + "pricing": { + "BRL": { + "hour": 4.41, + "month": 2254.0, + "year": 18934.0 + }, + "USD": { + "hour": 0.78, + "month": 399.0, + "year": 3352.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "DAL", + "NYC", + "CHI", + "SJC2" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 2.85, + "month": 1456.0, + "year": 12230.0 + }, + "USD": { + "hour": 0.57, + "month": 291.0, + "year": 2444.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "SYD", + "SYD2" + ], + "in_stock": [ + "SYD2" + ] + }, + "name": "Australia", + "pricing": { + "BRL": { + "hour": 4.41, + "month": 2254.0, + "year": 18934.0 + }, + "USD": { + "hour": 0.78, + "month": 399.0, + "year": 3352.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [ + "TYO4" + ] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 3.35, + "month": 1712.0, + "year": 14381.0 + }, + "USD": { + "hour": 0.67, + "month": 342.0, + "year": 2873.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [ + "ubuntu_24_04_x64_lts", + "ubuntu_22_04_x64_lts" + ], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [ + "LON2" + ] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 2.85, + "month": 1456.0, + "year": 12230.0 + }, + "USD": { + "hour": 0.57, + "month": 291.0, + "year": 2444.0 + } + }, + "stock_level": "high" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "BUE" + ], + "in_stock": [] + }, + "name": "Argentina", + "pricing": { + "BRL": { + "hour": 3.9, + "month": 1993.0, + "year": 16741.0 + }, + "USD": { + "hour": 0.78, + "month": 399.0, + "year": 3352.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 2.85, + "month": 1456.0, + "year": 12230.0 + }, + "USD": { + "hour": 0.57, + "month": 291.0, + "year": 2444.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [ + "SGP" + ] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 3.35, + "month": 1712.0, + "year": 14381.0 + }, + "USD": { + "hour": 0.67, + "month": 342.0, + "year": 2873.0 + } + }, + "stock_level": "medium" + }, + { + "deploys_instantly": [ + "ubuntu_22_04_x64_lts", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "AMS" + ], + "in_stock": [ + "AMS" + ] + }, + "name": "Netherlands", + "pricing": { + "BRL": { + "hour": 3.22, + "month": 1645.0, + "year": 13818.0 + }, + "USD": { + "hour": 0.57, + "month": 291.0, + "year": 2444.0 + } + }, + "stock_level": "high" + } + ], + "slug": "f4-metal-small", + "specs": { + "cpu": { + "clock": 4.4, + "cores": 12, + "count": 1, + "type": "AMD 4484PX" + }, + "drives": [ + { + "count": 2, + "size": "960GB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 96 + }, + "nics": [ + { + "count": 1, + "type": "2 x 10Gbps" + } + ] + } + }, + "id": "plan_PVwea4vvNB9OQ", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data" + ], + "name": "g4.rtx6kpro.large", + "regions": [ + { + "deploys_instantly": [ + "ubuntu24_ml_in_a_box", + "ubuntu_24_04_x64_lts" + ], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [ + "CHI", + "ASH" + ] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 110.18, + "month": 56302.0, + "year": 472937.0 + }, + "USD": { + "hour": 19.5, + "month": 9964.0, + "year": 83698.0 + } + }, + "stock_level": "low" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 0.0, + "month": 0.0, + "year": 0.0 + }, + "USD": { + "hour": null, + "month": null, + "year": null + } + }, + "stock_level": "unavailable" + } + ], + "slug": "g4-rtx6kpro-large", + "specs": { + "cpu": { + "clock": 3.55, + "cores": 64, + "count": 2, + "type": "AMD 9355" + }, + "drives": [ + { + "count": 4, + "size": "3.8TB", + "type": "NVME" + } + ], + "gpu": { + "count": 8, + "interconnect": "No Interconnect", + "type": "NVIDIA RTX PRO 6000 Server Edition", + "vram_per_gpu": 96 + }, + "memory": { + "total": 1536 + }, + "nics": [ + { + "count": 2, + "type": "100 Gbps" + } + ] + } + }, + "id": "plan_YGwn0Vnq063JD", + "type": "plans" + }, + { + "attributes": { + "features": [ + "ssh", + "raid", + "user_data", + "sev" + ], + "name": "m4.metal.xlarge", + "regions": [ + { + "deploys_instantly": [], + "locations": { + "available": [ + "DAL", + "LAX", + "NYC", + "CHI", + "ASH", + "MIA2", + "LAX2", + "SJC2" + ], + "in_stock": [] + }, + "name": "United States", + "pricing": { + "BRL": { + "hour": 16.27, + "month": 8314.0, + "year": 69838.0 + }, + "USD": { + "hour": 2.88, + "month": 1472.0, + "year": 12365.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "TYO3", + "TYO4" + ], + "in_stock": [] + }, + "name": "Japan", + "pricing": { + "BRL": { + "hour": 17.01, + "month": 8692.0, + "year": 73013.0 + }, + "USD": { + "hour": 3.01, + "month": 1538.0, + "year": 12919.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "LON", + "LON2" + ], + "in_stock": [] + }, + "name": "United Kingdom", + "pricing": { + "BRL": { + "hour": 16.27, + "month": 8314.0, + "year": 69838.0 + }, + "USD": { + "hour": 2.88, + "month": 1472.0, + "year": 12365.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "FRA", + "FRA2" + ], + "in_stock": [] + }, + "name": "Germany", + "pricing": { + "BRL": { + "hour": 16.27, + "month": 8314.0, + "year": 69838.0 + }, + "USD": { + "hour": 2.88, + "month": 1472.0, + "year": 12365.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "SGP" + ], + "in_stock": [] + }, + "name": "Singapore", + "pricing": { + "BRL": { + "hour": 17.01, + "month": 8692.0, + "year": 73013.0 + }, + "USD": { + "hour": 3.01, + "month": 1538.0, + "year": 12919.0 + } + }, + "stock_level": "unavailable" + }, + { + "deploys_instantly": [], + "locations": { + "available": [ + "AMS" + ], + "in_stock": [] + }, + "name": "Netherlands", + "pricing": { + "BRL": { + "hour": 16.27, + "month": 8314.0, + "year": 69838.0 + }, + "USD": { + "hour": 2.88, + "month": 1472.0, + "year": 12365.0 + } + }, + "stock_level": "unavailable" + } + ], + "slug": "m4-metal-xlarge", + "specs": { + "cpu": { + "clock": 3.15, + "cores": 48, + "count": 1, + "type": "AMD 9455P" + }, + "drives": [ + { + "count": 2, + "size": "480GB", + "type": "NVME" + }, + { + "count": 2, + "size": "3.8TB", + "type": "NVME" + } + ], + "gpu": {}, + "memory": { + "total": 768 + }, + "nics": [ + { + "count": 1, + "type": "2 x 10Gbps" + } + ] + } + }, + "id": "plan_y9815Xnx0vEkd", + "type": "plans" + } + ], + "meta": {} +} diff --git a/test/fixtures/latitude/regions.json b/test/fixtures/latitude/regions.json new file mode 100644 index 000000000..2a19e2488 --- /dev/null +++ b/test/fixtures/latitude/regions.json @@ -0,0 +1,285 @@ +{ + "data": [ + { + "attributes": { + "country": { + "name": "United States", + "slug": "united-states" + }, + "facility": "Digital Realty Silicon Valley - SJC15", + "name": "Silicon Valley", + "slug": "SJC2", + "type": "Core" + }, + "id": "loc_kW9EKa3v5RoBV", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Japan", + "slug": "japan" + }, + "facility": "Digital Realty Tokyo - NRT10", + "name": "Tokyo 4", + "slug": "TYO4", + "type": "Core" + }, + "id": "loc_9vAPXaMp5epzk", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "United States", + "slug": "united-states" + }, + "facility": "CenterSquare LAX5", + "name": "Los Angeles 2", + "slug": "LAX2", + "type": "Core" + }, + "id": "loc_bEvjLaBg0oqyx", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Netherlands", + "slug": "netherlands" + }, + "facility": "LeaseWeb Amsterdam AMS-01", + "name": "Amsterdam", + "slug": "AMS", + "type": "Core" + }, + "id": "loc_E7pWRawp0rD6y", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Germany", + "slug": "germany" + }, + "facility": "Leaseweb FRA1 ( Iron Mountain FRA-2)", + "name": "Frankfurt 2", + "slug": "FRA2", + "type": "Core" + }, + "id": "loc_qewOM5GbNp8ER", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Australia", + "slug": "australia" + }, + "facility": "Equinix SY5", + "name": "Sydney 2", + "slug": "SYD2", + "type": "Core" + }, + "id": "loc_OmJyo0vL5Dkw9", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Singapore", + "slug": "singapore" + }, + "facility": "Equinix SG2", + "name": "Singapore", + "slug": "SGP", + "type": "Core" + }, + "id": "loc_4g3PYaR30yozM", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Japan", + "slug": "japan" + }, + "facility": "Equinix TY10", + "name": "Tokyo", + "slug": "TYO3", + "type": "Core" + }, + "id": "loc_nzZOo0leNKQYb", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "United Kingdom", + "slug": "united-kingdom" + }, + "facility": "Telehouse Docklands South", + "name": "London 2", + "slug": "LON2", + "type": "Core" + }, + "id": "loc_28mop5gvNjxvn", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "United States", + "slug": "united-states" + }, + "facility": "Colohouse - Miami", + "name": "Miami", + "slug": "MIA2", + "type": "Core" + }, + "id": "loc_JLqG158d5BOgv", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Chile", + "slug": "chile" + }, + "facility": "Ascenty Chile 2", + "name": "Santiago 3", + "slug": "SAN3", + "type": "Core" + }, + "id": "loc_D6B9VaLRN7vrk", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Germany", + "slug": "germany" + }, + "facility": "Leaseweb Germany GmbH", + "name": "Frankfurt", + "slug": "FRA", + "type": "Core" + }, + "id": "loc_PogXka9d5JdBA", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Mexico", + "slug": "mexico" + }, + "facility": "Ascenty MEX1", + "name": "Mexico City", + "slug": "MEX2", + "type": "Core" + }, + "id": "loc_Ylnow0Ow09x1q", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Colombia", + "slug": "colombia" + }, + "facility": "Odata", + "name": "Bogota", + "slug": "BGT", + "type": "Core" + }, + "id": "loc_l3xMV0xKamQp7", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "United States", + "slug": "united-states" + }, + "facility": "DEFT IAD", + "name": "Ashburn", + "slug": "ASH", + "type": "Core" + }, + "id": "loc_zdexA0q8alQVv", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "United States", + "slug": "united-states" + }, + "facility": "Deft CH1", + "name": "Chicago", + "slug": "CHI", + "type": "Core" + }, + "id": "loc_wmgWeN6O5Yd7k", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Argentina", + "slug": "argentina" + }, + "facility": "SYT", + "name": "Buenos Aires", + "slug": "BUE", + "type": "Core" + }, + "id": "loc_rx3egaQx546Q8", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Brazil", + "slug": "brazil" + }, + "facility": "Scala SP2", + "name": "S\u00e3o Paulo 2", + "slug": "SAO2", + "type": "Core" + }, + "id": "loc_ZnPRbajK0koM1", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "United Kingdom", + "slug": "united-kingdom" + }, + "facility": "LDEX1", + "name": "London", + "slug": "LON", + "type": "Core" + }, + "id": "loc_ndYjv5rwaqp7l", + "type": "regions" + }, + { + "attributes": { + "country": { + "name": "Australia", + "slug": "australia" + }, + "facility": "Global Switch SYD", + "name": "Sydney", + "slug": "SYD", + "type": "Core" + }, + "id": "loc_Dy9815Xx5vEkd", + "type": "regions" + } + ], + "meta": {} +} diff --git a/test/fixtures/latitude/ssh_keys.json b/test/fixtures/latitude/ssh_keys.json new file mode 100644 index 000000000..a73cfd655 --- /dev/null +++ b/test/fixtures/latitude/ssh_keys.json @@ -0,0 +1,4 @@ +{ + "data": [], + "meta": {} +} diff --git a/test/fixtures/linode/_metadata.json b/test/fixtures/linode/_metadata.json index 029fa863d..b8122a17d 100644 --- a/test/fixtures/linode/_metadata.json +++ b/test/fixtures/linode/_metadata.json @@ -1,11 +1,13 @@ { "cloud": "linode", - "recorded_at": "2026-02-10T21:55:36Z", + "recorded_at": "2026-02-13T05:00:35Z", "fixtures": { - "profile": {"endpoint": "/profile", "recorded_at": "2026-02-10T21:55:34Z"}, - "ssh_keys": {"endpoint": "/profile/sshkeys", "recorded_at": "2026-02-10T21:55:34Z"}, - "instances": {"endpoint": "/linode/instances", "recorded_at": "2026-02-10T21:55:35Z"}, - "types": {"endpoint": "/linode/types", "recorded_at": "2026-02-10T21:55:35Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-10T21:55:36Z"} + "profile": {"endpoint": "/profile", "recorded_at": "2026-02-13T05:00:28Z"}, + "ssh_keys": {"endpoint": "/profile/sshkeys", "recorded_at": "2026-02-13T05:00:28Z"}, + "instances": {"endpoint": "/linode/instances", "recorded_at": "2026-02-13T05:00:28Z"}, + "types": {"endpoint": "/linode/types", "recorded_at": "2026-02-13T05:00:29Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T05:00:29Z"}, + "create_server": {"endpoint": "POST /linode/instances", "type": "live", "recorded_at": "2026-02-13T05:00:32Z"}, + "delete_server": {"endpoint": "DELETE /linode/instances/{id}", "type": "live", "recorded_at": "2026-02-13T05:00:35Z"} } } diff --git a/test/fixtures/linode/create_server.json b/test/fixtures/linode/create_server.json index ac9a6578f..e9f3ab34a 100644 --- a/test/fixtures/linode/create_server.json +++ b/test/fixtures/linode/create_server.json @@ -19,20 +19,20 @@ "Block Storage Encryption", "Maintenance Policy" ], - "created": "2026-02-11T03:01:09", + "created": "2026-02-13T05:00:30", "disk_encryption": "enabled", "group": "", "has_user_data": false, - "host_uuid": "fc1687288efff1e09ccc267d044e3002025130de", + "host_uuid": "65837a7981c07401e8aa47a35c483e28865bd9d9", "hypervisor": "kvm", - "id": 91482496, + "id": 91655974, "image": "linode/ubuntu24.04", "interface_generation": "legacy_config", "ipv4": [ - "23.92.20.47" + "172.104.30.73" ], - "ipv6": "2600:3c03::2000:caff:fea5:da9f/128", - "label": "spawn-record-1770778868", + "ipv6": "2600:3c03::2000:aeff:fe22:ed78/128", + "label": "spawn-record-1770958829", "lke_cluster_id": null, "maintenance_policy": "linode/migrate", "placement_group": null, @@ -49,6 +49,6 @@ "status": "provisioning", "tags": [], "type": "g6-nanode-1", - "updated": "2026-02-11T03:01:09", + "updated": "2026-02-13T05:00:30", "watchdog_enabled": true } diff --git a/test/fixtures/linode/regions.json b/test/fixtures/linode/regions.json index 201fd3543..e8228ae3f 100644 --- a/test/fixtures/linode/regions.json +++ b/test/fixtures/linode/regions.json @@ -30,7 +30,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -75,7 +76,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -120,7 +122,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -165,7 +168,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -188,6 +192,7 @@ "Backups", "NodeBalancers", "Block Storage", + "Object Storage", "GPU Linodes", "Kubernetes", "Cloud Firewall", @@ -210,7 +215,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -233,6 +239,7 @@ "Backups", "NodeBalancers", "Block Storage", + "Object Storage", "GPU Linodes", "Kubernetes", "Cloud Firewall", @@ -254,7 +261,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -296,10 +304,12 @@ "label": "Melbourne, AU", "monitors": { "alerts": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -344,7 +354,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -390,7 +401,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -435,7 +447,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -481,7 +494,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -526,7 +540,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -571,7 +586,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -617,7 +633,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -662,7 +679,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -707,7 +725,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -751,7 +770,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -796,7 +816,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -841,7 +862,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -886,7 +908,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -927,10 +950,12 @@ "label": "Washington, DC", "monitors": { "alerts": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -969,10 +994,12 @@ "label": "Sydney, AU", "monitors": { "alerts": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -1015,7 +1042,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -1058,7 +1086,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -1100,7 +1129,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -1142,7 +1172,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -1186,7 +1217,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -1226,7 +1258,9 @@ "label": "Singapore, SG", "monitors": { "alerts": [], - "metrics": [] + "metrics": [ + "NodeBalancers" + ] }, "placement_group_limits": { "maximum_linodes_per_flexible_pg": 5, @@ -1263,7 +1297,9 @@ "label": "London, UK", "monitors": { "alerts": [], - "metrics": [] + "metrics": [ + "NodeBalancers" + ] }, "placement_group_limits": { "maximum_linodes_per_flexible_pg": 5, @@ -1306,7 +1342,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -1350,7 +1387,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -1392,7 +1430,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { diff --git a/test/fixtures/scaleway/_metadata.json b/test/fixtures/scaleway/_metadata.json index c78a3354d..1be9bd968 100644 --- a/test/fixtures/scaleway/_metadata.json +++ b/test/fixtures/scaleway/_metadata.json @@ -1,8 +1,8 @@ { "cloud": "scaleway", - "recorded_at": "2026-02-11T01:51:27Z", + "recorded_at": "2026-02-13T05:00:30Z", "fixtures": { - "servers": {"endpoint": "/servers", "recorded_at": "2026-02-11T01:51:26Z"}, - "images": {"endpoint": "/images?per_page=10", "recorded_at": "2026-02-11T01:51:27Z"} + "servers": {"endpoint": "/servers", "recorded_at": "2026-02-13T05:00:29Z"}, + "images": {"endpoint": "/images?per_page=10", "recorded_at": "2026-02-13T05:00:30Z"} } } diff --git a/test/fixtures/scaleway/images.json b/test/fixtures/scaleway/images.json index 6f6505628..26d83f5a3 100644 --- a/test/fixtures/scaleway/images.json +++ b/test/fixtures/scaleway/images.json @@ -2,40 +2,40 @@ "images": [ { "arch": "x86_64", - "creation_date": "2026-02-10T16:31:15.771530+00:00", + "creation_date": "2026-02-11T15:09:32.597221+00:00", "default_bootscript": null, "extra_volumes": {}, "from_server": "", - "id": "374b4139-51c8-4497-80da-bde105394f09", - "modification_date": "2026-02-10T16:31:15.771530+00:00", - "name": "Windows Server 2025", + "id": "5c248ca4-5058-489b-8481-be7d2799fb0a", + "modification_date": "2026-02-11T15:09:32.597221+00:00", + "name": "k8s_base_node_instance", "organization": "51b656e3-4865-41e8-adbc-0c45bdd780db", "project": "51b656e3-4865-41e8-adbc-0c45bdd780db", "public": true, "root_volume": { - "id": "6a1029ca-329a-4202-9b6d-3445bdbf0916", - "name": "", - "size": 0, - "volume_type": "sbs_snapshot" + "id": "4159bb92-5a75-44ee-9ce5-8294de74cc2d", + "name": "k8s_base_node_instance", + "size": 10000000000, + "volume_type": "l_ssd" }, "state": "available", "tags": [], "zone": "fr-par-1" }, { - "arch": "x86_64", - "creation_date": "2026-02-10T11:30:39.697503+00:00", + "arch": "arm64", + "creation_date": "2026-02-11T15:08:54.481101+00:00", "default_bootscript": null, "extra_volumes": {}, "from_server": "", - "id": "ca5e7780-a7c3-451f-a528-0eabc745343c", - "modification_date": "2026-02-10T11:30:39.697503+00:00", - "name": "Windows Server 2025 Core", + "id": "28ee64af-8f6e-43d3-b9ea-d3d4a123b687", + "modification_date": "2026-02-11T15:08:54.481101+00:00", + "name": "k8s_base_node_instance", "organization": "51b656e3-4865-41e8-adbc-0c45bdd780db", "project": "51b656e3-4865-41e8-adbc-0c45bdd780db", "public": true, "root_volume": { - "id": "955c1e88-d75b-4ca1-b53e-b5c9c07a7bb1", + "id": "89b1b122-8470-4c54-8919-366266f658c8", "name": "", "size": 0, "volume_type": "sbs_snapshot" @@ -46,18 +46,18 @@ }, { "arch": "x86_64", - "creation_date": "2026-02-05T15:28:32.401289+00:00", + "creation_date": "2026-02-11T15:08:08.602556+00:00", "default_bootscript": null, "extra_volumes": {}, "from_server": "", - "id": "ca115f77-d927-4483-b1f8-354107143e8c", - "modification_date": "2026-02-05T15:28:32.401289+00:00", - "name": "Windows Server 2025", + "id": "0a25b795-9e9b-4351-a445-8865d3d41410", + "modification_date": "2026-02-11T15:08:08.602556+00:00", + "name": "k8s_base_node_instance", "organization": "51b656e3-4865-41e8-adbc-0c45bdd780db", "project": "51b656e3-4865-41e8-adbc-0c45bdd780db", "public": true, "root_volume": { - "id": "e7d1c335-e70a-419b-8a71-07f61e21b729", + "id": "7b182ad3-7b1e-4b7a-8c74-cc88302d3036", "name": "", "size": 0, "volume_type": "sbs_snapshot" @@ -68,18 +68,18 @@ }, { "arch": "x86_64", - "creation_date": "2026-02-05T14:56:51.105141+00:00", + "creation_date": "2026-02-10T16:31:15.771530+00:00", "default_bootscript": null, "extra_volumes": {}, "from_server": "", - "id": "3fea98bc-5e72-4f0c-a4bd-28d38010ff34", - "modification_date": "2026-02-05T14:56:51.105141+00:00", - "name": "Windows Server 2025 Core", + "id": "374b4139-51c8-4497-80da-bde105394f09", + "modification_date": "2026-02-10T16:31:15.771530+00:00", + "name": "Windows Server 2025", "organization": "51b656e3-4865-41e8-adbc-0c45bdd780db", "project": "51b656e3-4865-41e8-adbc-0c45bdd780db", "public": true, "root_volume": { - "id": "93775898-0256-4eac-a773-cb154aab73ba", + "id": "6a1029ca-329a-4202-9b6d-3445bdbf0916", "name": "", "size": 0, "volume_type": "sbs_snapshot" @@ -89,19 +89,19 @@ "zone": "fr-par-1" }, { - "arch": "arm64", - "creation_date": "2026-02-04T12:02:05.696710+00:00", + "arch": "x86_64", + "creation_date": "2026-02-10T11:30:39.697503+00:00", "default_bootscript": null, "extra_volumes": {}, "from_server": "", - "id": "2a3ad407-ad47-4b6a-89b3-7d6d820d24bb", - "modification_date": "2026-02-04T12:02:05.696710+00:00", - "name": "debian-trixie", + "id": "ca5e7780-a7c3-451f-a528-0eabc745343c", + "modification_date": "2026-02-10T11:30:39.697503+00:00", + "name": "Windows Server 2025 Core", "organization": "51b656e3-4865-41e8-adbc-0c45bdd780db", "project": "51b656e3-4865-41e8-adbc-0c45bdd780db", "public": true, "root_volume": { - "id": "77cd231f-aa0c-4bff-a180-7d5cde4f048e", + "id": "955c1e88-d75b-4ca1-b53e-b5c9c07a7bb1", "name": "", "size": 0, "volume_type": "sbs_snapshot" @@ -112,21 +112,21 @@ }, { "arch": "x86_64", - "creation_date": "2026-02-04T12:00:27.604925+00:00", + "creation_date": "2026-02-05T15:28:32.401289+00:00", "default_bootscript": null, "extra_volumes": {}, "from_server": "", - "id": "024676ab-6b3c-4473-b538-181a688d62e7", - "modification_date": "2026-02-04T12:00:27.604925+00:00", - "name": "debian-trixie", + "id": "ca115f77-d927-4483-b1f8-354107143e8c", + "modification_date": "2026-02-05T15:28:32.401289+00:00", + "name": "Windows Server 2025", "organization": "51b656e3-4865-41e8-adbc-0c45bdd780db", "project": "51b656e3-4865-41e8-adbc-0c45bdd780db", "public": true, "root_volume": { - "id": "c5af243c-1e63-4799-8f01-d3302d6fea72", - "name": "debian-trixie", - "size": 10000000000, - "volume_type": "l_ssd" + "id": "e7d1c335-e70a-419b-8a71-07f61e21b729", + "name": "", + "size": 0, + "volume_type": "sbs_snapshot" }, "state": "available", "tags": [], @@ -134,18 +134,18 @@ }, { "arch": "x86_64", - "creation_date": "2026-02-04T12:00:22.720696+00:00", + "creation_date": "2026-02-05T14:56:51.105141+00:00", "default_bootscript": null, "extra_volumes": {}, "from_server": "", - "id": "71d4a59b-5c9a-4084-b62b-f32bc066b1c3", - "modification_date": "2026-02-04T12:00:22.720696+00:00", - "name": "debian-trixie", + "id": "3fea98bc-5e72-4f0c-a4bd-28d38010ff34", + "modification_date": "2026-02-05T14:56:51.105141+00:00", + "name": "Windows Server 2025 Core", "organization": "51b656e3-4865-41e8-adbc-0c45bdd780db", "project": "51b656e3-4865-41e8-adbc-0c45bdd780db", "public": true, "root_volume": { - "id": "8f813bfe-0ad8-4ff1-aeb3-663d9e1670af", + "id": "93775898-0256-4eac-a773-cb154aab73ba", "name": "", "size": 0, "volume_type": "sbs_snapshot" @@ -156,18 +156,18 @@ }, { "arch": "arm64", - "creation_date": "2026-01-28T16:28:07.893202+00:00", + "creation_date": "2026-02-04T12:02:05.696710+00:00", "default_bootscript": null, "extra_volumes": {}, "from_server": "", - "id": "2dcbfb4f-1b77-4733-b734-954b09c2b17b", - "modification_date": "2026-01-28T16:28:07.893202+00:00", + "id": "2a3ad407-ad47-4b6a-89b3-7d6d820d24bb", + "modification_date": "2026-02-04T12:02:05.696710+00:00", "name": "debian-trixie", "organization": "51b656e3-4865-41e8-adbc-0c45bdd780db", "project": "51b656e3-4865-41e8-adbc-0c45bdd780db", "public": true, "root_volume": { - "id": "a33ea2ec-9531-4ce2-b2c1-6e4979c371f6", + "id": "77cd231f-aa0c-4bff-a180-7d5cde4f048e", "name": "", "size": 0, "volume_type": "sbs_snapshot" @@ -178,18 +178,18 @@ }, { "arch": "x86_64", - "creation_date": "2026-01-28T16:27:26.694891+00:00", + "creation_date": "2026-02-04T12:00:27.604925+00:00", "default_bootscript": null, "extra_volumes": {}, "from_server": "", - "id": "69982b79-5db1-487a-8fd4-cdb3547f4bb2", - "modification_date": "2026-01-28T16:27:26.694891+00:00", + "id": "024676ab-6b3c-4473-b538-181a688d62e7", + "modification_date": "2026-02-04T12:00:27.604925+00:00", "name": "debian-trixie", "organization": "51b656e3-4865-41e8-adbc-0c45bdd780db", "project": "51b656e3-4865-41e8-adbc-0c45bdd780db", "public": true, "root_volume": { - "id": "a6ed67b9-9789-4b18-875b-4e747eda3dc9", + "id": "c5af243c-1e63-4799-8f01-d3302d6fea72", "name": "debian-trixie", "size": 10000000000, "volume_type": "l_ssd" @@ -200,18 +200,18 @@ }, { "arch": "x86_64", - "creation_date": "2026-01-28T16:27:25.789355+00:00", + "creation_date": "2026-02-04T12:00:22.720696+00:00", "default_bootscript": null, "extra_volumes": {}, "from_server": "", - "id": "6d173ed8-ae70-4671-a8f1-584921c4e706", - "modification_date": "2026-01-28T16:27:25.789355+00:00", + "id": "71d4a59b-5c9a-4084-b62b-f32bc066b1c3", + "modification_date": "2026-02-04T12:00:22.720696+00:00", "name": "debian-trixie", "organization": "51b656e3-4865-41e8-adbc-0c45bdd780db", "project": "51b656e3-4865-41e8-adbc-0c45bdd780db", "public": true, "root_volume": { - "id": "5f780810-ef25-4def-987b-b03409862d08", + "id": "8f813bfe-0ad8-4ff1-aeb3-663d9e1670af", "name": "", "size": 0, "volume_type": "sbs_snapshot" diff --git a/test/fixtures/vultr/_metadata.json b/test/fixtures/vultr/_metadata.json index 96a70de8c..6eb5cace5 100644 --- a/test/fixtures/vultr/_metadata.json +++ b/test/fixtures/vultr/_metadata.json @@ -1,11 +1,8 @@ { "cloud": "vultr", - "recorded_at": "2026-02-10T21:55:33Z", + "recorded_at": "2026-02-13T05:00:32Z", "fixtures": { - "account": {"endpoint": "/account", "recorded_at": "2026-02-10T21:55:30Z"}, - "ssh_keys": {"endpoint": "/ssh-keys", "recorded_at": "2026-02-10T21:55:31Z"}, - "instances": {"endpoint": "/instances", "recorded_at": "2026-02-10T21:55:31Z"}, - "plans": {"endpoint": "/plans", "recorded_at": "2026-02-10T21:55:33Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-10T21:55:33Z"} + "plans": {"endpoint": "/plans", "recorded_at": "2026-02-13T05:00:30Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T05:00:30Z"} } } diff --git a/test/fixtures/vultr/plans.json b/test/fixtures/vultr/plans.json index d4382630c..0e454de94 100644 --- a/test/fixtures/vultr/plans.json +++ b/test/fixtures/vultr/plans.json @@ -1,12 +1,38 @@ { "meta": { "links": { - "next": "bmV4dF9fdmNnLWExMDAtM2MtMzBnLTIwdnJhbQ==", + "next": "bmV4dF9fdmNnLWE0MC02Yy0zMGctMTJ2cmFt", "prev": "" }, - "total": 159 + "total": 160 }, "plans": [ + { + "bandwidth": 0, + "cpu_vendor": "Intel", + "deploy_ondemand": true, + "deploy_preemptible": false, + "disk": 10, + "disk_count": 1, + "disk_type": "SSD", + "hourly_cost": 0, + "hourly_cost_preemptible": 0, + "id": "vc2-1c-0.5gb-free", + "invoice_type": "monthly", + "location_cost": {}, + "locations": [ + "sea", + "fra", + "mia" + ], + "monthly_cost": 0, + "monthly_cost_preemptible": 0, + "ram": 512, + "storage_type": "local_storage", + "type": "vc2", + "vcpu_count": 1, + "vcpu_type": "thread" + }, { "bandwidth": 512, "cpu_vendor": "Intel", @@ -1042,7 +1068,7 @@ "lax", "atl", "ams", - "fra", + "lhr", "sjc", "yto", "cdg", @@ -1086,6 +1112,7 @@ "invoice_type": "monthly", "location_cost": {}, "locations": [ + "ewr", "dfw", "sea", "lax", @@ -1136,11 +1163,11 @@ "ams", "sjc", "yto", - "cdg", "nrt", "waw", "mad", "icn", + "mia", "sgp", "sto", "mex", @@ -3153,6 +3180,7 @@ "dfw", "sea", "lax", + "atl", "ams", "lhr", "fra", @@ -3268,6 +3296,7 @@ "dfw", "sea", "lax", + "atl", "ams", "lhr", "fra", @@ -3566,6 +3595,7 @@ "locations": [ "ewr", "ord", + "sea", "lax", "ams", "lhr", @@ -3619,6 +3649,7 @@ "dfw", "sea", "lax", + "atl", "ams", "lhr", "fra", @@ -4007,6 +4038,7 @@ "dfw", "sea", "lax", + "atl", "ams", "lhr", "fra", @@ -4123,6 +4155,7 @@ "ams", "lhr", "sjc", + "syd", "cdg", "nrt", "waw", @@ -4431,6 +4464,7 @@ "dfw", "sea", "lax", + "atl", "ams", "lhr", "fra", @@ -4646,6 +4680,7 @@ "dfw", "sea", "lax", + "atl", "ams", "lhr", "fra", @@ -4699,6 +4734,7 @@ "dfw", "sea", "lax", + "atl", "ams", "lhr", "fra", @@ -4802,6 +4838,7 @@ "dfw", "sea", "lax", + "atl", "ams", "lhr", "fra", @@ -5271,6 +5308,7 @@ "locations": [ "ewr", "lhr", + "syd", "nrt", "blr" ], @@ -5334,9 +5372,7 @@ "locations": [ "lhr", "fra", - "syd", - "nrt", - "sgp" + "syd" ], "monthly_cost": 210, "monthly_cost_preemptible": 210, @@ -5362,7 +5398,7 @@ "invoice_type": "hourly", "location_cost": {}, "locations": [ - "ewr", + "syd", "nrt" ], "monthly_cost": 315, @@ -5372,35 +5408,6 @@ "type": "vcg", "vcpu_count": 6, "vcpu_type": "thread" - }, - { - "bandwidth": 3072, - "cpu_vendor": "Intel", - "deploy_ondemand": true, - "deploy_preemptible": false, - "disk": 700, - "disk_count": 1, - "disk_type": "CLOUDGPU", - "gpu_type": "NVIDIA_A100", - "gpu_vram_gb": 20, - "hourly_cost": 0.616, - "hourly_cost_preemptible": 0.616, - "id": "vcg-a100-3c-30g-20vram", - "invoice_type": "hourly", - "location_cost": {}, - "locations": [ - "ewr", - "fra", - "sjc", - "nrt" - ], - "monthly_cost": 450, - "monthly_cost_preemptible": 450, - "ram": 30720, - "storage_type": "local_storage", - "type": "vcg", - "vcpu_count": 3, - "vcpu_type": "thread" } ] } diff --git a/test/record.sh b/test/record.sh index 9f52bc90f..4fcda7063 100644 --- a/test/record.sh +++ b/test/record.sh @@ -860,10 +860,68 @@ printf '\n' mkdir -p "$FIXTURES_DIR" -for cloud in $CLOUDS_TO_RECORD; do - record_cloud "$cloud" +# Count clouds to decide sequential vs parallel +CLOUD_COUNT=0 +for _c in $CLOUDS_TO_RECORD; do + CLOUD_COUNT=$((CLOUD_COUNT + 1)) done +# Parallel recording: each cloud runs in its own subshell when recording 2+ clouds +# Each cloud writes to its own fixture dir so there are no conflicts. +if [[ "$CLOUD_COUNT" -gt 1 ]] && [[ "$PROMPT_FOR_CREDS" != "true" ]]; then + printf '%b\n' "${CYAN}Recording ${CLOUD_COUNT} clouds in parallel...${NC}" + printf '\n' + + PARALLEL_DIR=$(mktemp -d /tmp/spawn-record-parallel-XXXXXX) + PIDS="" + + for cloud in $CLOUDS_TO_RECORD; do + ( + # Each subshell writes its own result file + RESULT_FILE="${PARALLEL_DIR}/${cloud}.result" + OUTPUT_FILE="${PARALLEL_DIR}/${cloud}.log" + + # Capture all output + record_cloud "$cloud" > "$OUTPUT_FILE" 2>&1 + + # Write counters to result file for aggregation + printf '%d %d %d\n' "$RECORDED" "$SKIPPED" "$ERRORS" > "$RESULT_FILE" + ) & + PIDS="${PIDS} $!" + done + + # Wait for all parallel recordings + for pid in $PIDS; do + wait "$pid" 2>/dev/null || true + done + + # Aggregate results and print output + for cloud in $CLOUDS_TO_RECORD; do + OUTPUT_FILE="${PARALLEL_DIR}/${cloud}.log" + RESULT_FILE="${PARALLEL_DIR}/${cloud}.result" + + # Print the captured output + if [[ -f "$OUTPUT_FILE" ]]; then + cat "$OUTPUT_FILE" + fi + + # Aggregate counters + if [[ -f "$RESULT_FILE" ]]; then + read -r r s e < "$RESULT_FILE" + RECORDED=$((RECORDED + r)) + SKIPPED=$((SKIPPED + s)) + ERRORS=$((ERRORS + e)) + fi + done + + rm -rf "$PARALLEL_DIR" +else + # Sequential recording: single cloud or interactive credential prompting + for cloud in $CLOUDS_TO_RECORD; do + record_cloud "$cloud" + done +fi + # --- Summary --- printf '%b\n' "${CYAN}===============================${NC}" TOTAL=$((RECORDED + SKIPPED + ERRORS)) From 159d97f4cc69bc2e8b76064b984e8ad776805197 Mon Sep 17 00:00:00 2001 From: Ahmed Abushagur Date: Thu, 12 Feb 2026 22:39:47 -0800 Subject: [PATCH 04/13] feat: improved testing --- .claude/skills/setup-agent-team/qa-cycle.sh | 12 +- .../skills/setup-trigger-service/qa-cycle.sh | 1 + test/fixtures/civo/_metadata.json | 16 +- test/fixtures/civo/create_server.json | 12 +- test/fixtures/civo/delete_server.json | 2 +- test/fixtures/digitalocean/_metadata.json | 16 +- test/fixtures/digitalocean/create_server.json | 10 +- test/fixtures/digitalocean/regions.json | 13 +- test/fixtures/hetzner/_metadata.json | 14 +- test/fixtures/hetzner/create_server.json | 24 +- test/fixtures/hetzner/delete_server.json | 6 +- test/fixtures/lambda/_metadata.json | 8 +- test/fixtures/lambda/instance_types.json | 25 +- test/fixtures/latitude/_metadata.json | 8 +- test/fixtures/latitude/plans.json | 14 +- test/fixtures/linode/_metadata.json | 16 +- test/fixtures/linode/create_server.json | 14 +- test/fixtures/scaleway/_metadata.json | 6 +- test/fixtures/vultr/_metadata.json | 6 +- test/fixtures/vultr/plans.json | 1 - test/qa-dry-cycle.sh | 595 ++++++++++++++++++ 21 files changed, 700 insertions(+), 119 deletions(-) create mode 120000 .claude/skills/setup-trigger-service/qa-cycle.sh create mode 100644 test/qa-dry-cycle.sh diff --git a/.claude/skills/setup-agent-team/qa-cycle.sh b/.claude/skills/setup-agent-team/qa-cycle.sh index e28a76a5e..c54638dcf 100644 --- a/.claude/skills/setup-agent-team/qa-cycle.sh +++ b/.claude/skills/setup-agent-team/qa-cycle.sh @@ -172,21 +172,15 @@ for branch in $REMOTE_README_BRANCHES; do fi done -# Close stale qa PRs (open > 2 hours) +# Close stale qa PRs (open > 2 hours) — never auto-merge, leave for human review STALE_PRS=$(gh pr list --state open --label '' --json number,headRefName,updatedAt \ --jq '[.[] | select(.headRefName | startswith("qa/")) | select(.updatedAt < (now - 7200 | todate)) | .number] | .[]' 2>/dev/null) || true for pr_num in $STALE_PRS; do if [[ -n "$pr_num" ]]; then - PR_MERGEABLE=$(gh pr view "$pr_num" --json mergeable --jq '.mergeable' 2>/dev/null) || PR_MERGEABLE="UNKNOWN" - if [[ "$PR_MERGEABLE" == "MERGEABLE" ]]; then - log "Merging stale QA PR #${pr_num}..." - gh pr merge "$pr_num" --squash --delete-branch 2>&1 | tee -a "${LOG_FILE}" || true - else - log "Closing unmergeable stale QA PR #${pr_num}..." - gh pr close "$pr_num" --comment "Auto-closing: stale QA PR from a previous cycle. + log "Closing stale QA PR #${pr_num}..." + gh pr close "$pr_num" --comment "Auto-closing: stale QA PR from a previous cycle. -- qa/cycle" 2>&1 | tee -a "${LOG_FILE}" || true - fi fi done diff --git a/.claude/skills/setup-trigger-service/qa-cycle.sh b/.claude/skills/setup-trigger-service/qa-cycle.sh new file mode 120000 index 000000000..0c41e86a9 --- /dev/null +++ b/.claude/skills/setup-trigger-service/qa-cycle.sh @@ -0,0 +1 @@ +../setup-agent-team/qa-cycle.sh \ No newline at end of file diff --git a/test/fixtures/civo/_metadata.json b/test/fixtures/civo/_metadata.json index 44ff8c273..c36fd95c4 100644 --- a/test/fixtures/civo/_metadata.json +++ b/test/fixtures/civo/_metadata.json @@ -1,13 +1,13 @@ { "cloud": "civo", - "recorded_at": "2026-02-13T05:00:40Z", + "recorded_at": "2026-02-13T06:37:22Z", "fixtures": { - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T05:00:28Z"}, - "instances": {"endpoint": "/instances", "recorded_at": "2026-02-13T05:00:29Z"}, - "sshkeys": {"endpoint": "/sshkeys", "recorded_at": "2026-02-13T05:00:30Z"}, - "networks": {"endpoint": "/networks", "recorded_at": "2026-02-13T05:00:30Z"}, - "disk_images": {"endpoint": "/disk_images", "recorded_at": "2026-02-13T05:00:31Z"}, - "create_server": {"endpoint": "POST /instances", "type": "live", "recorded_at": "2026-02-13T05:00:34Z"}, - "delete_server": {"endpoint": "DELETE /instances/{id}", "type": "live", "recorded_at": "2026-02-13T05:00:40Z"} + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T06:37:10Z"}, + "instances": {"endpoint": "/instances", "recorded_at": "2026-02-13T06:37:11Z"}, + "sshkeys": {"endpoint": "/sshkeys", "recorded_at": "2026-02-13T06:37:12Z"}, + "networks": {"endpoint": "/networks", "recorded_at": "2026-02-13T06:37:12Z"}, + "disk_images": {"endpoint": "/disk_images", "recorded_at": "2026-02-13T06:37:13Z"}, + "create_server": {"endpoint": "POST /instances", "type": "live", "recorded_at": "2026-02-13T06:37:16Z"}, + "delete_server": {"endpoint": "DELETE /instances/{id}", "type": "live", "recorded_at": "2026-02-13T06:37:22Z"} } } diff --git a/test/fixtures/civo/create_server.json b/test/fixtures/civo/create_server.json index 1c6deb428..2d9511cd2 100644 --- a/test/fixtures/civo/create_server.json +++ b/test/fixtures/civo/create_server.json @@ -1,16 +1,16 @@ { "account_id": "d2449447-ac9e-4a4e-90a8-bb1aae89cb9a", "attached_volumes": [], - "civostatsd_token": "17b20ca4-8cdd-4ceb-8cc4-33a86877461b", + "civostatsd_token": "d6da608c-b760-4b0a-9b45-c1e6a25f407f", "cpu_cores": 1, - "created_at": "2026-02-13T05:00:34Z", + "created_at": "2026-02-13T06:37:16Z", "disk_gb": 25, "firewall_id": "11a86926-bfbb-4744-aaed-979a3ae85086", - "hostname": "spawn-record-1770958831", - "id": "59e311e5-8abf-44e6-be46-32c0bf6a2cfb", - "initial_password": "MNTA7Q9MW4", + "hostname": "spawn-record-1770964633", + "id": "7b31ce22-7ac5-4dad-a9e7-38fd76d82849", + "initial_password": "RSSnLdac9e", "initial_user": "root", - "name": "spawn-record-1770958831-5a39-8c2237", + "name": "spawn-record-1770964633-2d22-031d88", "namespace_id": "cust-default-d2449447-75059009d0bd", "network_id": "aee434ed-e2b8-4e5d-8eb2-97ef3d3062b3", "notes": "", diff --git a/test/fixtures/civo/delete_server.json b/test/fixtures/civo/delete_server.json index cfe5fb36c..f36db5a8e 100644 --- a/test/fixtures/civo/delete_server.json +++ b/test/fixtures/civo/delete_server.json @@ -1,4 +1,4 @@ { - "id": "59e311e5-8abf-44e6-be46-32c0bf6a2cfb", + "id": "7b31ce22-7ac5-4dad-a9e7-38fd76d82849", "result": "success" } diff --git a/test/fixtures/digitalocean/_metadata.json b/test/fixtures/digitalocean/_metadata.json index af1429efa..3b9e27360 100644 --- a/test/fixtures/digitalocean/_metadata.json +++ b/test/fixtures/digitalocean/_metadata.json @@ -1,13 +1,13 @@ { "cloud": "digitalocean", - "recorded_at": "2026-02-13T05:00:37Z", + "recorded_at": "2026-02-13T06:37:18Z", "fixtures": { - "account": {"endpoint": "/account", "recorded_at": "2026-02-13T05:00:28Z"}, - "ssh_keys": {"endpoint": "/account/keys", "recorded_at": "2026-02-13T05:00:29Z"}, - "droplets": {"endpoint": "/droplets", "recorded_at": "2026-02-13T05:00:29Z"}, - "sizes": {"endpoint": "/sizes", "recorded_at": "2026-02-13T05:00:29Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T05:00:30Z"}, - "create_server": {"endpoint": "POST /droplets", "type": "live", "recorded_at": "2026-02-13T05:00:33Z"}, - "delete_server": {"endpoint": "DELETE /droplets/{id}", "type": "live", "recorded_at": "2026-02-13T05:00:37Z"} + "account": {"endpoint": "/account", "recorded_at": "2026-02-13T06:37:10Z"}, + "ssh_keys": {"endpoint": "/account/keys", "recorded_at": "2026-02-13T06:37:11Z"}, + "droplets": {"endpoint": "/droplets", "recorded_at": "2026-02-13T06:37:11Z"}, + "sizes": {"endpoint": "/sizes", "recorded_at": "2026-02-13T06:37:12Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T06:37:12Z"}, + "create_server": {"endpoint": "POST /droplets", "type": "live", "recorded_at": "2026-02-13T06:37:14Z"}, + "delete_server": {"endpoint": "DELETE /droplets/{id}", "type": "live", "recorded_at": "2026-02-13T06:37:18Z"} } } diff --git a/test/fixtures/digitalocean/create_server.json b/test/fixtures/digitalocean/create_server.json index a4d5c605e..5fd324cf4 100644 --- a/test/fixtures/digitalocean/create_server.json +++ b/test/fixtures/digitalocean/create_server.json @@ -1,7 +1,7 @@ { "droplet": { "backup_ids": [], - "created_at": "2026-02-13T05:00:32Z", + "created_at": "2026-02-13T06:37:13Z", "disk": 10, "disk_info": [ { @@ -15,7 +15,7 @@ "features": [ "droplet_agent" ], - "id": 551370564, + "id": 551384349, "image": { "created_at": "2025-08-08T15:11:27Z", "description": "Ubuntu 24.04 (LTS) x64", @@ -51,7 +51,7 @@ "kernel": null, "locked": false, "memory": 512, - "name": "spawn-record-1770958830", + "name": "spawn-record-1770964632", "networks": { "v4": [], "v6": [] @@ -276,8 +276,8 @@ "links": { "actions": [ { - "href": "/v2/actions/3041635779", - "id": 3041635779, + "href": "/v2/actions/3041720329", + "id": 3041720329, "rel": "create" } ] diff --git a/test/fixtures/digitalocean/regions.json b/test/fixtures/digitalocean/regions.json index f6566eea2..3fed997ce 100644 --- a/test/fixtures/digitalocean/regions.json +++ b/test/fixtures/digitalocean/regions.json @@ -312,6 +312,7 @@ "c-32-intel", "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", + "c5-32vcpu-64gb", "m-24vcpu-192gb", "g-32vcpu-128gb", "so-16vcpu-128gb-intel", @@ -348,7 +349,8 @@ "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", "gpu-h100x1-80gb", - "gpu-h200x1-141gb" + "gpu-h200x1-141gb", + "so1_5-32vcpu-256gb" ], "slug": "nyc2" }, @@ -932,7 +934,6 @@ "gd-2vcpu-8gb", "g-2vcpu-8gb-intel", "gd-2vcpu-8gb-intel", - "s-4vcpu-16gb-amd", "m-2vcpu-16gb", "c-4", "g6_5-2vcpu-8gb", @@ -944,7 +945,6 @@ "m3-2vcpu-16gb", "c-4-intel", "m3-2vcpu-16gb-intel", - "s-8vcpu-16gb-amd", "s-8vcpu-16gb-intel", "c2-4vcpu-8gb-intel", "c5-4vcpu-8gb", @@ -1179,12 +1179,10 @@ "gd-32vcpu-128gb-intel", "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", - "c-48-intel", "m3-24vcpu-192gb-intel", "m-32vcpu-256gb", "gd-40vcpu-160gb", "g6_5-32vcpu-128gb", - "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", "so-24vcpu-192gb-intel", "so-24vcpu-192gb", @@ -1198,7 +1196,6 @@ "g-48vcpu-192gb-intel", "c2-60vcpu-120gb-intel", "gd-48vcpu-192gb-intel", - "c5-48vcpu-96gb-intel", "so1_5-24vcpu-192gb", "so-32vcpu-256gb-intel", "g5_5-48vcpu-192gb-intel", @@ -1474,6 +1471,7 @@ "c2-32vcpu-64gb", "m-16vcpu-128gb-intel", "m3-16vcpu-128gb", + "c-32-intel", "c-48", "m-24vcpu-192gb", "g-32vcpu-128gb", @@ -1787,7 +1785,6 @@ "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", "c5-32vcpu-64gb", - "c-48", "m-24vcpu-192gb", "g-32vcpu-128gb", "so-16vcpu-128gb-intel", @@ -1795,7 +1792,6 @@ "m6-16vcpu-128gb", "gd-32vcpu-128gb", "so1_5-16vcpu-128gb-intel", - "c2-48vcpu-96gb", "m-24vcpu-192gb-intel", "g-32vcpu-128gb-intel", "m3-24vcpu-192gb", @@ -2101,7 +2097,6 @@ "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", "m-48vcpu-384gb-intel", - "gpu-h200x1-141gb", "m3-48vcpu-384gb-intel", "so-48vcpu-384gb-intel" ], diff --git a/test/fixtures/hetzner/_metadata.json b/test/fixtures/hetzner/_metadata.json index 87157b730..12a0d7c8a 100644 --- a/test/fixtures/hetzner/_metadata.json +++ b/test/fixtures/hetzner/_metadata.json @@ -1,12 +1,12 @@ { "cloud": "hetzner", - "recorded_at": "2026-02-13T05:00:36Z", + "recorded_at": "2026-02-13T06:37:18Z", "fixtures": { - "server_types": {"endpoint": "/server_types?per_page=50", "recorded_at": "2026-02-13T05:00:29Z"}, - "locations": {"endpoint": "/locations", "recorded_at": "2026-02-13T05:00:29Z"}, - "ssh_keys": {"endpoint": "/ssh_keys", "recorded_at": "2026-02-13T05:00:30Z"}, - "servers": {"endpoint": "/servers", "recorded_at": "2026-02-13T05:00:31Z"}, - "create_server": {"endpoint": "POST /servers", "type": "live", "recorded_at": "2026-02-13T05:00:33Z"}, - "delete_server": {"endpoint": "DELETE /servers/{id}", "type": "live", "recorded_at": "2026-02-13T05:00:36Z"} + "server_types": {"endpoint": "/server_types?per_page=50", "recorded_at": "2026-02-13T06:37:11Z"}, + "locations": {"endpoint": "/locations", "recorded_at": "2026-02-13T06:37:11Z"}, + "ssh_keys": {"endpoint": "/ssh_keys", "recorded_at": "2026-02-13T06:37:12Z"}, + "servers": {"endpoint": "/servers", "recorded_at": "2026-02-13T06:37:13Z"}, + "create_server": {"endpoint": "POST /servers", "type": "live", "recorded_at": "2026-02-13T06:37:15Z"}, + "delete_server": {"endpoint": "DELETE /servers/{id}", "type": "live", "recorded_at": "2026-02-13T06:37:18Z"} } } diff --git a/test/fixtures/hetzner/create_server.json b/test/fixtures/hetzner/create_server.json index d0cfc9fce..448f16ed9 100644 --- a/test/fixtures/hetzner/create_server.json +++ b/test/fixtures/hetzner/create_server.json @@ -3,11 +3,11 @@ "command": "create_server", "error": null, "finished": null, - "id": 611302701715360, + "id": 611311291703796, "progress": 0, "resources": [ { - "id": 120889518, + "id": 120895015, "type": "server" }, { @@ -15,7 +15,7 @@ "type": "image" } ], - "started": "2026-02-13T05:00:33Z", + "started": "2026-02-13T06:37:14Z", "status": "running" }, "next_actions": [ @@ -23,23 +23,23 @@ "command": "start_server", "error": null, "finished": null, - "id": 611302701715361, - "parent_id": 611302701715360, + "id": 611311291703797, + "parent_id": 611311291703796, "progress": 0, "resources": [ { - "id": 120889518, + "id": 120895015, "type": "server" } ], - "started": "2026-02-13T05:00:33Z", + "started": "2026-02-13T06:37:14Z", "status": "running" } ], "root_password": null, "server": { "backup_window": null, - "created": "2026-02-13T05:00:33Z", + "created": "2026-02-13T06:37:14Z", "datacenter": { "description": "Nuremberg 1 virtual DC 3", "id": 2, @@ -168,7 +168,7 @@ ] } }, - "id": 120889518, + "id": 120895015, "image": { "architecture": "x86", "bound_to": null, @@ -207,7 +207,7 @@ "network_zone": "eu-central" }, "locked": false, - "name": "spawn-record-1770958831", + "name": "spawn-record-1770964633", "outgoing_traffic": 0, "placement_group": null, "primary_disk_size": 40, @@ -222,13 +222,13 @@ "ipv4": { "blocked": false, "dns_ptr": "static.81.176.245.188.clients.your-server.de", - "id": 117975188, + "id": 117982494, "ip": "188.245.176.81" }, "ipv6": { "blocked": false, "dns_ptr": [], - "id": 117975189, + "id": 117982495, "ip": "2a01:4f8:1c1a:f748::/64" } }, diff --git a/test/fixtures/hetzner/delete_server.json b/test/fixtures/hetzner/delete_server.json index ee03f36a6..0c51bbad8 100644 --- a/test/fixtures/hetzner/delete_server.json +++ b/test/fixtures/hetzner/delete_server.json @@ -3,15 +3,15 @@ "command": "delete_server", "error": null, "finished": null, - "id": 611302701715371, + "id": 611311291703818, "progress": 0, "resources": [ { - "id": 120889518, + "id": 120895015, "type": "server" } ], - "started": "2026-02-13T05:00:36Z", + "started": "2026-02-13T06:37:17Z", "status": "running" } } diff --git a/test/fixtures/lambda/_metadata.json b/test/fixtures/lambda/_metadata.json index d691ce397..1858a8b2b 100644 --- a/test/fixtures/lambda/_metadata.json +++ b/test/fixtures/lambda/_metadata.json @@ -1,9 +1,9 @@ { "cloud": "lambda", - "recorded_at": "2026-02-13T05:00:29Z", + "recorded_at": "2026-02-13T06:37:11Z", "fixtures": { - "instances": {"endpoint": "/instances", "recorded_at": "2026-02-13T05:00:28Z"}, - "ssh_keys": {"endpoint": "/ssh-keys", "recorded_at": "2026-02-13T05:00:28Z"}, - "instance_types": {"endpoint": "/instance-types", "recorded_at": "2026-02-13T05:00:29Z"} + "instances": {"endpoint": "/instances", "recorded_at": "2026-02-13T06:37:10Z"}, + "ssh_keys": {"endpoint": "/ssh-keys", "recorded_at": "2026-02-13T06:37:10Z"}, + "instance_types": {"endpoint": "/instance-types", "recorded_at": "2026-02-13T06:37:11Z"} } } diff --git a/test/fixtures/lambda/instance_types.json b/test/fixtures/lambda/instance_types.json index 7a88a4c96..55b7c795d 100644 --- a/test/fixtures/lambda/instance_types.json +++ b/test/fixtures/lambda/instance_types.json @@ -75,6 +75,10 @@ { "description": "Arizona, USA", "name": "us-west-2" + }, + { + "description": "India", + "name": "asia-south-1" } ] }, @@ -146,12 +150,7 @@ "vcpus": 26 } }, - "regions_with_capacity_available": [ - { - "description": "Utah, USA", - "name": "us-west-3" - } - ] + "regions_with_capacity_available": [] }, "gpu_1x_h100_sxm5": { "instance_type": { @@ -246,12 +245,7 @@ "vcpus": 52 } }, - "regions_with_capacity_available": [ - { - "description": "Central Texas, USA", - "name": "us-south-3" - } - ] + "regions_with_capacity_available": [] }, "gpu_4x_a100": { "instance_type": { @@ -311,7 +305,12 @@ "vcpus": 104 } }, - "regions_with_capacity_available": [] + "regions_with_capacity_available": [ + { + "description": "North Texas, USA", + "name": "us-south-2" + } + ] }, "gpu_8x_a100": { "instance_type": { diff --git a/test/fixtures/latitude/_metadata.json b/test/fixtures/latitude/_metadata.json index 4ea9d87d3..51134fe55 100644 --- a/test/fixtures/latitude/_metadata.json +++ b/test/fixtures/latitude/_metadata.json @@ -1,9 +1,9 @@ { "cloud": "latitude", - "recorded_at": "2026-02-13T05:00:30Z", + "recorded_at": "2026-02-13T06:37:12Z", "fixtures": { - "ssh_keys": {"endpoint": "/ssh_keys", "recorded_at": "2026-02-13T05:00:28Z"}, - "plans": {"endpoint": "/plans", "recorded_at": "2026-02-13T05:00:30Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T05:00:30Z"} + "ssh_keys": {"endpoint": "/ssh_keys", "recorded_at": "2026-02-13T06:37:10Z"}, + "plans": {"endpoint": "/plans", "recorded_at": "2026-02-13T06:37:11Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T06:37:12Z"} } } diff --git a/test/fixtures/latitude/plans.json b/test/fixtures/latitude/plans.json index 296a1f71e..e04f77dde 100644 --- a/test/fixtures/latitude/plans.json +++ b/test/fixtures/latitude/plans.json @@ -378,6 +378,7 @@ "SJC2" ], "in_stock": [ + "DAL", "MIA2" ] }, @@ -639,9 +640,7 @@ "stock_level": "unavailable" }, { - "deploys_instantly": [ - "ubuntu_24_04_x64_lts" - ], + "deploys_instantly": [], "locations": { "available": [ "DAL", @@ -653,9 +652,7 @@ "LAX2", "SJC2" ], - "in_stock": [ - "CHI" - ] + "in_stock": [] }, "name": "United States", "pricing": { @@ -670,7 +667,7 @@ "year": 1672.0 } }, - "stock_level": "low" + "stock_level": "unavailable" }, { "deploys_instantly": [], @@ -845,6 +842,7 @@ "SJC2" ], "in_stock": [ + "LAX", "MIA2" ] }, @@ -1492,7 +1490,7 @@ "year": 7896.0 } }, - "stock_level": "low" + "stock_level": "medium" }, { "deploys_instantly": [], diff --git a/test/fixtures/linode/_metadata.json b/test/fixtures/linode/_metadata.json index b8122a17d..0109f5b7f 100644 --- a/test/fixtures/linode/_metadata.json +++ b/test/fixtures/linode/_metadata.json @@ -1,13 +1,13 @@ { "cloud": "linode", - "recorded_at": "2026-02-13T05:00:35Z", + "recorded_at": "2026-02-13T06:37:17Z", "fixtures": { - "profile": {"endpoint": "/profile", "recorded_at": "2026-02-13T05:00:28Z"}, - "ssh_keys": {"endpoint": "/profile/sshkeys", "recorded_at": "2026-02-13T05:00:28Z"}, - "instances": {"endpoint": "/linode/instances", "recorded_at": "2026-02-13T05:00:28Z"}, - "types": {"endpoint": "/linode/types", "recorded_at": "2026-02-13T05:00:29Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T05:00:29Z"}, - "create_server": {"endpoint": "POST /linode/instances", "type": "live", "recorded_at": "2026-02-13T05:00:32Z"}, - "delete_server": {"endpoint": "DELETE /linode/instances/{id}", "type": "live", "recorded_at": "2026-02-13T05:00:35Z"} + "profile": {"endpoint": "/profile", "recorded_at": "2026-02-13T06:37:10Z"}, + "ssh_keys": {"endpoint": "/profile/sshkeys", "recorded_at": "2026-02-13T06:37:10Z"}, + "instances": {"endpoint": "/linode/instances", "recorded_at": "2026-02-13T06:37:10Z"}, + "types": {"endpoint": "/linode/types", "recorded_at": "2026-02-13T06:37:11Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T06:37:11Z"}, + "create_server": {"endpoint": "POST /linode/instances", "type": "live", "recorded_at": "2026-02-13T06:37:13Z"}, + "delete_server": {"endpoint": "DELETE /linode/instances/{id}", "type": "live", "recorded_at": "2026-02-13T06:37:17Z"} } } diff --git a/test/fixtures/linode/create_server.json b/test/fixtures/linode/create_server.json index e9f3ab34a..f0405c827 100644 --- a/test/fixtures/linode/create_server.json +++ b/test/fixtures/linode/create_server.json @@ -19,20 +19,20 @@ "Block Storage Encryption", "Maintenance Policy" ], - "created": "2026-02-13T05:00:30", + "created": "2026-02-13T06:37:12", "disk_encryption": "enabled", "group": "", "has_user_data": false, - "host_uuid": "65837a7981c07401e8aa47a35c483e28865bd9d9", + "host_uuid": "e36b971087c98564b67156145d654ec2cb99733a", "hypervisor": "kvm", - "id": 91655974, + "id": 91660266, "image": "linode/ubuntu24.04", "interface_generation": "legacy_config", "ipv4": [ - "172.104.30.73" + "45.56.108.222" ], - "ipv6": "2600:3c03::2000:aeff:fe22:ed78/128", - "label": "spawn-record-1770958829", + "ipv6": "2600:3c03::2000:2fff:fe0f:c385/128", + "label": "spawn-record-1770964631", "lke_cluster_id": null, "maintenance_policy": "linode/migrate", "placement_group": null, @@ -49,6 +49,6 @@ "status": "provisioning", "tags": [], "type": "g6-nanode-1", - "updated": "2026-02-13T05:00:30", + "updated": "2026-02-13T06:37:12", "watchdog_enabled": true } diff --git a/test/fixtures/scaleway/_metadata.json b/test/fixtures/scaleway/_metadata.json index 1be9bd968..991bbab65 100644 --- a/test/fixtures/scaleway/_metadata.json +++ b/test/fixtures/scaleway/_metadata.json @@ -1,8 +1,8 @@ { "cloud": "scaleway", - "recorded_at": "2026-02-13T05:00:30Z", + "recorded_at": "2026-02-13T06:37:11Z", "fixtures": { - "servers": {"endpoint": "/servers", "recorded_at": "2026-02-13T05:00:29Z"}, - "images": {"endpoint": "/images?per_page=10", "recorded_at": "2026-02-13T05:00:30Z"} + "servers": {"endpoint": "/servers", "recorded_at": "2026-02-13T06:37:10Z"}, + "images": {"endpoint": "/images?per_page=10", "recorded_at": "2026-02-13T06:37:11Z"} } } diff --git a/test/fixtures/vultr/_metadata.json b/test/fixtures/vultr/_metadata.json index 6eb5cace5..46fa29ce9 100644 --- a/test/fixtures/vultr/_metadata.json +++ b/test/fixtures/vultr/_metadata.json @@ -1,8 +1,8 @@ { "cloud": "vultr", - "recorded_at": "2026-02-13T05:00:32Z", + "recorded_at": "2026-02-13T06:37:13Z", "fixtures": { - "plans": {"endpoint": "/plans", "recorded_at": "2026-02-13T05:00:30Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T05:00:30Z"} + "plans": {"endpoint": "/plans", "recorded_at": "2026-02-13T06:37:12Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T06:37:12Z"} } } diff --git a/test/fixtures/vultr/plans.json b/test/fixtures/vultr/plans.json index 0e454de94..7794b0820 100644 --- a/test/fixtures/vultr/plans.json +++ b/test/fixtures/vultr/plans.json @@ -1068,7 +1068,6 @@ "lax", "atl", "ams", - "lhr", "sjc", "yto", "cdg", diff --git a/test/qa-dry-cycle.sh b/test/qa-dry-cycle.sh new file mode 100644 index 000000000..51b31c0a8 --- /dev/null +++ b/test/qa-dry-cycle.sh @@ -0,0 +1,595 @@ +#!/bin/bash +set -eo pipefail + +# QA Dry Run — Same phases as qa-cycle.sh but NO GitHub interaction. +# Runs locally: records fixtures, mock tests, spawns fix agents, re-tests. +# Skips: git push, PRs, issues, remote branch cleanup, git reset --hard origin/main. +# Artifacts saved to .docs/qa-dry-run-latest/ for inspection. + +REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" +if [[ -z "${REPO_ROOT}" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +fi +cd "${REPO_ROOT}" + +SPAWN_REASON="${SPAWN_REASON:-manual}" +WORKTREE_BASE="/tmp/spawn-worktrees/qa-dry" +CYCLE_TIMEOUT=2700 # 45 min total + +# Output directory for dry-run artifacts +DRY_OUTPUT_DIR="${REPO_ROOT}/.docs/qa-dry-run-latest" +rm -rf "${DRY_OUTPUT_DIR}" +mkdir -p "${DRY_OUTPUT_DIR}" + +LOG_FILE="${DRY_OUTPUT_DIR}/qa-dry-run.log" + +# Results files +RESULTS_PHASE2="/tmp/spawn-qa-dry-results.txt" +RESULTS_PHASE4="/tmp/spawn-qa-dry-results-retry.txt" + +# Ensure directories +mkdir -p "$(dirname "${LOG_FILE}")" "${WORKTREE_BASE}" + +log() { + printf '[%s] [qa-dry] %s\n' "$(date +'%Y-%m-%d %H:%M:%S')" "$*" | tee -a "${LOG_FILE}" +} + +# Log what would happen in a real cycle +dry_log() { + log "DRY_RUN: Would run: $*" + printf '[would-run] %s\n' "$*" >> "${DRY_OUTPUT_DIR}/would-commit.txt" +} + +cleanup() { + local exit_code=$? + log "Running cleanup (exit_code=${exit_code})..." + cd "${REPO_ROOT}" 2>/dev/null || true + git worktree prune 2>/dev/null || true + rm -rf "${WORKTREE_BASE}" 2>/dev/null || true + rm -f "${RESULTS_PHASE2}" "${RESULTS_PHASE4}" "/tmp/spawn-qa-dry-record-output.txt" 2>/dev/null || true + # Save results files to output dir before deleting + [[ -f "${RESULTS_PHASE2}" ]] && cp "${RESULTS_PHASE2}" "${DRY_OUTPUT_DIR}/results-phase2.txt" 2>/dev/null || true + [[ -f "${RESULTS_PHASE4}" ]] && cp "${RESULTS_PHASE4}" "${DRY_OUTPUT_DIR}/results-phase4.txt" 2>/dev/null || true + log "=== QA Dry Run Done (exit_code=${exit_code}) ===" + exit $exit_code +} + +trap cleanup EXIT SIGTERM SIGINT + +# macOS-compatible timeout: run command with a time limit +# Usage: run_with_timeout SECONDS COMMAND [ARGS...] +run_with_timeout() { + local secs="$1"; shift + "$@" & + local pid=$! + local elapsed=0 + while kill -0 "$pid" 2>/dev/null; do + if [[ "$elapsed" -ge "$secs" ]]; then + kill "$pid" 2>/dev/null + sleep 1 + kill -9 "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + return 124 + fi + sleep 1 + elapsed=$((elapsed + 1)) + done + wait "$pid" 2>/dev/null +} + +log "=== Starting QA Dry Run ===" +log "Repo root: ${REPO_ROOT}" +log "Output dir: ${DRY_OUTPUT_DIR}" +log "Timeout: ${CYCLE_TIMEOUT}s" + +# Track start time for total cycle timeout +CYCLE_START=$(date +%s) + +check_timeout() { + local now elapsed + now=$(date +%s) + elapsed=$((now - CYCLE_START)) + if [[ "$elapsed" -ge "$CYCLE_TIMEOUT" ]]; then + log "TIMEOUT: Cycle exceeded ${CYCLE_TIMEOUT}s, stopping" + return 1 + fi + return 0 +} + +# ============================================================ +# Pre-cycle cleanup (local only — no remote branch/PR operations) +# ============================================================ +log "Pre-cycle cleanup (local only)..." + +# Clean stale worktrees +git worktree prune 2>&1 | tee -a "${LOG_FILE}" || true +if [[ -d "${WORKTREE_BASE}" ]]; then + rm -rf "${WORKTREE_BASE}" 2>&1 | tee -a "${LOG_FILE}" || true + log "Removed stale ${WORKTREE_BASE} directory" +fi +mkdir -p "${WORKTREE_BASE}" + +# Delete stale local qa-dry/* branches only +LOCAL_QA_BRANCHES=$(git branch --list 'qa-dry/*' | tr -d ' *') || true +for branch in $LOCAL_QA_BRANCHES; do + if [[ -n "$branch" ]]; then + git branch -D "$branch" 2>&1 | tee -a "${LOG_FILE}" || true + fi +done + +log "Pre-cycle cleanup complete" + +# ============================================================ +# Phase 0: Key Preflight +# ============================================================ +log "=== Phase 0: Key Preflight ===" + +if [[ -f "${REPO_ROOT}/shared/key-request.sh" ]]; then + source "${REPO_ROOT}/shared/key-request.sh" + load_cloud_keys_from_config + if [[ -n "${MISSING_KEY_PROVIDERS:-}" ]]; then + log "Key preflight: Missing keys for: ${MISSING_KEY_PROVIDERS}" + log "Phase 0: Missing keys for: ${MISSING_KEY_PROVIDERS}" + dry_log "request_missing_cloud_keys for: ${MISSING_KEY_PROVIDERS}" + else + log "Phase 0: All cloud keys available" + fi +else + log "Phase 0: shared/key-request.sh not found, skipping key preflight" +fi + +check_timeout || exit 0 + +# ============================================================ +# Phase 1: Record fixtures +# ============================================================ +log "=== Phase 1: Record fixtures ===" + +RECORD_OUTPUT="/tmp/spawn-qa-dry-record-output.txt" +rm -f "${RECORD_OUTPUT}" + +RECORD_EXIT=0 +bash test/record.sh allsaved 2>&1 | tee -a "${LOG_FILE}" | tee "${RECORD_OUTPUT}" || RECORD_EXIT=$? + +if [[ "${RECORD_EXIT}" -eq 0 ]]; then + log "Phase 1: All fixtures recorded successfully" +else + log "Phase 1: Some fixture recordings failed, identifying failed clouds..." + + # Parse which clouds had failures from record.sh output + RECORD_FAILED_CLOUDS="" + current_cloud="" + while IFS= read -r line; do + clean=$(printf '%s' "$line" | sed 's/\x1b\[[0-9;]*m//g') + case "$clean" in + *"Recording "*" ━━━"*) + current_cloud=$(printf '%s' "$clean" | sed 's/.*Recording //; s/ ━━━.*//') + ;; + *"fail "*) + if [[ -n "${current_cloud}" ]]; then + case " ${RECORD_FAILED_CLOUDS} " in + *" ${current_cloud} "*) ;; # already tracked + *) RECORD_FAILED_CLOUDS="${RECORD_FAILED_CLOUDS} ${current_cloud}" ;; + esac + fi + ;; + esac + done < "${RECORD_OUTPUT}" + RECORD_FAILED_CLOUDS=$(printf '%s' "${RECORD_FAILED_CLOUDS}" | sed 's/^ //') + + if [[ -n "${RECORD_FAILED_CLOUDS}" ]]; then + log "Phase 1: Failed clouds: ${RECORD_FAILED_CLOUDS}" + + # Separate auth failures from code failures + NON_AUTH_FAILED_CLOUDS="" + STALE_KEY_PROVIDERS="" + AUTH_PATTERN="401|403|[Uu]nauthorized|[Ff]orbidden|[Ii]nvalid.*(token|key|api)|[Aa]ccess.denied|[Aa]uthentication.failed" + + for cloud in ${RECORD_FAILED_CLOUDS}; do + error_output=$(sed -n "/Recording ${cloud}/,/Recording \|━━━ \|Results:/p" "${RECORD_OUTPUT}" | head -50 || true) + + if printf '%s' "${error_output}" | grep -iqE "${AUTH_PATTERN}"; then + log "Phase 1: Auth failure for ${cloud} — key is stale, skipping fix agent" + if type invalidate_cloud_key &>/dev/null; then + invalidate_cloud_key "${cloud}" + while IFS= read -r var_name; do + [[ -n "${var_name}" ]] && unset "${var_name}" 2>/dev/null || true + done <<< "$(get_cloud_env_vars "${cloud}")" + fi + STALE_KEY_PROVIDERS="${STALE_KEY_PROVIDERS} ${cloud}" + else + NON_AUTH_FAILED_CLOUDS="${NON_AUTH_FAILED_CLOUDS} ${cloud}" + fi + done + NON_AUTH_FAILED_CLOUDS=$(printf '%s' "${NON_AUTH_FAILED_CLOUDS}" | sed 's/^ //') + STALE_KEY_PROVIDERS=$(printf '%s' "${STALE_KEY_PROVIDERS}" | sed 's/^ //') + + if [[ -n "${STALE_KEY_PROVIDERS}" ]]; then + log "Phase 1: Stale keys detected: ${STALE_KEY_PROVIDERS}" + fi + + # Spawn ONE agent per non-auth failed cloud (10 min each, one attempt only) + RECORD_FIX_PIDS="" + for cloud in ${NON_AUTH_FAILED_CLOUDS}; do + check_timeout || break + + error_lines=$(sed -n "/Recording ${cloud}/,/Recording \|━━━ \|Results:/p" "${RECORD_OUTPUT}" | head -30 || true) + + log "Phase 1: Spawning agent to debug ${cloud} recording failure" + worktree="${WORKTREE_BASE}/record-fix-${cloud}" + branch_name="qa-dry/record-fix-${cloud}" + + git worktree add "${worktree}" -b "${branch_name}" HEAD 2>&1 | tee -a "${LOG_FILE}" || { + log "Phase 1: Could not create worktree for ${cloud}, skipping" + continue + } + + ( + cd "${worktree}" + run_with_timeout 600 \ + claude -p "The API fixture recording for cloud '${cloud}' is failing in test/record.sh. + +Error output: +${error_lines} + +This likely means the cloud provider's API has changed. Investigate thoroughly and fix. + +## Investigation Steps (follow in order): + +1. **Check the provider's documentation:** + - Search for \"${cloud} API documentation\" or visit the provider's developer docs + - Look for recent API changelog or breaking changes announcements + - Note any version changes, deprecated endpoints, or new required headers + +2. **Understand the current implementation:** + - Read ${cloud}/lib/common.sh to see how API calls are currently made + - Read test/record.sh to understand the recording flow (get_endpoints, call_api) + - Identify which specific API endpoint is failing from the error output + +3. **Test the API manually (if possible):** + - Try calling the failing endpoint with curl to see the actual response + - Check if authentication headers have changed (API key format, Bearer tokens, etc.) + - Verify the endpoint URL is still correct (base URL changes, versioning) + - Compare the actual API response format with what the code expects + +4. **Check for common issues:** + - Has the base URL changed? (e.g., api.provider.com → api.v2.provider.com) + - Are there new required headers? (Content-Type, User-Agent, API version) + - Has the response schema changed? (different field names, nested structures) + - Are there new rate limits or authentication requirements? + +5. **Fix the lib/common.sh API functions:** + - Update endpoint URLs, headers, or request format to match current API + - Preserve backward compatibility where possible + - Add comments explaining what changed and why + +6. **Test your fix:** + - Run: bash test/record.sh ${cloud} + - Verify all endpoints record successfully + - Check that response formats are correct + +7. **Syntax check and commit:** + - Run: bash -n ${cloud}/lib/common.sh + - Commit with a clear message explaining what API change you fixed + +Only modify ${cloud}/lib/common.sh and test/record.sh if the recording infrastructure needs updating." \ + 2>&1 | tee -a "${LOG_FILE}" || true + + # Check for changes (uncommitted OR committed by the agent) + has_uncommitted=$(git status --porcelain 2>/dev/null) + has_commits=$(git log HEAD@{1}..HEAD --oneline 2>/dev/null || true) + + if [[ -n "$has_uncommitted" ]] && bash -n "${cloud}/lib/common.sh" 2>/dev/null; then + git add "${cloud}/lib/common.sh" "test/record.sh" 2>/dev/null || true + git commit -m "$(printf 'fix: Update %s API integration for recording\n\nAgent: qa-record-fixer (dry run)\nCo-Authored-By: Claude Opus 4.6 ' "${cloud}")" || true + fi + + # Save diff instead of pushing + git diff HEAD~1..HEAD > "${DRY_OUTPUT_DIR}/diff-record-fix-${cloud}.patch" 2>/dev/null || true + dry_log "git push -u origin ${branch_name} && gh pr create for ${cloud} record fix" + ) & + RECORD_FIX_PIDS="${RECORD_FIX_PIDS} $!" + done + + # Wait for record-fix agents + for pid in ${RECORD_FIX_PIDS}; do + wait "$pid" 2>/dev/null || true + done + + # Clean up worktrees + for cloud in ${NON_AUTH_FAILED_CLOUDS}; do + git worktree remove "${WORKTREE_BASE}/record-fix-${cloud}" 2>/dev/null || true + git branch -D "qa-dry/record-fix-${cloud}" 2>/dev/null || true + done + git worktree prune 2>/dev/null || true + + # Re-record (ONE retry, no agents on second failure) — no git reset to origin + log "Phase 1: Re-recording after fixes (no agents on second failure)..." + bash test/record.sh allsaved 2>&1 | tee -a "${LOG_FILE}" || { + log "Phase 1: Re-record still has failures — continuing with existing fixtures" + } + fi + + # Log stale key request (but don't actually send) + if [[ -n "${STALE_KEY_PROVIDERS:-}" ]]; then + dry_log "request_missing_cloud_keys for stale providers: ${STALE_KEY_PROVIDERS}" + fi +fi + +# --- Track consecutive Phase 1 failures per cloud --- +FINAL_RECORD_FAILED="" +FINAL_RECORD_SUCCEEDED="" +if [[ -f "${RECORD_OUTPUT}" ]]; then + _current_cloud="" + _cloud_had_error="" + while IFS= read -r line; do + clean=$(printf '%s' "$line" | sed 's/\x1b\[[0-9;]*m//g') + case "$clean" in + *"Recording "*" ━━━"*) + if [[ -n "${_current_cloud}" ]]; then + if [[ "${_cloud_had_error}" == "true" ]]; then + FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}" + else + FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}" + fi + fi + _current_cloud=$(printf '%s' "$clean" | sed 's/.*Recording //; s/ ━━━.*//') + _cloud_had_error="" + ;; + *"fail "*) + _cloud_had_error="true" + ;; + *"done "*) + ;; + esac + done < "${RECORD_OUTPUT}" + if [[ -n "${_current_cloud}" ]]; then + if [[ "${_cloud_had_error}" == "true" ]]; then + FINAL_RECORD_FAILED="${FINAL_RECORD_FAILED} ${_current_cloud}" + else + FINAL_RECORD_SUCCEEDED="${FINAL_RECORD_SUCCEEDED} ${_current_cloud}" + fi + fi +fi +FINAL_RECORD_FAILED=$(printf '%s' "${FINAL_RECORD_FAILED}" | sed 's/^ //') +FINAL_RECORD_SUCCEEDED=$(printf '%s' "${FINAL_RECORD_SUCCEEDED}" | sed 's/^ //') + +# Log escalation info but don't create GitHub issues +if [[ -n "${FINAL_RECORD_FAILED}" ]]; then + log "Phase 1: Failed clouds (would track/escalate): ${FINAL_RECORD_FAILED}" + for cloud in ${FINAL_RECORD_FAILED}; do + dry_log "gh issue create for ${cloud} persistent recording failure" + done +fi + +rm -f "${RECORD_OUTPUT}" +check_timeout || exit 0 + +# ============================================================ +# Phase 2: Run mock tests +# ============================================================ +log "=== Phase 2: Run mock tests ===" + +rm -f "${RESULTS_PHASE2}" +MOCK_EXIT=0 +RESULTS_FILE="${RESULTS_PHASE2}" bash test/mock.sh 2>&1 | tee -a "${LOG_FILE}" || MOCK_EXIT=$? + +if [[ -f "${RESULTS_PHASE2}" ]]; then + TOTAL_TESTS=$(wc -l < "${RESULTS_PHASE2}" | tr -d ' ') + PASS_COUNT=$(grep -c ':pass$' "${RESULTS_PHASE2}" || true) + FAIL_COUNT=$(grep -c ':fail$' "${RESULTS_PHASE2}" || true) + log "Phase 2: ${PASS_COUNT} passed, ${FAIL_COUNT} failed, ${TOTAL_TESTS} total" + cp "${RESULTS_PHASE2}" "${DRY_OUTPUT_DIR}/results-phase2.txt" 2>/dev/null || true +else + log "Phase 2: No results file generated" + FAIL_COUNT=0 +fi + +check_timeout || exit 0 + +# ============================================================ +# Phase 3: Fix mock failures +# ============================================================ +log "=== Phase 3: Fix failures ===" + +if [[ "${FAIL_COUNT:-0}" -eq 0 ]]; then + log "Phase 3: No failures to fix" +else + FAILURES="" + FAILED_CLOUDS="" + if [[ -f "${RESULTS_PHASE2}" ]]; then + FAILURES=$(grep ':fail$' "${RESULTS_PHASE2}" | sed 's/:fail$//' || true) + FAILED_CLOUDS=$(grep ':fail$' "${RESULTS_PHASE2}" | sed 's/:fail$//' | cut -d/ -f1 | sort -u || true) + fi + + # Capture full mock test output per-cloud for richer agent context + MOCK_OUTPUT_DIR="/tmp/spawn-qa-dry-mock-output" + rm -rf "${MOCK_OUTPUT_DIR}" + mkdir -p "${MOCK_OUTPUT_DIR}" + for cloud in $FAILED_CLOUDS; do + log "Phase 3: Capturing full mock test output for ${cloud}..." + bash test/mock.sh "$cloud" > "${MOCK_OUTPUT_DIR}/${cloud}.log" 2>&1 || true + done + + AGENT_PIDS="" + for cloud in $FAILED_CLOUDS; do + check_timeout || break + + cloud_failures=$(printf '%s\n' $FAILURES | grep "^${cloud}/" || true) + failing_scripts="" + failing_agents="" + for combo in $cloud_failures; do + agent=$(printf '%s' "$combo" | cut -d/ -f2) + script_path="${cloud}/${agent}.sh" + failing_scripts="${failing_scripts} ${script_path}" + failing_agents="${failing_agents} ${agent}" + done + failing_scripts=$(printf '%s' "$failing_scripts" | sed 's/^ //') + failing_agents=$(printf '%s' "$failing_agents" | sed 's/^ //') + + error_context="" + if [[ -f "${MOCK_OUTPUT_DIR}/${cloud}.log" ]]; then + error_context=$(cat "${MOCK_OUTPUT_DIR}/${cloud}.log") + # Also save to output dir for inspection + cp "${MOCK_OUTPUT_DIR}/${cloud}.log" "${DRY_OUTPUT_DIR}/agent-fix-${cloud}.log" 2>/dev/null || true + fi + + fail_count=$(printf '%s\n' $cloud_failures | wc -l | tr -d ' ') + log "Phase 3: Spawning agent to fix ${fail_count} failing script(s) in ${cloud}" + + worktree="${WORKTREE_BASE}/fix-${cloud}" + branch_name="qa-dry/fix-${cloud}" + + git worktree add "${worktree}" -b "${branch_name}" HEAD 2>&1 | tee -a "${LOG_FILE}" || { + log "Phase 3: Could not create worktree for ${cloud}, skipping" + continue + } + + dry_log "git worktree add ... -b qa/fix-${cloud} origin/main" + + # Spawn ONE Claude agent per cloud to fix all its failing scripts (15 min timeout) + ( + cd "${worktree}" + run_with_timeout 900 \ + claude -p "Fix the failing mock tests for cloud '${cloud}' in the spawn codebase. + +Failing scripts: ${failing_scripts} + +Error context from test run: +${error_context} + +## Investigation & Fix Process (be thorough): + +1. **Understand the test infrastructure:** + - Read test/mock.sh to see how mocking works (curl interception, fixture matching) + - Read ${cloud}/lib/common.sh to understand the cloud's API primitives + - Check test/fixtures/${cloud}/ to see what API responses are mocked + +2. **For EACH failing script, investigate the root cause:** + - Read the failing script (${cloud}/.sh) + - Identify which API calls are being made + - Check if the script is making API calls that aren't mocked in test/fixtures/${cloud}/ + - Look for missing fixtures, incorrect API endpoint URLs, or changed function signatures + +3. **Check the cloud provider's current API (if needed):** + - If the script seems correct but fixtures seem outdated, check the provider's API docs + - Compare fixture responses with current API documentation + - Look for API changes: new required parameters, different response formats, endpoint deprecations + +4. **Common failure patterns to check:** + - Missing test fixtures (script calls an API that has no mock response) + - Wrong API endpoint format (e.g., /v2/servers vs /servers) + - Missing authentication setup (API token not set in mock environment) + - Incorrect assumptions about SSH connectivity in mock mode + - Scripts calling commands that don't work in mock mode (ssh, scp without proper mocking) + +5. **Fix the issues:** + - Update scripts to work properly with the mock infrastructure + - Add missing fixture files if needed (test/fixtures/${cloud}/.json) + - Fix API calls to match current provider API + - Ensure proper error handling for mock environment + +6. **Test each fix incrementally:** + - After fixing each script, run: RESULTS_FILE=/tmp/fix-test.txt bash test/mock.sh ${cloud} + - Verify the specific script now passes + - Check for regressions in other scripts + +7. **Syntax check and commit:** + - Run: bash -n on each modified script + - Test final state: RESULTS_FILE=/tmp/fix-test.txt bash test/mock.sh ${cloud} + - Commit all fixes with a message listing what was fixed and why + +You can modify: scripts in ${cloud}/, test/fixtures/${cloud}/, and test/mock.sh if infrastructure updates are needed." \ + 2>&1 | tee -a "${LOG_FILE}" || true + + # Always check for changes + syntax_ok=true + for script in ${failing_scripts}; do + if [[ -f "${script}" ]] && ! bash -n "${script}" 2>/dev/null; then + log "Phase 3: Syntax check failed for ${script}" + syntax_ok=false + fi + done + + # Stage any uncommitted changes the agent left behind + if [[ "$syntax_ok" == "true" ]] && [[ -n "$(git status --porcelain)" ]]; then + git add ${failing_scripts} "${cloud}/lib/common.sh" "test/fixtures/${cloud}/" "test/mock.sh" 2>/dev/null || true + git commit -m "$(cat < +FIXEOF + )" || true + fi + + # Save diff instead of pushing + git diff HEAD~1..HEAD > "${DRY_OUTPUT_DIR}/diff-fix-${cloud}.patch" 2>/dev/null || true + dry_log "git add ${cloud}/ test/fixtures/${cloud}/ test/mock.sh && git commit && git push && gh pr create for ${cloud}" + ) & + AGENT_PIDS="${AGENT_PIDS} $!" + done + + # Wait for all fix agents to complete + for pid in $AGENT_PIDS; do + wait "$pid" 2>/dev/null || true + done + + # Clean up worktrees (one per cloud) + for cloud in $FAILED_CLOUDS; do + git worktree remove "${WORKTREE_BASE}/fix-${cloud}" 2>/dev/null || true + git branch -D "qa-dry/fix-${cloud}" 2>/dev/null || true + done + git worktree prune 2>/dev/null || true + + # Clean up per-cloud mock output + rm -rf "${MOCK_OUTPUT_DIR}" 2>/dev/null || true + + log "Phase 3: Fix agents complete" +fi + +check_timeout || exit 0 + +# ============================================================ +# Phase 4: Re-run mock tests + update README (local only) +# ============================================================ +log "=== Phase 4: Re-run tests and update README ===" + +# No git fetch/reset — work with current local state + +rm -f "${RESULTS_PHASE4}" +RESULTS_FILE="${RESULTS_PHASE4}" bash test/mock.sh 2>&1 | tee -a "${LOG_FILE}" || true + +if [[ -f "${RESULTS_PHASE4}" ]]; then + RETRY_PASS=$(grep -c ':pass$' "${RESULTS_PHASE4}" || true) + RETRY_FAIL=$(grep -c ':fail$' "${RESULTS_PHASE4}" || true) + log "Phase 4: ${RETRY_PASS} passed, ${RETRY_FAIL} failed" + cp "${RESULTS_PHASE4}" "${DRY_OUTPUT_DIR}/results-phase4.txt" 2>/dev/null || true + + python3 test/update-readme.py "${RESULTS_PHASE4}" 2>&1 | tee -a "${LOG_FILE}" + + # Save README diff instead of pushing + if [[ -n "$(git diff --name-only README.md 2>/dev/null)" ]]; then + git diff README.md > "${DRY_OUTPUT_DIR}/diff-readme.patch" 2>/dev/null || true + log "Phase 4: README changes saved to ${DRY_OUTPUT_DIR}/diff-readme.patch" + dry_log "git checkout -b qa/readme-update-\$(date +%s) && git add README.md && git commit && git push && gh pr create" + # Revert the local README change so we don't leave dirty state + git checkout README.md 2>/dev/null || true + else + log "Phase 4: No README changes needed" + fi +else + log "Phase 4: No results file generated" +fi + +# Final summary +log "=== QA Dry Run Summary ===" +log "Phase 2: ${PASS_COUNT:-0} pass / ${FAIL_COUNT:-0} fail" +log "Phase 4: ${RETRY_PASS:-0} pass / ${RETRY_FAIL:-0} fail" +if [[ "${FAIL_COUNT:-0}" -gt 0 ]] && [[ "${RETRY_FAIL:-0}" -lt "${FAIL_COUNT:-0}" ]]; then + FIXED=$(( ${FAIL_COUNT:-0} - ${RETRY_FAIL:-0} )) + log "Fixed ${FIXED} failure(s) this cycle" +fi +log "Artifacts saved to: ${DRY_OUTPUT_DIR}/" +log "=== QA Dry Run Complete ===" From 6d0467f6675847d6a3e9b178dffd2b3bfeb9a52c Mon Sep 17 00:00:00 2001 From: Ahmed Abushagur Date: Tue, 10 Feb 2026 14:32:13 -0800 Subject: [PATCH 05/13] feat: testing --- runpod/claude.sh | 71 ++++++ test/fixtures/digitalocean/_metadata.json | 14 +- test/fixtures/digitalocean/regions.json | 119 ++++++--- test/fixtures/hetzner/_metadata.json | 14 +- test/fixtures/hetzner/create_server.json | 32 ++- test/fixtures/hetzner/delete_server.json | 6 +- test/fixtures/hetzner/servers.json | 283 +++++++++++++++++++++- test/fixtures/linode/_metadata.json | 14 +- test/fixtures/linode/regions.json | 109 +++------ test/fixtures/vultr/_metadata.json | 9 +- test/fixtures/vultr/plans.json | 80 +++--- test/record.sh | 62 +---- vastai/claude.sh | 69 ++++++ 13 files changed, 630 insertions(+), 252 deletions(-) create mode 100644 runpod/claude.sh create mode 100644 vastai/claude.sh diff --git a/runpod/claude.sh b/runpod/claude.sh new file mode 100644 index 000000000..2559eb80b --- /dev/null +++ b/runpod/claude.sh @@ -0,0 +1,71 @@ +#!/bin/bash +# shellcheck disable=SC2154 +set -eo pipefail + +# Source common functions - try local file first, fall back to remote +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +# shellcheck source=runpod/lib/common.sh +if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then + source "${SCRIPT_DIR}/lib/common.sh" +else + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/runpod/lib/common.sh)" +fi + +log_info "Claude Code on RunPod" +echo "" + +# 1. Ensure RunPod API key is configured +ensure_runpod_token + +# 2. Ensure SSH key exists (user must add to RunPod console) +ensure_ssh_key + +# 3. Get pod name and create pod +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" + +# 4. Wait for SSH connectivity and install base tools +verify_server_connectivity +install_base_tools + +# 5. Verify Claude Code is installed (fallback to manual install) +log_warn "Verifying Claude Code installation..." +if ! run_server "${RUNPOD_POD_ID}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then + log_warn "Claude Code not found, installing manually..." + run_server "${RUNPOD_POD_ID}" "curl -fsSL https://claude.ai/install.sh | bash" +fi +log_info "Claude Code is installed" + +# 6. Get OpenRouter API key +echo "" +if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then + log_info "Using OpenRouter API key from environment" +else + OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) +fi + +# 7. Inject environment variables +log_warn "Setting up environment variables..." +inject_env_vars_ssh "${RUNPOD_POD_ID}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ + "ANTHROPIC_AUTH_TOKEN=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_API_KEY=" \ + "CLAUDE_CODE_SKIP_ONBOARDING=1" \ + "CLAUDE_CODE_ENABLE_TELEMETRY=0" + +# 8. Configure Claude Code settings +setup_claude_code_config "${OPENROUTER_API_KEY}" \ + "upload_file ${RUNPOD_POD_ID}" \ + "run_server ${RUNPOD_POD_ID}" + +echo "" +log_info "RunPod pod setup completed successfully!" +log_info "Pod: ${SERVER_NAME} (ID: ${RUNPOD_POD_ID})" +echo "" + +# 9. Start Claude Code interactively +log_warn "Starting Claude Code..." +sleep 1 +clear +interactive_session "${RUNPOD_POD_ID}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/test/fixtures/digitalocean/_metadata.json b/test/fixtures/digitalocean/_metadata.json index 3b9e27360..9b546f315 100644 --- a/test/fixtures/digitalocean/_metadata.json +++ b/test/fixtures/digitalocean/_metadata.json @@ -1,13 +1,11 @@ { "cloud": "digitalocean", - "recorded_at": "2026-02-13T06:37:18Z", + "recorded_at": "2026-02-10T21:55:29Z", "fixtures": { - "account": {"endpoint": "/account", "recorded_at": "2026-02-13T06:37:10Z"}, - "ssh_keys": {"endpoint": "/account/keys", "recorded_at": "2026-02-13T06:37:11Z"}, - "droplets": {"endpoint": "/droplets", "recorded_at": "2026-02-13T06:37:11Z"}, - "sizes": {"endpoint": "/sizes", "recorded_at": "2026-02-13T06:37:12Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T06:37:12Z"}, - "create_server": {"endpoint": "POST /droplets", "type": "live", "recorded_at": "2026-02-13T06:37:14Z"}, - "delete_server": {"endpoint": "DELETE /droplets/{id}", "type": "live", "recorded_at": "2026-02-13T06:37:18Z"} + "account": {"endpoint": "/account", "recorded_at": "2026-02-10T21:55:27Z"}, + "ssh_keys": {"endpoint": "/account/keys", "recorded_at": "2026-02-10T21:55:27Z"}, + "droplets": {"endpoint": "/droplets", "recorded_at": "2026-02-10T21:55:28Z"}, + "sizes": {"endpoint": "/sizes", "recorded_at": "2026-02-10T21:55:29Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-10T21:55:29Z"} } } diff --git a/test/fixtures/digitalocean/regions.json b/test/fixtures/digitalocean/regions.json index 3fed997ce..2ec9f844f 100644 --- a/test/fixtures/digitalocean/regions.json +++ b/test/fixtures/digitalocean/regions.json @@ -282,43 +282,32 @@ "c5-8vcpu-16gb-intel", "so1_5-4vcpu-32gb", "m-8vcpu-64gb", - "c-16", "g6_5-8vcpu-32gb", "g5_5-8vcpu-32gb-intel", - "c2-16vcpu-32gb", "m-8vcpu-64gb-intel", "m3-8vcpu-64gb", "c-16-intel", "m3-8vcpu-64gb-intel", "c2-16vcpu-32gb-intel", - "c5-16vcpu-32gb", - "g-16vcpu-64gb", "so-8vcpu-64gb-intel", "so-8vcpu-64gb", "m6-8vcpu-64gb", - "gd-16vcpu-64gb", "so1_5-8vcpu-64gb-intel", "g-16vcpu-64gb-intel", "gd-16vcpu-64gb-intel", "c5-16vcpu-32gb-intel", "so1_5-8vcpu-64gb", "m-16vcpu-128gb", - "c-32", - "g6_5-16vcpu-64gb", "g5_5-16vcpu-64gb-intel", - "c2-32vcpu-64gb", "m-16vcpu-128gb-intel", "m3-16vcpu-128gb", "c-32-intel", "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", - "c5-32vcpu-64gb", "m-24vcpu-192gb", - "g-32vcpu-128gb", "so-16vcpu-128gb-intel", "so-16vcpu-128gb", "m6-16vcpu-128gb", - "gd-32vcpu-128gb", "so1_5-16vcpu-128gb-intel", "m-24vcpu-192gb-intel", "g-32vcpu-128gb-intel", @@ -326,31 +315,22 @@ "gd-32vcpu-128gb-intel", "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", - "c-48-intel", "m3-24vcpu-192gb-intel", - "m-32vcpu-256gb", - "g6_5-32vcpu-128gb", - "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", "so-24vcpu-192gb-intel", "so-24vcpu-192gb", "m6-24vcpu-192gb", "m-32vcpu-256gb-intel", - "m3-32vcpu-256gb", "so1_5-24vcpu-192gb-intel", "m3-32vcpu-256gb-intel", "g-48vcpu-192gb-intel", "gd-48vcpu-192gb-intel", - "c5-48vcpu-96gb-intel", "so1_5-24vcpu-192gb", "so-32vcpu-256gb-intel", - "so-32vcpu-256gb", - "m6-32vcpu-256gb", "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", "gpu-h100x1-80gb", - "gpu-h200x1-141gb", - "so1_5-32vcpu-256gb" + "gpu-h200x1-141gb" ], "slug": "nyc2" }, @@ -409,6 +389,7 @@ "gd-2vcpu-8gb", "g-2vcpu-8gb-intel", "gd-2vcpu-8gb-intel", + "s-4vcpu-16gb-amd", "m-2vcpu-16gb", "c-4", "g6_5-2vcpu-8gb", @@ -420,6 +401,7 @@ "m3-2vcpu-16gb", "c-4-intel", "m3-2vcpu-16gb-intel", + "s-8vcpu-16gb-amd", "s-8vcpu-16gb-intel", "c2-4vcpu-8gb-intel", "c5-4vcpu-8gb", @@ -434,6 +416,7 @@ "gd-4vcpu-16gb-intel", "c5-4vcpu-8gb-intel", "so1_5-2vcpu-16gb", + "s-8vcpu-32gb-amd", "m-4vcpu-32gb", "c-8", "g6_5-4vcpu-16gb", @@ -488,7 +471,6 @@ "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", "c5-32vcpu-64gb", - "c-48", "m-24vcpu-192gb", "g-32vcpu-128gb", "so-16vcpu-128gb-intel", @@ -934,6 +916,7 @@ "gd-2vcpu-8gb", "g-2vcpu-8gb-intel", "gd-2vcpu-8gb-intel", + "s-4vcpu-16gb-amd", "m-2vcpu-16gb", "c-4", "g6_5-2vcpu-8gb", @@ -945,6 +928,7 @@ "m3-2vcpu-16gb", "c-4-intel", "m3-2vcpu-16gb-intel", + "s-8vcpu-16gb-amd", "s-8vcpu-16gb-intel", "c2-4vcpu-8gb-intel", "c5-4vcpu-8gb", @@ -959,6 +943,7 @@ "gd-4vcpu-16gb-intel", "c5-4vcpu-8gb-intel", "so1_5-2vcpu-16gb", + "s-8vcpu-32gb-amd", "m-4vcpu-32gb", "c-8", "g6_5-4vcpu-16gb", @@ -1026,18 +1011,36 @@ "m3-24vcpu-192gb", "g-40vcpu-160gb", "gd-32vcpu-128gb-intel", + "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", + "c-48-intel", "m3-24vcpu-192gb-intel", "gd-40vcpu-160gb", "g6_5-32vcpu-128gb", + "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", "so-24vcpu-192gb-intel", + "so-24vcpu-192gb", + "m6-24vcpu-192gb", "m-32vcpu-256gb-intel", + "c-60-intel", "so1_5-24vcpu-192gb-intel", "m3-32vcpu-256gb-intel", + "g-48vcpu-192gb-intel", + "c2-60vcpu-120gb-intel", + "gd-48vcpu-192gb-intel", + "c5-48vcpu-96gb-intel", "so-32vcpu-256gb-intel", + "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", - "gpu-h100x1-80gb" + "g-60vcpu-240gb-intel", + "m-48vcpu-384gb-intel", + "gd-60vcpu-240gb-intel", + "c5-60vcpu-120gb-intel", + "gpu-h100x1-80gb", + "m3-48vcpu-384gb-intel", + "g5_5-60vcpu-240gb-intel", + "so-48vcpu-384gb-intel" ], "slug": "ams3" }, @@ -1155,16 +1158,13 @@ "c5-16vcpu-32gb-intel", "so1_5-8vcpu-64gb", "m-16vcpu-128gb", - "c-32", "g6_5-16vcpu-64gb", "g5_5-16vcpu-64gb-intel", - "c2-32vcpu-64gb", "m-16vcpu-128gb-intel", "m3-16vcpu-128gb", "c-32-intel", "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", - "c5-32vcpu-64gb", "m-24vcpu-192gb", "g-32vcpu-128gb", "so-16vcpu-128gb-intel", @@ -1179,10 +1179,12 @@ "gd-32vcpu-128gb-intel", "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", + "c-48-intel", "m3-24vcpu-192gb-intel", "m-32vcpu-256gb", "gd-40vcpu-160gb", "g6_5-32vcpu-128gb", + "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", "so-24vcpu-192gb-intel", "so-24vcpu-192gb", @@ -1196,6 +1198,7 @@ "g-48vcpu-192gb-intel", "c2-60vcpu-120gb-intel", "gd-48vcpu-192gb-intel", + "c5-48vcpu-96gb-intel", "so1_5-24vcpu-192gb", "so-32vcpu-256gb-intel", "g5_5-48vcpu-192gb-intel", @@ -1332,19 +1335,26 @@ "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", "m-24vcpu-192gb", + "g-32vcpu-128gb", "so-16vcpu-128gb-intel", "so-16vcpu-128gb", "m6-16vcpu-128gb", + "gd-32vcpu-128gb", "so1_5-16vcpu-128gb-intel", + "gpu-l40sx1-48gb", "gpu-6000adax1-48gb", "m-24vcpu-192gb-intel", "g-32vcpu-128gb-intel", "m3-24vcpu-192gb", + "g-40vcpu-160gb", "gd-32vcpu-128gb-intel", "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", "c-48-intel", "m3-24vcpu-192gb-intel", + "m-32vcpu-256gb", + "gd-40vcpu-160gb", + "g6_5-32vcpu-128gb", "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", "so-24vcpu-192gb-intel", @@ -1352,14 +1362,18 @@ "m6-24vcpu-192gb", "m-32vcpu-256gb-intel", "c-60-intel", + "m3-32vcpu-256gb", "so1_5-24vcpu-192gb-intel", "m3-32vcpu-256gb-intel", + "g6_5-40vcpu-160gb", "g-48vcpu-192gb-intel", "c2-60vcpu-120gb-intel", "gd-48vcpu-192gb-intel", "c5-48vcpu-96gb-intel", "so1_5-24vcpu-192gb", "so-32vcpu-256gb-intel", + "so-32vcpu-256gb", + "m6-32vcpu-256gb", "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", "g-60vcpu-240gb-intel", @@ -1367,6 +1381,7 @@ "gd-60vcpu-240gb-intel", "c5-60vcpu-120gb-intel", "gpu-h100x1-80gb", + "so1_5-32vcpu-256gb", "m3-48vcpu-384gb-intel", "g5_5-60vcpu-240gb-intel", "so-48vcpu-384gb-intel" @@ -1448,41 +1463,78 @@ "so1_5-4vcpu-32gb-intel", "g-8vcpu-32gb-intel", "gd-8vcpu-32gb-intel", + "c5-8vcpu-16gb-intel", "so1_5-4vcpu-32gb", "m-8vcpu-64gb", "c-16", "g6_5-8vcpu-32gb", + "g5_5-8vcpu-32gb-intel", "c2-16vcpu-32gb", "m-8vcpu-64gb-intel", "m3-8vcpu-64gb", "c-16-intel", + "m3-8vcpu-64gb-intel", "c2-16vcpu-32gb-intel", "c5-16vcpu-32gb", "g-16vcpu-64gb", + "so-8vcpu-64gb-intel", "so-8vcpu-64gb", "m6-8vcpu-64gb", "gd-16vcpu-64gb", + "so1_5-8vcpu-64gb-intel", "g-16vcpu-64gb-intel", "gd-16vcpu-64gb-intel", + "c5-16vcpu-32gb-intel", "so1_5-8vcpu-64gb", "m-16vcpu-128gb", "c-32", "g6_5-16vcpu-64gb", + "g5_5-16vcpu-64gb-intel", "c2-32vcpu-64gb", "m-16vcpu-128gb-intel", "m3-16vcpu-128gb", "c-32-intel", + "m3-16vcpu-128gb-intel", + "c2-32vcpu-64gb-intel", "c-48", "m-24vcpu-192gb", "g-32vcpu-128gb", + "so-16vcpu-128gb-intel", "so-16vcpu-128gb", "m6-16vcpu-128gb", "gd-32vcpu-128gb", + "so1_5-16vcpu-128gb-intel", "c2-48vcpu-96gb", + "m-24vcpu-192gb-intel", "g-32vcpu-128gb-intel", "g-40vcpu-160gb", + "gd-32vcpu-128gb-intel", + "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", - "gd-40vcpu-160gb" + "c-48-intel", + "m3-24vcpu-192gb-intel", + "gd-40vcpu-160gb", + "c2-48vcpu-96gb-intel", + "g5_5-32vcpu-128gb-intel", + "so-24vcpu-192gb-intel", + "m-32vcpu-256gb-intel", + "c-60-intel", + "so1_5-24vcpu-192gb-intel", + "m3-32vcpu-256gb-intel", + "g-48vcpu-192gb-intel", + "c2-60vcpu-120gb-intel", + "gd-48vcpu-192gb-intel", + "c5-48vcpu-96gb-intel", + "so-32vcpu-256gb-intel", + "g5_5-48vcpu-192gb-intel", + "so1_5-32vcpu-256gb-intel", + "g-60vcpu-240gb-intel", + "m-48vcpu-384gb-intel", + "gd-60vcpu-240gb-intel", + "c5-60vcpu-120gb-intel", + "m3-48vcpu-384gb-intel", + "g5_5-60vcpu-240gb-intel", + "so-48vcpu-384gb-intel" ], "slug": "sfo2" }, @@ -1785,6 +1837,7 @@ "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", "c5-32vcpu-64gb", + "c-48", "m-24vcpu-192gb", "g-32vcpu-128gb", "so-16vcpu-128gb-intel", @@ -1792,6 +1845,7 @@ "m6-16vcpu-128gb", "gd-32vcpu-128gb", "so1_5-16vcpu-128gb-intel", + "c2-48vcpu-96gb", "m-24vcpu-192gb-intel", "g-32vcpu-128gb-intel", "m3-24vcpu-192gb", @@ -1799,30 +1853,40 @@ "gd-32vcpu-128gb-intel", "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", + "c-48-intel", "m3-24vcpu-192gb-intel", "m-32vcpu-256gb", "gd-40vcpu-160gb", "g6_5-32vcpu-128gb", + "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", + "c5-48vcpu-96gb", "so-24vcpu-192gb-intel", "so-24vcpu-192gb", "m6-24vcpu-192gb", "m-32vcpu-256gb-intel", + "c-60-intel", "m3-32vcpu-256gb", "so1_5-24vcpu-192gb-intel", "m3-32vcpu-256gb-intel", "g6_5-40vcpu-160gb", "g-48vcpu-192gb-intel", + "c2-60vcpu-120gb-intel", "gd-48vcpu-192gb-intel", + "c5-48vcpu-96gb-intel", "so1_5-24vcpu-192gb", "so-32vcpu-256gb-intel", "so-32vcpu-256gb", "m6-32vcpu-256gb", "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", + "g-60vcpu-240gb-intel", "m-48vcpu-384gb-intel", + "gd-60vcpu-240gb-intel", + "c5-60vcpu-120gb-intel", "so1_5-32vcpu-256gb", "m3-48vcpu-384gb-intel", + "g5_5-60vcpu-240gb-intel", "so-48vcpu-384gb-intel" ], "slug": "sfo3" @@ -2097,6 +2161,7 @@ "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", "m-48vcpu-384gb-intel", + "gpu-h200x1-141gb", "m3-48vcpu-384gb-intel", "so-48vcpu-384gb-intel" ], diff --git a/test/fixtures/hetzner/_metadata.json b/test/fixtures/hetzner/_metadata.json index 12a0d7c8a..c5883a23a 100644 --- a/test/fixtures/hetzner/_metadata.json +++ b/test/fixtures/hetzner/_metadata.json @@ -1,12 +1,12 @@ { "cloud": "hetzner", - "recorded_at": "2026-02-13T06:37:18Z", + "recorded_at": "2026-02-10T21:55:26Z", "fixtures": { - "server_types": {"endpoint": "/server_types?per_page=50", "recorded_at": "2026-02-13T06:37:11Z"}, - "locations": {"endpoint": "/locations", "recorded_at": "2026-02-13T06:37:11Z"}, - "ssh_keys": {"endpoint": "/ssh_keys", "recorded_at": "2026-02-13T06:37:12Z"}, - "servers": {"endpoint": "/servers", "recorded_at": "2026-02-13T06:37:13Z"}, - "create_server": {"endpoint": "POST /servers", "type": "live", "recorded_at": "2026-02-13T06:37:15Z"}, - "delete_server": {"endpoint": "DELETE /servers/{id}", "type": "live", "recorded_at": "2026-02-13T06:37:18Z"} + "server_types": {"endpoint": "/server_types?per_page=50", "recorded_at": "2026-02-10T21:55:19Z"}, + "locations": {"endpoint": "/locations", "recorded_at": "2026-02-10T21:55:20Z"}, + "ssh_keys": {"endpoint": "/ssh_keys", "recorded_at": "2026-02-10T21:55:21Z"}, + "servers": {"endpoint": "/servers", "recorded_at": "2026-02-10T21:55:22Z"}, + "create_server": {"endpoint": "POST /servers", "type": "live", "recorded_at": "2026-02-10T21:55:24Z"}, + "delete_server": {"endpoint": "DELETE /servers/{id}", "type": "live", "recorded_at": "2026-02-10T21:55:26Z"} } } diff --git a/test/fixtures/hetzner/create_server.json b/test/fixtures/hetzner/create_server.json index 448f16ed9..74637d827 100644 --- a/test/fixtures/hetzner/create_server.json +++ b/test/fixtures/hetzner/create_server.json @@ -3,11 +3,11 @@ "command": "create_server", "error": null, "finished": null, - "id": 611311291703796, + "id": 610821665885201, "progress": 0, "resources": [ { - "id": 120895015, + "id": 120682780, "type": "server" }, { @@ -15,7 +15,7 @@ "type": "image" } ], - "started": "2026-02-13T06:37:14Z", + "started": "2026-02-10T21:55:23Z", "status": "running" }, "next_actions": [ @@ -23,23 +23,23 @@ "command": "start_server", "error": null, "finished": null, - "id": 611311291703797, - "parent_id": 611311291703796, + "id": 610821665885202, + "parent_id": 610821665885201, "progress": 0, "resources": [ { - "id": 120895015, + "id": 120682780, "type": "server" } ], - "started": "2026-02-13T06:37:14Z", + "started": "2026-02-10T21:55:23Z", "status": "running" } ], "root_password": null, "server": { "backup_window": null, - "created": "2026-02-13T06:37:14Z", + "created": "2026-02-10T21:55:23Z", "datacenter": { "description": "Nuremberg 1 virtual DC 3", "id": 2, @@ -65,7 +65,6 @@ 45, 93, 94, - 95, 96, 97, 98, @@ -92,7 +91,6 @@ 45, 93, 94, - 95, 96, 97, 98, @@ -168,7 +166,7 @@ ] } }, - "id": 120895015, + "id": 120682780, "image": { "architecture": "x86", "bound_to": null, @@ -207,7 +205,7 @@ "network_zone": "eu-central" }, "locked": false, - "name": "spawn-record-1770964633", + "name": "spawn-record-1770760522", "outgoing_traffic": 0, "placement_group": null, "primary_disk_size": 40, @@ -221,15 +219,15 @@ "floating_ips": [], "ipv4": { "blocked": false, - "dns_ptr": "static.81.176.245.188.clients.your-server.de", - "id": 117982494, - "ip": "188.245.176.81" + "dns_ptr": "static.197.210.245.188.clients.your-server.de", + "id": 117603379, + "ip": "188.245.210.197" }, "ipv6": { "blocked": false, "dns_ptr": [], - "id": 117982495, - "ip": "2a01:4f8:1c1a:f748::/64" + "id": 117603380, + "ip": "2a01:4f8:1c1e:bc55::/64" } }, "rescue_enabled": false, diff --git a/test/fixtures/hetzner/delete_server.json b/test/fixtures/hetzner/delete_server.json index 0c51bbad8..bbf825949 100644 --- a/test/fixtures/hetzner/delete_server.json +++ b/test/fixtures/hetzner/delete_server.json @@ -3,15 +3,15 @@ "command": "delete_server", "error": null, "finished": null, - "id": 611311291703818, + "id": 610821665885215, "progress": 0, "resources": [ { - "id": 120895015, + "id": 120682780, "type": "server" } ], - "started": "2026-02-13T06:37:17Z", + "started": "2026-02-10T21:55:26Z", "status": "running" } } diff --git a/test/fixtures/hetzner/servers.json b/test/fixtures/hetzner/servers.json index 8ff3b8b96..dd6315039 100644 --- a/test/fixtures/hetzner/servers.json +++ b/test/fixtures/hetzner/servers.json @@ -6,8 +6,287 @@ "page": 1, "per_page": 25, "previous_page": null, - "total_entries": 0 + "total_entries": 1 } }, - "servers": [] + "servers": [ + { + "backup_window": null, + "created": "2026-02-10T21:32:27Z", + "datacenter": { + "description": "Nuremberg 1 virtual DC 3", + "id": 2, + "location": { + "city": "Nuremberg", + "country": "DE", + "description": "Nuremberg DC Park 1", + "id": 2, + "latitude": 49.452102, + "longitude": 11.076665, + "name": "nbg1", + "network_zone": "eu-central" + }, + "name": "nbg1-dc3", + "server_types": { + "available": [ + 27, + 28, + 29, + 30, + 31, + 32, + 45, + 93, + 94, + 96, + 97, + 98, + 99, + 100, + 101, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117 + ], + "available_for_migration": [ + 27, + 28, + 29, + 30, + 31, + 32, + 45, + 93, + 94, + 96, + 97, + 98, + 99, + 100, + 101, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117 + ], + "supported": [ + 1, + 3, + 5, + 7, + 9, + 11, + 12, + 13, + 14, + 15, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 104, + 105, + 106, + 107, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117 + ] + } + }, + "id": 120681182, + "image": { + "architecture": "x86", + "bound_to": null, + "created": "2024-04-25T13:26:27Z", + "created_from": null, + "deleted": null, + "deprecated": null, + "description": "Ubuntu 24.04", + "disk_size": 5, + "id": 161547269, + "image_size": null, + "labels": {}, + "name": "ubuntu-24.04", + "os_flavor": "ubuntu", + "os_version": "24.04", + "protection": { + "delete": false + }, + "rapid_deploy": true, + "status": "available", + "type": "system" + }, + "included_traffic": 21990232555520, + "ingoing_traffic": 0, + "iso": null, + "labels": {}, + "load_balancers": [], + "location": { + "city": "Nuremberg", + "country": "DE", + "description": "Nuremberg DC Park 1", + "id": 2, + "latitude": 49.452102, + "longitude": 11.076665, + "name": "nbg1", + "network_zone": "eu-central" + }, + "locked": false, + "name": "claude", + "outgoing_traffic": 0, + "placement_group": null, + "primary_disk_size": 40, + "private_net": [], + "protection": { + "delete": false, + "rebuild": false + }, + "public_net": { + "firewalls": [], + "floating_ips": [], + "ipv4": { + "blocked": false, + "dns_ptr": "static.157.65.225.46.clients.your-server.de", + "id": 117600167, + "ip": "46.225.65.157" + }, + "ipv6": { + "blocked": false, + "dns_ptr": [], + "id": 117600168, + "ip": "2a01:4f8:1c19:e41b::/64" + } + }, + "rescue_enabled": false, + "server_type": { + "architecture": "x86", + "category": "cost_optimized", + "cores": 2, + "cpu_type": "shared", + "deprecated": false, + "deprecation": null, + "description": "CX 23", + "disk": 40, + "id": 114, + "locations": [ + { + "deprecation": null, + "id": 1, + "name": "fsn1" + }, + { + "deprecation": null, + "id": 2, + "name": "nbg1" + }, + { + "deprecation": null, + "id": 3, + "name": "hel1" + } + ], + "memory": 4, + "name": "cx23", + "prices": [ + { + "included_traffic": 21990232555520, + "location": "fsn1", + "price_hourly": { + "gross": "0.0056000000000000", + "net": "0.0056000000" + }, + "price_monthly": { + "gross": "3.4900000000000000", + "net": "3.4900000000" + }, + "price_per_tb_traffic": { + "gross": "1.2000000000000000", + "net": "1.2000000000" + } + }, + { + "included_traffic": 21990232555520, + "location": "hel1", + "price_hourly": { + "gross": "0.0056000000000000", + "net": "0.0056000000" + }, + "price_monthly": { + "gross": "3.4900000000000000", + "net": "3.4900000000" + }, + "price_per_tb_traffic": { + "gross": "1.2000000000000000", + "net": "1.2000000000" + } + }, + { + "included_traffic": 21990232555520, + "location": "nbg1", + "price_hourly": { + "gross": "0.0056000000000000", + "net": "0.0056000000" + }, + "price_monthly": { + "gross": "3.4900000000000000", + "net": "3.4900000000" + }, + "price_per_tb_traffic": { + "gross": "1.2000000000000000", + "net": "1.2000000000" + } + } + ], + "storage_type": "local" + }, + "status": "running", + "volumes": [] + } + ] } diff --git a/test/fixtures/linode/_metadata.json b/test/fixtures/linode/_metadata.json index 0109f5b7f..029fa863d 100644 --- a/test/fixtures/linode/_metadata.json +++ b/test/fixtures/linode/_metadata.json @@ -1,13 +1,11 @@ { "cloud": "linode", - "recorded_at": "2026-02-13T06:37:17Z", + "recorded_at": "2026-02-10T21:55:36Z", "fixtures": { - "profile": {"endpoint": "/profile", "recorded_at": "2026-02-13T06:37:10Z"}, - "ssh_keys": {"endpoint": "/profile/sshkeys", "recorded_at": "2026-02-13T06:37:10Z"}, - "instances": {"endpoint": "/linode/instances", "recorded_at": "2026-02-13T06:37:10Z"}, - "types": {"endpoint": "/linode/types", "recorded_at": "2026-02-13T06:37:11Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T06:37:11Z"}, - "create_server": {"endpoint": "POST /linode/instances", "type": "live", "recorded_at": "2026-02-13T06:37:13Z"}, - "delete_server": {"endpoint": "DELETE /linode/instances/{id}", "type": "live", "recorded_at": "2026-02-13T06:37:17Z"} + "profile": {"endpoint": "/profile", "recorded_at": "2026-02-10T21:55:34Z"}, + "ssh_keys": {"endpoint": "/profile/sshkeys", "recorded_at": "2026-02-10T21:55:34Z"}, + "instances": {"endpoint": "/linode/instances", "recorded_at": "2026-02-10T21:55:35Z"}, + "types": {"endpoint": "/linode/types", "recorded_at": "2026-02-10T21:55:35Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-10T21:55:36Z"} } } diff --git a/test/fixtures/linode/regions.json b/test/fixtures/linode/regions.json index e8228ae3f..201fd3543 100644 --- a/test/fixtures/linode/regions.json +++ b/test/fixtures/linode/regions.json @@ -30,8 +30,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -76,8 +75,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -122,8 +120,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -168,8 +165,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -192,7 +188,6 @@ "Backups", "NodeBalancers", "Block Storage", - "Object Storage", "GPU Linodes", "Kubernetes", "Cloud Firewall", @@ -215,8 +210,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -239,7 +233,6 @@ "Backups", "NodeBalancers", "Block Storage", - "Object Storage", "GPU Linodes", "Kubernetes", "Cloud Firewall", @@ -261,8 +254,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -304,12 +296,10 @@ "label": "Melbourne, AU", "monitors": { "alerts": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -354,8 +344,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -401,8 +390,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -447,8 +435,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -494,8 +481,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -540,8 +526,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -586,8 +571,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -633,8 +617,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -679,8 +662,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -725,8 +707,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -770,8 +751,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -816,8 +796,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -862,8 +841,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -908,8 +886,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -950,12 +927,10 @@ "label": "Washington, DC", "monitors": { "alerts": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -994,12 +969,10 @@ "label": "Sydney, AU", "monitors": { "alerts": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -1042,8 +1015,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -1086,8 +1058,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -1129,8 +1100,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -1172,8 +1142,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -1217,8 +1186,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -1258,9 +1226,7 @@ "label": "Singapore, SG", "monitors": { "alerts": [], - "metrics": [ - "NodeBalancers" - ] + "metrics": [] }, "placement_group_limits": { "maximum_linodes_per_flexible_pg": 5, @@ -1297,9 +1263,7 @@ "label": "London, UK", "monitors": { "alerts": [], - "metrics": [ - "NodeBalancers" - ] + "metrics": [] }, "placement_group_limits": { "maximum_linodes_per_flexible_pg": 5, @@ -1342,8 +1306,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -1387,8 +1350,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -1430,8 +1392,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { diff --git a/test/fixtures/vultr/_metadata.json b/test/fixtures/vultr/_metadata.json index 46fa29ce9..96a70de8c 100644 --- a/test/fixtures/vultr/_metadata.json +++ b/test/fixtures/vultr/_metadata.json @@ -1,8 +1,11 @@ { "cloud": "vultr", - "recorded_at": "2026-02-13T06:37:13Z", + "recorded_at": "2026-02-10T21:55:33Z", "fixtures": { - "plans": {"endpoint": "/plans", "recorded_at": "2026-02-13T06:37:12Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T06:37:12Z"} + "account": {"endpoint": "/account", "recorded_at": "2026-02-10T21:55:30Z"}, + "ssh_keys": {"endpoint": "/ssh-keys", "recorded_at": "2026-02-10T21:55:31Z"}, + "instances": {"endpoint": "/instances", "recorded_at": "2026-02-10T21:55:31Z"}, + "plans": {"endpoint": "/plans", "recorded_at": "2026-02-10T21:55:33Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-10T21:55:33Z"} } } diff --git a/test/fixtures/vultr/plans.json b/test/fixtures/vultr/plans.json index 7794b0820..d4382630c 100644 --- a/test/fixtures/vultr/plans.json +++ b/test/fixtures/vultr/plans.json @@ -1,38 +1,12 @@ { "meta": { "links": { - "next": "bmV4dF9fdmNnLWE0MC02Yy0zMGctMTJ2cmFt", + "next": "bmV4dF9fdmNnLWExMDAtM2MtMzBnLTIwdnJhbQ==", "prev": "" }, - "total": 160 + "total": 159 }, "plans": [ - { - "bandwidth": 0, - "cpu_vendor": "Intel", - "deploy_ondemand": true, - "deploy_preemptible": false, - "disk": 10, - "disk_count": 1, - "disk_type": "SSD", - "hourly_cost": 0, - "hourly_cost_preemptible": 0, - "id": "vc2-1c-0.5gb-free", - "invoice_type": "monthly", - "location_cost": {}, - "locations": [ - "sea", - "fra", - "mia" - ], - "monthly_cost": 0, - "monthly_cost_preemptible": 0, - "ram": 512, - "storage_type": "local_storage", - "type": "vc2", - "vcpu_count": 1, - "vcpu_type": "thread" - }, { "bandwidth": 512, "cpu_vendor": "Intel", @@ -1068,6 +1042,7 @@ "lax", "atl", "ams", + "fra", "sjc", "yto", "cdg", @@ -1111,7 +1086,6 @@ "invoice_type": "monthly", "location_cost": {}, "locations": [ - "ewr", "dfw", "sea", "lax", @@ -1162,11 +1136,11 @@ "ams", "sjc", "yto", + "cdg", "nrt", "waw", "mad", "icn", - "mia", "sgp", "sto", "mex", @@ -3179,7 +3153,6 @@ "dfw", "sea", "lax", - "atl", "ams", "lhr", "fra", @@ -3295,7 +3268,6 @@ "dfw", "sea", "lax", - "atl", "ams", "lhr", "fra", @@ -3594,7 +3566,6 @@ "locations": [ "ewr", "ord", - "sea", "lax", "ams", "lhr", @@ -3648,7 +3619,6 @@ "dfw", "sea", "lax", - "atl", "ams", "lhr", "fra", @@ -4037,7 +4007,6 @@ "dfw", "sea", "lax", - "atl", "ams", "lhr", "fra", @@ -4154,7 +4123,6 @@ "ams", "lhr", "sjc", - "syd", "cdg", "nrt", "waw", @@ -4463,7 +4431,6 @@ "dfw", "sea", "lax", - "atl", "ams", "lhr", "fra", @@ -4679,7 +4646,6 @@ "dfw", "sea", "lax", - "atl", "ams", "lhr", "fra", @@ -4733,7 +4699,6 @@ "dfw", "sea", "lax", - "atl", "ams", "lhr", "fra", @@ -4837,7 +4802,6 @@ "dfw", "sea", "lax", - "atl", "ams", "lhr", "fra", @@ -5307,7 +5271,6 @@ "locations": [ "ewr", "lhr", - "syd", "nrt", "blr" ], @@ -5371,7 +5334,9 @@ "locations": [ "lhr", "fra", - "syd" + "syd", + "nrt", + "sgp" ], "monthly_cost": 210, "monthly_cost_preemptible": 210, @@ -5397,7 +5362,7 @@ "invoice_type": "hourly", "location_cost": {}, "locations": [ - "syd", + "ewr", "nrt" ], "monthly_cost": 315, @@ -5407,6 +5372,35 @@ "type": "vcg", "vcpu_count": 6, "vcpu_type": "thread" + }, + { + "bandwidth": 3072, + "cpu_vendor": "Intel", + "deploy_ondemand": true, + "deploy_preemptible": false, + "disk": 700, + "disk_count": 1, + "disk_type": "CLOUDGPU", + "gpu_type": "NVIDIA_A100", + "gpu_vram_gb": 20, + "hourly_cost": 0.616, + "hourly_cost_preemptible": 0.616, + "id": "vcg-a100-3c-30g-20vram", + "invoice_type": "hourly", + "location_cost": {}, + "locations": [ + "ewr", + "fra", + "sjc", + "nrt" + ], + "monthly_cost": 450, + "monthly_cost_preemptible": 450, + "ram": 30720, + "storage_type": "local_storage", + "type": "vcg", + "vcpu_count": 3, + "vcpu_type": "thread" } ] } diff --git a/test/record.sh b/test/record.sh index 4fcda7063..9f52bc90f 100644 --- a/test/record.sh +++ b/test/record.sh @@ -860,68 +860,10 @@ printf '\n' mkdir -p "$FIXTURES_DIR" -# Count clouds to decide sequential vs parallel -CLOUD_COUNT=0 -for _c in $CLOUDS_TO_RECORD; do - CLOUD_COUNT=$((CLOUD_COUNT + 1)) +for cloud in $CLOUDS_TO_RECORD; do + record_cloud "$cloud" done -# Parallel recording: each cloud runs in its own subshell when recording 2+ clouds -# Each cloud writes to its own fixture dir so there are no conflicts. -if [[ "$CLOUD_COUNT" -gt 1 ]] && [[ "$PROMPT_FOR_CREDS" != "true" ]]; then - printf '%b\n' "${CYAN}Recording ${CLOUD_COUNT} clouds in parallel...${NC}" - printf '\n' - - PARALLEL_DIR=$(mktemp -d /tmp/spawn-record-parallel-XXXXXX) - PIDS="" - - for cloud in $CLOUDS_TO_RECORD; do - ( - # Each subshell writes its own result file - RESULT_FILE="${PARALLEL_DIR}/${cloud}.result" - OUTPUT_FILE="${PARALLEL_DIR}/${cloud}.log" - - # Capture all output - record_cloud "$cloud" > "$OUTPUT_FILE" 2>&1 - - # Write counters to result file for aggregation - printf '%d %d %d\n' "$RECORDED" "$SKIPPED" "$ERRORS" > "$RESULT_FILE" - ) & - PIDS="${PIDS} $!" - done - - # Wait for all parallel recordings - for pid in $PIDS; do - wait "$pid" 2>/dev/null || true - done - - # Aggregate results and print output - for cloud in $CLOUDS_TO_RECORD; do - OUTPUT_FILE="${PARALLEL_DIR}/${cloud}.log" - RESULT_FILE="${PARALLEL_DIR}/${cloud}.result" - - # Print the captured output - if [[ -f "$OUTPUT_FILE" ]]; then - cat "$OUTPUT_FILE" - fi - - # Aggregate counters - if [[ -f "$RESULT_FILE" ]]; then - read -r r s e < "$RESULT_FILE" - RECORDED=$((RECORDED + r)) - SKIPPED=$((SKIPPED + s)) - ERRORS=$((ERRORS + e)) - fi - done - - rm -rf "$PARALLEL_DIR" -else - # Sequential recording: single cloud or interactive credential prompting - for cloud in $CLOUDS_TO_RECORD; do - record_cloud "$cloud" - done -fi - # --- Summary --- printf '%b\n' "${CYAN}===============================${NC}" TOTAL=$((RECORDED + SKIPPED + ERRORS)) diff --git a/vastai/claude.sh b/vastai/claude.sh new file mode 100644 index 000000000..c5ac3fbdc --- /dev/null +++ b/vastai/claude.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# shellcheck disable=SC2154 +set -eo pipefail + +# Source common functions - try local file first, fall back to remote +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)" +# shellcheck source=vastai/lib/common.sh +if [[ -f "${SCRIPT_DIR}/lib/common.sh" ]]; then + source "${SCRIPT_DIR}/lib/common.sh" +else + eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/vastai/lib/common.sh)" +fi + +log_info "Claude Code on Vast.ai" +echo "" + +# 1. Ensure vastai CLI and API key are configured +ensure_vastai_cli +ensure_vastai_token + +# 2. Get instance name and create instance +SERVER_NAME=$(get_server_name) +create_server "${SERVER_NAME}" + +# 3. Wait for SSH connectivity and install base tools +verify_server_connectivity +install_base_tools + +# 4. Verify Claude Code is installed (fallback to manual install) +log_warn "Verifying Claude Code installation..." +if ! run_server "${VASTAI_INSTANCE_ID}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then + log_warn "Claude Code not found, installing manually..." + run_server "${VASTAI_INSTANCE_ID}" "curl -fsSL https://claude.ai/install.sh | bash" +fi +log_info "Claude Code is installed" + +# 5. Get OpenRouter API key +echo "" +if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then + log_info "Using OpenRouter API key from environment" +else + OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) +fi + +# 6. Inject environment variables +log_warn "Setting up environment variables..." +inject_env_vars_ssh "${VASTAI_INSTANCE_ID}" upload_file run_server \ + "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ + "ANTHROPIC_AUTH_TOKEN=${OPENROUTER_API_KEY}" \ + "ANTHROPIC_API_KEY=" \ + "CLAUDE_CODE_SKIP_ONBOARDING=1" \ + "CLAUDE_CODE_ENABLE_TELEMETRY=0" + +# 7. Configure Claude Code settings +setup_claude_code_config "${OPENROUTER_API_KEY}" \ + "upload_file ${VASTAI_INSTANCE_ID}" \ + "run_server ${VASTAI_INSTANCE_ID}" + +echo "" +log_info "Vast.ai instance setup completed successfully!" +log_info "Instance: ${SERVER_NAME} (ID: ${VASTAI_INSTANCE_ID})" +echo "" + +# 8. Start Claude Code interactively +log_warn "Starting Claude Code..." +sleep 1 +clear +interactive_session "${VASTAI_INSTANCE_ID}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" From a310ad2a08cf4fa523010ed67fa12bb0df6c3a0c Mon Sep 17 00:00:00 2001 From: Ahmed Abushagur Date: Thu, 12 Feb 2026 23:58:11 -0800 Subject: [PATCH 06/13] fix: local settings to git ignore --- .claude/settings.local.json | 22 ---------------------- .gitignore | 9 +++++++++ 2 files changed, 9 insertions(+), 22 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index d5f3891e3..000000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(done)", - "Bash(grep:*)", - "Bash(for cloud in sprite hetzner digitalocean vultr linode)", - "Bash(do)", - "Bash(echo:*)", - "Bash(if ! grep -q \"setup_shell_environment\\\\|inject_env_vars\" \"/Users/ahmed/Documents/GitHub/spawn/$cloud/lib/common.sh\")", - "Bash(then)", - "Bash(fi)", - "Bash(if ! grep -q \"setup_claude_code_config\\\\|setup_openclaw_config\" \"/Users/ahmed/Documents/GitHub/spawn/$cloud/lib/common.sh\")", - "Bash(if ! grep -q \"run_server\\\\|upload_file\\\\|interactive_session\" \"/Users/ahmed/Documents/GitHub/spawn/$cloud/lib/common.sh\")", - "Bash( ls -d /Users/ahmed/Documents/GitHub/spawn/*/)", - "Bash(xargs:*)", - "Read(//private/tmp/**)", - "Bash(wc:*)", - "Bash(ls:*)", - "Bash(bash:*)" - ] - } -} diff --git a/.gitignore b/.gitignore index 678dbe0ad..5f1f87994 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules/ .docs/ __pycache__/ .claude/skills/*/start-*.sh +.claude/settings.local.json cli/cli.js # Sensitive files — never commit secrets or private keys @@ -15,3 +16,11 @@ id_rsa id_ed25519 credentials.json service-account.json +.claude/settings.json +.claude/settings.json +.claude/settings.json +.claude/settings.local.json +.DS_Store +.claude/settings.local.json +.DS_Store +.claude/settings.local.json From 915b681dad2210fb91aaff15faf428b2fa9cb2c0 Mon Sep 17 00:00:00 2001 From: Ahmed Abushagur Date: Tue, 10 Feb 2026 14:32:13 -0800 Subject: [PATCH 07/13] feat: testing --- .claude/settings.local.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 000000000..d5f3891e3 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,22 @@ +{ + "permissions": { + "allow": [ + "Bash(done)", + "Bash(grep:*)", + "Bash(for cloud in sprite hetzner digitalocean vultr linode)", + "Bash(do)", + "Bash(echo:*)", + "Bash(if ! grep -q \"setup_shell_environment\\\\|inject_env_vars\" \"/Users/ahmed/Documents/GitHub/spawn/$cloud/lib/common.sh\")", + "Bash(then)", + "Bash(fi)", + "Bash(if ! grep -q \"setup_claude_code_config\\\\|setup_openclaw_config\" \"/Users/ahmed/Documents/GitHub/spawn/$cloud/lib/common.sh\")", + "Bash(if ! grep -q \"run_server\\\\|upload_file\\\\|interactive_session\" \"/Users/ahmed/Documents/GitHub/spawn/$cloud/lib/common.sh\")", + "Bash( ls -d /Users/ahmed/Documents/GitHub/spawn/*/)", + "Bash(xargs:*)", + "Read(//private/tmp/**)", + "Bash(wc:*)", + "Bash(ls:*)", + "Bash(bash:*)" + ] + } +} From 613764c9c3f022df1c4807dd88b4c1a998b42060 Mon Sep 17 00:00:00 2001 From: Ahmed Abushagur Date: Thu, 12 Feb 2026 21:35:30 -0800 Subject: [PATCH 08/13] feat: qa on refactor/discovery bot --- test/fixtures/digitalocean/_metadata.json | 14 +- test/fixtures/digitalocean/regions.json | 108 ++------- test/fixtures/hetzner/_metadata.json | 14 +- test/fixtures/hetzner/create_server.json | 32 +-- test/fixtures/hetzner/delete_server.json | 6 +- test/fixtures/hetzner/servers.json | 283 +--------------------- test/fixtures/linode/_metadata.json | 14 +- test/fixtures/linode/regions.json | 109 ++++++--- test/fixtures/vultr/_metadata.json | 9 +- test/fixtures/vultr/plans.json | 81 ++++--- test/record.sh | 62 ++++- 11 files changed, 250 insertions(+), 482 deletions(-) diff --git a/test/fixtures/digitalocean/_metadata.json b/test/fixtures/digitalocean/_metadata.json index 9b546f315..af1429efa 100644 --- a/test/fixtures/digitalocean/_metadata.json +++ b/test/fixtures/digitalocean/_metadata.json @@ -1,11 +1,13 @@ { "cloud": "digitalocean", - "recorded_at": "2026-02-10T21:55:29Z", + "recorded_at": "2026-02-13T05:00:37Z", "fixtures": { - "account": {"endpoint": "/account", "recorded_at": "2026-02-10T21:55:27Z"}, - "ssh_keys": {"endpoint": "/account/keys", "recorded_at": "2026-02-10T21:55:27Z"}, - "droplets": {"endpoint": "/droplets", "recorded_at": "2026-02-10T21:55:28Z"}, - "sizes": {"endpoint": "/sizes", "recorded_at": "2026-02-10T21:55:29Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-10T21:55:29Z"} + "account": {"endpoint": "/account", "recorded_at": "2026-02-13T05:00:28Z"}, + "ssh_keys": {"endpoint": "/account/keys", "recorded_at": "2026-02-13T05:00:29Z"}, + "droplets": {"endpoint": "/droplets", "recorded_at": "2026-02-13T05:00:29Z"}, + "sizes": {"endpoint": "/sizes", "recorded_at": "2026-02-13T05:00:29Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T05:00:30Z"}, + "create_server": {"endpoint": "POST /droplets", "type": "live", "recorded_at": "2026-02-13T05:00:33Z"}, + "delete_server": {"endpoint": "DELETE /droplets/{id}", "type": "live", "recorded_at": "2026-02-13T05:00:37Z"} } } diff --git a/test/fixtures/digitalocean/regions.json b/test/fixtures/digitalocean/regions.json index 2ec9f844f..f6566eea2 100644 --- a/test/fixtures/digitalocean/regions.json +++ b/test/fixtures/digitalocean/regions.json @@ -282,32 +282,42 @@ "c5-8vcpu-16gb-intel", "so1_5-4vcpu-32gb", "m-8vcpu-64gb", + "c-16", "g6_5-8vcpu-32gb", "g5_5-8vcpu-32gb-intel", + "c2-16vcpu-32gb", "m-8vcpu-64gb-intel", "m3-8vcpu-64gb", "c-16-intel", "m3-8vcpu-64gb-intel", "c2-16vcpu-32gb-intel", + "c5-16vcpu-32gb", + "g-16vcpu-64gb", "so-8vcpu-64gb-intel", "so-8vcpu-64gb", "m6-8vcpu-64gb", + "gd-16vcpu-64gb", "so1_5-8vcpu-64gb-intel", "g-16vcpu-64gb-intel", "gd-16vcpu-64gb-intel", "c5-16vcpu-32gb-intel", "so1_5-8vcpu-64gb", "m-16vcpu-128gb", + "c-32", + "g6_5-16vcpu-64gb", "g5_5-16vcpu-64gb-intel", + "c2-32vcpu-64gb", "m-16vcpu-128gb-intel", "m3-16vcpu-128gb", "c-32-intel", "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", "m-24vcpu-192gb", + "g-32vcpu-128gb", "so-16vcpu-128gb-intel", "so-16vcpu-128gb", "m6-16vcpu-128gb", + "gd-32vcpu-128gb", "so1_5-16vcpu-128gb-intel", "m-24vcpu-192gb-intel", "g-32vcpu-128gb-intel", @@ -315,18 +325,26 @@ "gd-32vcpu-128gb-intel", "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", + "c-48-intel", "m3-24vcpu-192gb-intel", + "m-32vcpu-256gb", + "g6_5-32vcpu-128gb", + "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", "so-24vcpu-192gb-intel", "so-24vcpu-192gb", "m6-24vcpu-192gb", "m-32vcpu-256gb-intel", + "m3-32vcpu-256gb", "so1_5-24vcpu-192gb-intel", "m3-32vcpu-256gb-intel", "g-48vcpu-192gb-intel", "gd-48vcpu-192gb-intel", + "c5-48vcpu-96gb-intel", "so1_5-24vcpu-192gb", "so-32vcpu-256gb-intel", + "so-32vcpu-256gb", + "m6-32vcpu-256gb", "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", "gpu-h100x1-80gb", @@ -389,7 +407,6 @@ "gd-2vcpu-8gb", "g-2vcpu-8gb-intel", "gd-2vcpu-8gb-intel", - "s-4vcpu-16gb-amd", "m-2vcpu-16gb", "c-4", "g6_5-2vcpu-8gb", @@ -401,7 +418,6 @@ "m3-2vcpu-16gb", "c-4-intel", "m3-2vcpu-16gb-intel", - "s-8vcpu-16gb-amd", "s-8vcpu-16gb-intel", "c2-4vcpu-8gb-intel", "c5-4vcpu-8gb", @@ -416,7 +432,6 @@ "gd-4vcpu-16gb-intel", "c5-4vcpu-8gb-intel", "so1_5-2vcpu-16gb", - "s-8vcpu-32gb-amd", "m-4vcpu-32gb", "c-8", "g6_5-4vcpu-16gb", @@ -471,6 +486,7 @@ "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", "c5-32vcpu-64gb", + "c-48", "m-24vcpu-192gb", "g-32vcpu-128gb", "so-16vcpu-128gb-intel", @@ -943,7 +959,6 @@ "gd-4vcpu-16gb-intel", "c5-4vcpu-8gb-intel", "so1_5-2vcpu-16gb", - "s-8vcpu-32gb-amd", "m-4vcpu-32gb", "c-8", "g6_5-4vcpu-16gb", @@ -1011,36 +1026,18 @@ "m3-24vcpu-192gb", "g-40vcpu-160gb", "gd-32vcpu-128gb-intel", - "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", - "c-48-intel", "m3-24vcpu-192gb-intel", "gd-40vcpu-160gb", "g6_5-32vcpu-128gb", - "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", "so-24vcpu-192gb-intel", - "so-24vcpu-192gb", - "m6-24vcpu-192gb", "m-32vcpu-256gb-intel", - "c-60-intel", "so1_5-24vcpu-192gb-intel", "m3-32vcpu-256gb-intel", - "g-48vcpu-192gb-intel", - "c2-60vcpu-120gb-intel", - "gd-48vcpu-192gb-intel", - "c5-48vcpu-96gb-intel", "so-32vcpu-256gb-intel", - "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", - "g-60vcpu-240gb-intel", - "m-48vcpu-384gb-intel", - "gd-60vcpu-240gb-intel", - "c5-60vcpu-120gb-intel", - "gpu-h100x1-80gb", - "m3-48vcpu-384gb-intel", - "g5_5-60vcpu-240gb-intel", - "so-48vcpu-384gb-intel" + "gpu-h100x1-80gb" ], "slug": "ams3" }, @@ -1158,13 +1155,16 @@ "c5-16vcpu-32gb-intel", "so1_5-8vcpu-64gb", "m-16vcpu-128gb", + "c-32", "g6_5-16vcpu-64gb", "g5_5-16vcpu-64gb-intel", + "c2-32vcpu-64gb", "m-16vcpu-128gb-intel", "m3-16vcpu-128gb", "c-32-intel", "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", + "c5-32vcpu-64gb", "m-24vcpu-192gb", "g-32vcpu-128gb", "so-16vcpu-128gb-intel", @@ -1335,26 +1335,19 @@ "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", "m-24vcpu-192gb", - "g-32vcpu-128gb", "so-16vcpu-128gb-intel", "so-16vcpu-128gb", "m6-16vcpu-128gb", - "gd-32vcpu-128gb", "so1_5-16vcpu-128gb-intel", - "gpu-l40sx1-48gb", "gpu-6000adax1-48gb", "m-24vcpu-192gb-intel", "g-32vcpu-128gb-intel", "m3-24vcpu-192gb", - "g-40vcpu-160gb", "gd-32vcpu-128gb-intel", "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", "c-48-intel", "m3-24vcpu-192gb-intel", - "m-32vcpu-256gb", - "gd-40vcpu-160gb", - "g6_5-32vcpu-128gb", "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", "so-24vcpu-192gb-intel", @@ -1362,18 +1355,14 @@ "m6-24vcpu-192gb", "m-32vcpu-256gb-intel", "c-60-intel", - "m3-32vcpu-256gb", "so1_5-24vcpu-192gb-intel", "m3-32vcpu-256gb-intel", - "g6_5-40vcpu-160gb", "g-48vcpu-192gb-intel", "c2-60vcpu-120gb-intel", "gd-48vcpu-192gb-intel", "c5-48vcpu-96gb-intel", "so1_5-24vcpu-192gb", "so-32vcpu-256gb-intel", - "so-32vcpu-256gb", - "m6-32vcpu-256gb", "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", "g-60vcpu-240gb-intel", @@ -1381,7 +1370,6 @@ "gd-60vcpu-240gb-intel", "c5-60vcpu-120gb-intel", "gpu-h100x1-80gb", - "so1_5-32vcpu-256gb", "m3-48vcpu-384gb-intel", "g5_5-60vcpu-240gb-intel", "so-48vcpu-384gb-intel" @@ -1463,78 +1451,40 @@ "so1_5-4vcpu-32gb-intel", "g-8vcpu-32gb-intel", "gd-8vcpu-32gb-intel", - "c5-8vcpu-16gb-intel", "so1_5-4vcpu-32gb", "m-8vcpu-64gb", "c-16", "g6_5-8vcpu-32gb", - "g5_5-8vcpu-32gb-intel", "c2-16vcpu-32gb", "m-8vcpu-64gb-intel", "m3-8vcpu-64gb", "c-16-intel", - "m3-8vcpu-64gb-intel", "c2-16vcpu-32gb-intel", "c5-16vcpu-32gb", "g-16vcpu-64gb", - "so-8vcpu-64gb-intel", "so-8vcpu-64gb", "m6-8vcpu-64gb", "gd-16vcpu-64gb", - "so1_5-8vcpu-64gb-intel", "g-16vcpu-64gb-intel", "gd-16vcpu-64gb-intel", - "c5-16vcpu-32gb-intel", "so1_5-8vcpu-64gb", "m-16vcpu-128gb", "c-32", "g6_5-16vcpu-64gb", - "g5_5-16vcpu-64gb-intel", "c2-32vcpu-64gb", "m-16vcpu-128gb-intel", "m3-16vcpu-128gb", - "c-32-intel", - "m3-16vcpu-128gb-intel", - "c2-32vcpu-64gb-intel", "c-48", "m-24vcpu-192gb", "g-32vcpu-128gb", - "so-16vcpu-128gb-intel", "so-16vcpu-128gb", "m6-16vcpu-128gb", "gd-32vcpu-128gb", - "so1_5-16vcpu-128gb-intel", "c2-48vcpu-96gb", - "m-24vcpu-192gb-intel", "g-32vcpu-128gb-intel", "g-40vcpu-160gb", - "gd-32vcpu-128gb-intel", - "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", - "c-48-intel", - "m3-24vcpu-192gb-intel", - "gd-40vcpu-160gb", - "c2-48vcpu-96gb-intel", - "g5_5-32vcpu-128gb-intel", - "so-24vcpu-192gb-intel", - "m-32vcpu-256gb-intel", - "c-60-intel", - "so1_5-24vcpu-192gb-intel", - "m3-32vcpu-256gb-intel", - "g-48vcpu-192gb-intel", - "c2-60vcpu-120gb-intel", - "gd-48vcpu-192gb-intel", - "c5-48vcpu-96gb-intel", - "so-32vcpu-256gb-intel", - "g5_5-48vcpu-192gb-intel", - "so1_5-32vcpu-256gb-intel", - "g-60vcpu-240gb-intel", - "m-48vcpu-384gb-intel", - "gd-60vcpu-240gb-intel", - "c5-60vcpu-120gb-intel", - "m3-48vcpu-384gb-intel", - "g5_5-60vcpu-240gb-intel", - "so-48vcpu-384gb-intel" + "gd-40vcpu-160gb" ], "slug": "sfo2" }, @@ -1853,40 +1803,30 @@ "gd-32vcpu-128gb-intel", "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", - "c-48-intel", "m3-24vcpu-192gb-intel", "m-32vcpu-256gb", "gd-40vcpu-160gb", "g6_5-32vcpu-128gb", - "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", - "c5-48vcpu-96gb", "so-24vcpu-192gb-intel", "so-24vcpu-192gb", "m6-24vcpu-192gb", "m-32vcpu-256gb-intel", - "c-60-intel", "m3-32vcpu-256gb", "so1_5-24vcpu-192gb-intel", "m3-32vcpu-256gb-intel", "g6_5-40vcpu-160gb", "g-48vcpu-192gb-intel", - "c2-60vcpu-120gb-intel", "gd-48vcpu-192gb-intel", - "c5-48vcpu-96gb-intel", "so1_5-24vcpu-192gb", "so-32vcpu-256gb-intel", "so-32vcpu-256gb", "m6-32vcpu-256gb", "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", - "g-60vcpu-240gb-intel", "m-48vcpu-384gb-intel", - "gd-60vcpu-240gb-intel", - "c5-60vcpu-120gb-intel", "so1_5-32vcpu-256gb", "m3-48vcpu-384gb-intel", - "g5_5-60vcpu-240gb-intel", "so-48vcpu-384gb-intel" ], "slug": "sfo3" diff --git a/test/fixtures/hetzner/_metadata.json b/test/fixtures/hetzner/_metadata.json index c5883a23a..87157b730 100644 --- a/test/fixtures/hetzner/_metadata.json +++ b/test/fixtures/hetzner/_metadata.json @@ -1,12 +1,12 @@ { "cloud": "hetzner", - "recorded_at": "2026-02-10T21:55:26Z", + "recorded_at": "2026-02-13T05:00:36Z", "fixtures": { - "server_types": {"endpoint": "/server_types?per_page=50", "recorded_at": "2026-02-10T21:55:19Z"}, - "locations": {"endpoint": "/locations", "recorded_at": "2026-02-10T21:55:20Z"}, - "ssh_keys": {"endpoint": "/ssh_keys", "recorded_at": "2026-02-10T21:55:21Z"}, - "servers": {"endpoint": "/servers", "recorded_at": "2026-02-10T21:55:22Z"}, - "create_server": {"endpoint": "POST /servers", "type": "live", "recorded_at": "2026-02-10T21:55:24Z"}, - "delete_server": {"endpoint": "DELETE /servers/{id}", "type": "live", "recorded_at": "2026-02-10T21:55:26Z"} + "server_types": {"endpoint": "/server_types?per_page=50", "recorded_at": "2026-02-13T05:00:29Z"}, + "locations": {"endpoint": "/locations", "recorded_at": "2026-02-13T05:00:29Z"}, + "ssh_keys": {"endpoint": "/ssh_keys", "recorded_at": "2026-02-13T05:00:30Z"}, + "servers": {"endpoint": "/servers", "recorded_at": "2026-02-13T05:00:31Z"}, + "create_server": {"endpoint": "POST /servers", "type": "live", "recorded_at": "2026-02-13T05:00:33Z"}, + "delete_server": {"endpoint": "DELETE /servers/{id}", "type": "live", "recorded_at": "2026-02-13T05:00:36Z"} } } diff --git a/test/fixtures/hetzner/create_server.json b/test/fixtures/hetzner/create_server.json index 74637d827..d0cfc9fce 100644 --- a/test/fixtures/hetzner/create_server.json +++ b/test/fixtures/hetzner/create_server.json @@ -3,11 +3,11 @@ "command": "create_server", "error": null, "finished": null, - "id": 610821665885201, + "id": 611302701715360, "progress": 0, "resources": [ { - "id": 120682780, + "id": 120889518, "type": "server" }, { @@ -15,7 +15,7 @@ "type": "image" } ], - "started": "2026-02-10T21:55:23Z", + "started": "2026-02-13T05:00:33Z", "status": "running" }, "next_actions": [ @@ -23,23 +23,23 @@ "command": "start_server", "error": null, "finished": null, - "id": 610821665885202, - "parent_id": 610821665885201, + "id": 611302701715361, + "parent_id": 611302701715360, "progress": 0, "resources": [ { - "id": 120682780, + "id": 120889518, "type": "server" } ], - "started": "2026-02-10T21:55:23Z", + "started": "2026-02-13T05:00:33Z", "status": "running" } ], "root_password": null, "server": { "backup_window": null, - "created": "2026-02-10T21:55:23Z", + "created": "2026-02-13T05:00:33Z", "datacenter": { "description": "Nuremberg 1 virtual DC 3", "id": 2, @@ -65,6 +65,7 @@ 45, 93, 94, + 95, 96, 97, 98, @@ -91,6 +92,7 @@ 45, 93, 94, + 95, 96, 97, 98, @@ -166,7 +168,7 @@ ] } }, - "id": 120682780, + "id": 120889518, "image": { "architecture": "x86", "bound_to": null, @@ -205,7 +207,7 @@ "network_zone": "eu-central" }, "locked": false, - "name": "spawn-record-1770760522", + "name": "spawn-record-1770958831", "outgoing_traffic": 0, "placement_group": null, "primary_disk_size": 40, @@ -219,15 +221,15 @@ "floating_ips": [], "ipv4": { "blocked": false, - "dns_ptr": "static.197.210.245.188.clients.your-server.de", - "id": 117603379, - "ip": "188.245.210.197" + "dns_ptr": "static.81.176.245.188.clients.your-server.de", + "id": 117975188, + "ip": "188.245.176.81" }, "ipv6": { "blocked": false, "dns_ptr": [], - "id": 117603380, - "ip": "2a01:4f8:1c1e:bc55::/64" + "id": 117975189, + "ip": "2a01:4f8:1c1a:f748::/64" } }, "rescue_enabled": false, diff --git a/test/fixtures/hetzner/delete_server.json b/test/fixtures/hetzner/delete_server.json index bbf825949..ee03f36a6 100644 --- a/test/fixtures/hetzner/delete_server.json +++ b/test/fixtures/hetzner/delete_server.json @@ -3,15 +3,15 @@ "command": "delete_server", "error": null, "finished": null, - "id": 610821665885215, + "id": 611302701715371, "progress": 0, "resources": [ { - "id": 120682780, + "id": 120889518, "type": "server" } ], - "started": "2026-02-10T21:55:26Z", + "started": "2026-02-13T05:00:36Z", "status": "running" } } diff --git a/test/fixtures/hetzner/servers.json b/test/fixtures/hetzner/servers.json index dd6315039..8ff3b8b96 100644 --- a/test/fixtures/hetzner/servers.json +++ b/test/fixtures/hetzner/servers.json @@ -6,287 +6,8 @@ "page": 1, "per_page": 25, "previous_page": null, - "total_entries": 1 + "total_entries": 0 } }, - "servers": [ - { - "backup_window": null, - "created": "2026-02-10T21:32:27Z", - "datacenter": { - "description": "Nuremberg 1 virtual DC 3", - "id": 2, - "location": { - "city": "Nuremberg", - "country": "DE", - "description": "Nuremberg DC Park 1", - "id": 2, - "latitude": 49.452102, - "longitude": 11.076665, - "name": "nbg1", - "network_zone": "eu-central" - }, - "name": "nbg1-dc3", - "server_types": { - "available": [ - 27, - 28, - 29, - 30, - 31, - 32, - 45, - 93, - 94, - 96, - 97, - 98, - 99, - 100, - 101, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ], - "available_for_migration": [ - 27, - 28, - 29, - 30, - 31, - 32, - 45, - 93, - 94, - 96, - 97, - 98, - 99, - 100, - 101, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ], - "supported": [ - 1, - 3, - 5, - 7, - 9, - 11, - 12, - 13, - 14, - 15, - 22, - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 33, - 34, - 35, - 36, - 37, - 38, - 39, - 40, - 41, - 42, - 43, - 44, - 45, - 93, - 94, - 95, - 96, - 97, - 98, - 99, - 100, - 101, - 104, - 105, - 106, - 107, - 109, - 110, - 111, - 112, - 113, - 114, - 115, - 116, - 117 - ] - } - }, - "id": 120681182, - "image": { - "architecture": "x86", - "bound_to": null, - "created": "2024-04-25T13:26:27Z", - "created_from": null, - "deleted": null, - "deprecated": null, - "description": "Ubuntu 24.04", - "disk_size": 5, - "id": 161547269, - "image_size": null, - "labels": {}, - "name": "ubuntu-24.04", - "os_flavor": "ubuntu", - "os_version": "24.04", - "protection": { - "delete": false - }, - "rapid_deploy": true, - "status": "available", - "type": "system" - }, - "included_traffic": 21990232555520, - "ingoing_traffic": 0, - "iso": null, - "labels": {}, - "load_balancers": [], - "location": { - "city": "Nuremberg", - "country": "DE", - "description": "Nuremberg DC Park 1", - "id": 2, - "latitude": 49.452102, - "longitude": 11.076665, - "name": "nbg1", - "network_zone": "eu-central" - }, - "locked": false, - "name": "claude", - "outgoing_traffic": 0, - "placement_group": null, - "primary_disk_size": 40, - "private_net": [], - "protection": { - "delete": false, - "rebuild": false - }, - "public_net": { - "firewalls": [], - "floating_ips": [], - "ipv4": { - "blocked": false, - "dns_ptr": "static.157.65.225.46.clients.your-server.de", - "id": 117600167, - "ip": "46.225.65.157" - }, - "ipv6": { - "blocked": false, - "dns_ptr": [], - "id": 117600168, - "ip": "2a01:4f8:1c19:e41b::/64" - } - }, - "rescue_enabled": false, - "server_type": { - "architecture": "x86", - "category": "cost_optimized", - "cores": 2, - "cpu_type": "shared", - "deprecated": false, - "deprecation": null, - "description": "CX 23", - "disk": 40, - "id": 114, - "locations": [ - { - "deprecation": null, - "id": 1, - "name": "fsn1" - }, - { - "deprecation": null, - "id": 2, - "name": "nbg1" - }, - { - "deprecation": null, - "id": 3, - "name": "hel1" - } - ], - "memory": 4, - "name": "cx23", - "prices": [ - { - "included_traffic": 21990232555520, - "location": "fsn1", - "price_hourly": { - "gross": "0.0056000000000000", - "net": "0.0056000000" - }, - "price_monthly": { - "gross": "3.4900000000000000", - "net": "3.4900000000" - }, - "price_per_tb_traffic": { - "gross": "1.2000000000000000", - "net": "1.2000000000" - } - }, - { - "included_traffic": 21990232555520, - "location": "hel1", - "price_hourly": { - "gross": "0.0056000000000000", - "net": "0.0056000000" - }, - "price_monthly": { - "gross": "3.4900000000000000", - "net": "3.4900000000" - }, - "price_per_tb_traffic": { - "gross": "1.2000000000000000", - "net": "1.2000000000" - } - }, - { - "included_traffic": 21990232555520, - "location": "nbg1", - "price_hourly": { - "gross": "0.0056000000000000", - "net": "0.0056000000" - }, - "price_monthly": { - "gross": "3.4900000000000000", - "net": "3.4900000000" - }, - "price_per_tb_traffic": { - "gross": "1.2000000000000000", - "net": "1.2000000000" - } - } - ], - "storage_type": "local" - }, - "status": "running", - "volumes": [] - } - ] + "servers": [] } diff --git a/test/fixtures/linode/_metadata.json b/test/fixtures/linode/_metadata.json index 029fa863d..b8122a17d 100644 --- a/test/fixtures/linode/_metadata.json +++ b/test/fixtures/linode/_metadata.json @@ -1,11 +1,13 @@ { "cloud": "linode", - "recorded_at": "2026-02-10T21:55:36Z", + "recorded_at": "2026-02-13T05:00:35Z", "fixtures": { - "profile": {"endpoint": "/profile", "recorded_at": "2026-02-10T21:55:34Z"}, - "ssh_keys": {"endpoint": "/profile/sshkeys", "recorded_at": "2026-02-10T21:55:34Z"}, - "instances": {"endpoint": "/linode/instances", "recorded_at": "2026-02-10T21:55:35Z"}, - "types": {"endpoint": "/linode/types", "recorded_at": "2026-02-10T21:55:35Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-10T21:55:36Z"} + "profile": {"endpoint": "/profile", "recorded_at": "2026-02-13T05:00:28Z"}, + "ssh_keys": {"endpoint": "/profile/sshkeys", "recorded_at": "2026-02-13T05:00:28Z"}, + "instances": {"endpoint": "/linode/instances", "recorded_at": "2026-02-13T05:00:28Z"}, + "types": {"endpoint": "/linode/types", "recorded_at": "2026-02-13T05:00:29Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T05:00:29Z"}, + "create_server": {"endpoint": "POST /linode/instances", "type": "live", "recorded_at": "2026-02-13T05:00:32Z"}, + "delete_server": {"endpoint": "DELETE /linode/instances/{id}", "type": "live", "recorded_at": "2026-02-13T05:00:35Z"} } } diff --git a/test/fixtures/linode/regions.json b/test/fixtures/linode/regions.json index 201fd3543..e8228ae3f 100644 --- a/test/fixtures/linode/regions.json +++ b/test/fixtures/linode/regions.json @@ -30,7 +30,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -75,7 +76,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -120,7 +122,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -165,7 +168,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -188,6 +192,7 @@ "Backups", "NodeBalancers", "Block Storage", + "Object Storage", "GPU Linodes", "Kubernetes", "Cloud Firewall", @@ -210,7 +215,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -233,6 +239,7 @@ "Backups", "NodeBalancers", "Block Storage", + "Object Storage", "GPU Linodes", "Kubernetes", "Cloud Firewall", @@ -254,7 +261,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -296,10 +304,12 @@ "label": "Melbourne, AU", "monitors": { "alerts": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -344,7 +354,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -390,7 +401,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -435,7 +447,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -481,7 +494,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -526,7 +540,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -571,7 +586,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -617,7 +633,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -662,7 +679,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -707,7 +725,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -751,7 +770,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -796,7 +816,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -841,7 +862,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -886,7 +908,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -927,10 +950,12 @@ "label": "Washington, DC", "monitors": { "alerts": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -969,10 +994,12 @@ "label": "Sydney, AU", "monitors": { "alerts": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -1015,7 +1042,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -1058,7 +1086,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -1100,7 +1129,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -1142,7 +1172,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -1186,7 +1217,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -1226,7 +1258,9 @@ "label": "Singapore, SG", "monitors": { "alerts": [], - "metrics": [] + "metrics": [ + "NodeBalancers" + ] }, "placement_group_limits": { "maximum_linodes_per_flexible_pg": 5, @@ -1263,7 +1297,9 @@ "label": "London, UK", "monitors": { "alerts": [], - "metrics": [] + "metrics": [ + "NodeBalancers" + ] }, "placement_group_limits": { "maximum_linodes_per_flexible_pg": 5, @@ -1306,7 +1342,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -1350,7 +1387,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { @@ -1392,7 +1430,8 @@ "Managed Databases" ], "metrics": [ - "Managed Databases" + "Managed Databases", + "NodeBalancers" ] }, "placement_group_limits": { diff --git a/test/fixtures/vultr/_metadata.json b/test/fixtures/vultr/_metadata.json index 96a70de8c..6eb5cace5 100644 --- a/test/fixtures/vultr/_metadata.json +++ b/test/fixtures/vultr/_metadata.json @@ -1,11 +1,8 @@ { "cloud": "vultr", - "recorded_at": "2026-02-10T21:55:33Z", + "recorded_at": "2026-02-13T05:00:32Z", "fixtures": { - "account": {"endpoint": "/account", "recorded_at": "2026-02-10T21:55:30Z"}, - "ssh_keys": {"endpoint": "/ssh-keys", "recorded_at": "2026-02-10T21:55:31Z"}, - "instances": {"endpoint": "/instances", "recorded_at": "2026-02-10T21:55:31Z"}, - "plans": {"endpoint": "/plans", "recorded_at": "2026-02-10T21:55:33Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-10T21:55:33Z"} + "plans": {"endpoint": "/plans", "recorded_at": "2026-02-13T05:00:30Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T05:00:30Z"} } } diff --git a/test/fixtures/vultr/plans.json b/test/fixtures/vultr/plans.json index d4382630c..0e454de94 100644 --- a/test/fixtures/vultr/plans.json +++ b/test/fixtures/vultr/plans.json @@ -1,12 +1,38 @@ { "meta": { "links": { - "next": "bmV4dF9fdmNnLWExMDAtM2MtMzBnLTIwdnJhbQ==", + "next": "bmV4dF9fdmNnLWE0MC02Yy0zMGctMTJ2cmFt", "prev": "" }, - "total": 159 + "total": 160 }, "plans": [ + { + "bandwidth": 0, + "cpu_vendor": "Intel", + "deploy_ondemand": true, + "deploy_preemptible": false, + "disk": 10, + "disk_count": 1, + "disk_type": "SSD", + "hourly_cost": 0, + "hourly_cost_preemptible": 0, + "id": "vc2-1c-0.5gb-free", + "invoice_type": "monthly", + "location_cost": {}, + "locations": [ + "sea", + "fra", + "mia" + ], + "monthly_cost": 0, + "monthly_cost_preemptible": 0, + "ram": 512, + "storage_type": "local_storage", + "type": "vc2", + "vcpu_count": 1, + "vcpu_type": "thread" + }, { "bandwidth": 512, "cpu_vendor": "Intel", @@ -1042,7 +1068,7 @@ "lax", "atl", "ams", - "fra", + "lhr", "sjc", "yto", "cdg", @@ -1086,6 +1112,7 @@ "invoice_type": "monthly", "location_cost": {}, "locations": [ + "ewr", "dfw", "sea", "lax", @@ -1136,11 +1163,11 @@ "ams", "sjc", "yto", - "cdg", "nrt", "waw", "mad", "icn", + "mia", "sgp", "sto", "mex", @@ -3153,6 +3180,7 @@ "dfw", "sea", "lax", + "atl", "ams", "lhr", "fra", @@ -3268,6 +3296,7 @@ "dfw", "sea", "lax", + "atl", "ams", "lhr", "fra", @@ -3566,6 +3595,7 @@ "locations": [ "ewr", "ord", + "sea", "lax", "ams", "lhr", @@ -3619,6 +3649,7 @@ "dfw", "sea", "lax", + "atl", "ams", "lhr", "fra", @@ -4007,6 +4038,7 @@ "dfw", "sea", "lax", + "atl", "ams", "lhr", "fra", @@ -4123,6 +4155,7 @@ "ams", "lhr", "sjc", + "syd", "cdg", "nrt", "waw", @@ -4431,6 +4464,7 @@ "dfw", "sea", "lax", + "atl", "ams", "lhr", "fra", @@ -4646,6 +4680,7 @@ "dfw", "sea", "lax", + "atl", "ams", "lhr", "fra", @@ -4699,6 +4734,7 @@ "dfw", "sea", "lax", + "atl", "ams", "lhr", "fra", @@ -4802,6 +4838,7 @@ "dfw", "sea", "lax", + "atl", "ams", "lhr", "fra", @@ -5271,6 +5308,7 @@ "locations": [ "ewr", "lhr", + "syd", "nrt", "blr" ], @@ -5334,9 +5372,7 @@ "locations": [ "lhr", "fra", - "syd", - "nrt", - "sgp" + "syd" ], "monthly_cost": 210, "monthly_cost_preemptible": 210, @@ -5362,7 +5398,7 @@ "invoice_type": "hourly", "location_cost": {}, "locations": [ - "ewr", + "syd", "nrt" ], "monthly_cost": 315, @@ -5372,35 +5408,6 @@ "type": "vcg", "vcpu_count": 6, "vcpu_type": "thread" - }, - { - "bandwidth": 3072, - "cpu_vendor": "Intel", - "deploy_ondemand": true, - "deploy_preemptible": false, - "disk": 700, - "disk_count": 1, - "disk_type": "CLOUDGPU", - "gpu_type": "NVIDIA_A100", - "gpu_vram_gb": 20, - "hourly_cost": 0.616, - "hourly_cost_preemptible": 0.616, - "id": "vcg-a100-3c-30g-20vram", - "invoice_type": "hourly", - "location_cost": {}, - "locations": [ - "ewr", - "fra", - "sjc", - "nrt" - ], - "monthly_cost": 450, - "monthly_cost_preemptible": 450, - "ram": 30720, - "storage_type": "local_storage", - "type": "vcg", - "vcpu_count": 3, - "vcpu_type": "thread" } ] } diff --git a/test/record.sh b/test/record.sh index 9f52bc90f..4fcda7063 100644 --- a/test/record.sh +++ b/test/record.sh @@ -860,10 +860,68 @@ printf '\n' mkdir -p "$FIXTURES_DIR" -for cloud in $CLOUDS_TO_RECORD; do - record_cloud "$cloud" +# Count clouds to decide sequential vs parallel +CLOUD_COUNT=0 +for _c in $CLOUDS_TO_RECORD; do + CLOUD_COUNT=$((CLOUD_COUNT + 1)) done +# Parallel recording: each cloud runs in its own subshell when recording 2+ clouds +# Each cloud writes to its own fixture dir so there are no conflicts. +if [[ "$CLOUD_COUNT" -gt 1 ]] && [[ "$PROMPT_FOR_CREDS" != "true" ]]; then + printf '%b\n' "${CYAN}Recording ${CLOUD_COUNT} clouds in parallel...${NC}" + printf '\n' + + PARALLEL_DIR=$(mktemp -d /tmp/spawn-record-parallel-XXXXXX) + PIDS="" + + for cloud in $CLOUDS_TO_RECORD; do + ( + # Each subshell writes its own result file + RESULT_FILE="${PARALLEL_DIR}/${cloud}.result" + OUTPUT_FILE="${PARALLEL_DIR}/${cloud}.log" + + # Capture all output + record_cloud "$cloud" > "$OUTPUT_FILE" 2>&1 + + # Write counters to result file for aggregation + printf '%d %d %d\n' "$RECORDED" "$SKIPPED" "$ERRORS" > "$RESULT_FILE" + ) & + PIDS="${PIDS} $!" + done + + # Wait for all parallel recordings + for pid in $PIDS; do + wait "$pid" 2>/dev/null || true + done + + # Aggregate results and print output + for cloud in $CLOUDS_TO_RECORD; do + OUTPUT_FILE="${PARALLEL_DIR}/${cloud}.log" + RESULT_FILE="${PARALLEL_DIR}/${cloud}.result" + + # Print the captured output + if [[ -f "$OUTPUT_FILE" ]]; then + cat "$OUTPUT_FILE" + fi + + # Aggregate counters + if [[ -f "$RESULT_FILE" ]]; then + read -r r s e < "$RESULT_FILE" + RECORDED=$((RECORDED + r)) + SKIPPED=$((SKIPPED + s)) + ERRORS=$((ERRORS + e)) + fi + done + + rm -rf "$PARALLEL_DIR" +else + # Sequential recording: single cloud or interactive credential prompting + for cloud in $CLOUDS_TO_RECORD; do + record_cloud "$cloud" + done +fi + # --- Summary --- printf '%b\n' "${CYAN}===============================${NC}" TOTAL=$((RECORDED + SKIPPED + ERRORS)) From 0929406e24d6ff805d7c18c88e192ff909186e9f Mon Sep 17 00:00:00 2001 From: Ahmed Abushagur Date: Thu, 12 Feb 2026 22:39:47 -0800 Subject: [PATCH 09/13] feat: improved testing --- test/fixtures/digitalocean/_metadata.json | 16 +++++++-------- test/fixtures/digitalocean/regions.json | 13 ++++-------- test/fixtures/hetzner/_metadata.json | 14 ++++++------- test/fixtures/hetzner/create_server.json | 24 +++++++++++------------ test/fixtures/hetzner/delete_server.json | 6 +++--- test/fixtures/linode/_metadata.json | 16 +++++++-------- test/fixtures/vultr/_metadata.json | 6 +++--- test/fixtures/vultr/plans.json | 1 - 8 files changed, 45 insertions(+), 51 deletions(-) diff --git a/test/fixtures/digitalocean/_metadata.json b/test/fixtures/digitalocean/_metadata.json index af1429efa..3b9e27360 100644 --- a/test/fixtures/digitalocean/_metadata.json +++ b/test/fixtures/digitalocean/_metadata.json @@ -1,13 +1,13 @@ { "cloud": "digitalocean", - "recorded_at": "2026-02-13T05:00:37Z", + "recorded_at": "2026-02-13T06:37:18Z", "fixtures": { - "account": {"endpoint": "/account", "recorded_at": "2026-02-13T05:00:28Z"}, - "ssh_keys": {"endpoint": "/account/keys", "recorded_at": "2026-02-13T05:00:29Z"}, - "droplets": {"endpoint": "/droplets", "recorded_at": "2026-02-13T05:00:29Z"}, - "sizes": {"endpoint": "/sizes", "recorded_at": "2026-02-13T05:00:29Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T05:00:30Z"}, - "create_server": {"endpoint": "POST /droplets", "type": "live", "recorded_at": "2026-02-13T05:00:33Z"}, - "delete_server": {"endpoint": "DELETE /droplets/{id}", "type": "live", "recorded_at": "2026-02-13T05:00:37Z"} + "account": {"endpoint": "/account", "recorded_at": "2026-02-13T06:37:10Z"}, + "ssh_keys": {"endpoint": "/account/keys", "recorded_at": "2026-02-13T06:37:11Z"}, + "droplets": {"endpoint": "/droplets", "recorded_at": "2026-02-13T06:37:11Z"}, + "sizes": {"endpoint": "/sizes", "recorded_at": "2026-02-13T06:37:12Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T06:37:12Z"}, + "create_server": {"endpoint": "POST /droplets", "type": "live", "recorded_at": "2026-02-13T06:37:14Z"}, + "delete_server": {"endpoint": "DELETE /droplets/{id}", "type": "live", "recorded_at": "2026-02-13T06:37:18Z"} } } diff --git a/test/fixtures/digitalocean/regions.json b/test/fixtures/digitalocean/regions.json index f6566eea2..3fed997ce 100644 --- a/test/fixtures/digitalocean/regions.json +++ b/test/fixtures/digitalocean/regions.json @@ -312,6 +312,7 @@ "c-32-intel", "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", + "c5-32vcpu-64gb", "m-24vcpu-192gb", "g-32vcpu-128gb", "so-16vcpu-128gb-intel", @@ -348,7 +349,8 @@ "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", "gpu-h100x1-80gb", - "gpu-h200x1-141gb" + "gpu-h200x1-141gb", + "so1_5-32vcpu-256gb" ], "slug": "nyc2" }, @@ -932,7 +934,6 @@ "gd-2vcpu-8gb", "g-2vcpu-8gb-intel", "gd-2vcpu-8gb-intel", - "s-4vcpu-16gb-amd", "m-2vcpu-16gb", "c-4", "g6_5-2vcpu-8gb", @@ -944,7 +945,6 @@ "m3-2vcpu-16gb", "c-4-intel", "m3-2vcpu-16gb-intel", - "s-8vcpu-16gb-amd", "s-8vcpu-16gb-intel", "c2-4vcpu-8gb-intel", "c5-4vcpu-8gb", @@ -1179,12 +1179,10 @@ "gd-32vcpu-128gb-intel", "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", - "c-48-intel", "m3-24vcpu-192gb-intel", "m-32vcpu-256gb", "gd-40vcpu-160gb", "g6_5-32vcpu-128gb", - "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", "so-24vcpu-192gb-intel", "so-24vcpu-192gb", @@ -1198,7 +1196,6 @@ "g-48vcpu-192gb-intel", "c2-60vcpu-120gb-intel", "gd-48vcpu-192gb-intel", - "c5-48vcpu-96gb-intel", "so1_5-24vcpu-192gb", "so-32vcpu-256gb-intel", "g5_5-48vcpu-192gb-intel", @@ -1474,6 +1471,7 @@ "c2-32vcpu-64gb", "m-16vcpu-128gb-intel", "m3-16vcpu-128gb", + "c-32-intel", "c-48", "m-24vcpu-192gb", "g-32vcpu-128gb", @@ -1787,7 +1785,6 @@ "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", "c5-32vcpu-64gb", - "c-48", "m-24vcpu-192gb", "g-32vcpu-128gb", "so-16vcpu-128gb-intel", @@ -1795,7 +1792,6 @@ "m6-16vcpu-128gb", "gd-32vcpu-128gb", "so1_5-16vcpu-128gb-intel", - "c2-48vcpu-96gb", "m-24vcpu-192gb-intel", "g-32vcpu-128gb-intel", "m3-24vcpu-192gb", @@ -2101,7 +2097,6 @@ "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", "m-48vcpu-384gb-intel", - "gpu-h200x1-141gb", "m3-48vcpu-384gb-intel", "so-48vcpu-384gb-intel" ], diff --git a/test/fixtures/hetzner/_metadata.json b/test/fixtures/hetzner/_metadata.json index 87157b730..12a0d7c8a 100644 --- a/test/fixtures/hetzner/_metadata.json +++ b/test/fixtures/hetzner/_metadata.json @@ -1,12 +1,12 @@ { "cloud": "hetzner", - "recorded_at": "2026-02-13T05:00:36Z", + "recorded_at": "2026-02-13T06:37:18Z", "fixtures": { - "server_types": {"endpoint": "/server_types?per_page=50", "recorded_at": "2026-02-13T05:00:29Z"}, - "locations": {"endpoint": "/locations", "recorded_at": "2026-02-13T05:00:29Z"}, - "ssh_keys": {"endpoint": "/ssh_keys", "recorded_at": "2026-02-13T05:00:30Z"}, - "servers": {"endpoint": "/servers", "recorded_at": "2026-02-13T05:00:31Z"}, - "create_server": {"endpoint": "POST /servers", "type": "live", "recorded_at": "2026-02-13T05:00:33Z"}, - "delete_server": {"endpoint": "DELETE /servers/{id}", "type": "live", "recorded_at": "2026-02-13T05:00:36Z"} + "server_types": {"endpoint": "/server_types?per_page=50", "recorded_at": "2026-02-13T06:37:11Z"}, + "locations": {"endpoint": "/locations", "recorded_at": "2026-02-13T06:37:11Z"}, + "ssh_keys": {"endpoint": "/ssh_keys", "recorded_at": "2026-02-13T06:37:12Z"}, + "servers": {"endpoint": "/servers", "recorded_at": "2026-02-13T06:37:13Z"}, + "create_server": {"endpoint": "POST /servers", "type": "live", "recorded_at": "2026-02-13T06:37:15Z"}, + "delete_server": {"endpoint": "DELETE /servers/{id}", "type": "live", "recorded_at": "2026-02-13T06:37:18Z"} } } diff --git a/test/fixtures/hetzner/create_server.json b/test/fixtures/hetzner/create_server.json index d0cfc9fce..448f16ed9 100644 --- a/test/fixtures/hetzner/create_server.json +++ b/test/fixtures/hetzner/create_server.json @@ -3,11 +3,11 @@ "command": "create_server", "error": null, "finished": null, - "id": 611302701715360, + "id": 611311291703796, "progress": 0, "resources": [ { - "id": 120889518, + "id": 120895015, "type": "server" }, { @@ -15,7 +15,7 @@ "type": "image" } ], - "started": "2026-02-13T05:00:33Z", + "started": "2026-02-13T06:37:14Z", "status": "running" }, "next_actions": [ @@ -23,23 +23,23 @@ "command": "start_server", "error": null, "finished": null, - "id": 611302701715361, - "parent_id": 611302701715360, + "id": 611311291703797, + "parent_id": 611311291703796, "progress": 0, "resources": [ { - "id": 120889518, + "id": 120895015, "type": "server" } ], - "started": "2026-02-13T05:00:33Z", + "started": "2026-02-13T06:37:14Z", "status": "running" } ], "root_password": null, "server": { "backup_window": null, - "created": "2026-02-13T05:00:33Z", + "created": "2026-02-13T06:37:14Z", "datacenter": { "description": "Nuremberg 1 virtual DC 3", "id": 2, @@ -168,7 +168,7 @@ ] } }, - "id": 120889518, + "id": 120895015, "image": { "architecture": "x86", "bound_to": null, @@ -207,7 +207,7 @@ "network_zone": "eu-central" }, "locked": false, - "name": "spawn-record-1770958831", + "name": "spawn-record-1770964633", "outgoing_traffic": 0, "placement_group": null, "primary_disk_size": 40, @@ -222,13 +222,13 @@ "ipv4": { "blocked": false, "dns_ptr": "static.81.176.245.188.clients.your-server.de", - "id": 117975188, + "id": 117982494, "ip": "188.245.176.81" }, "ipv6": { "blocked": false, "dns_ptr": [], - "id": 117975189, + "id": 117982495, "ip": "2a01:4f8:1c1a:f748::/64" } }, diff --git a/test/fixtures/hetzner/delete_server.json b/test/fixtures/hetzner/delete_server.json index ee03f36a6..0c51bbad8 100644 --- a/test/fixtures/hetzner/delete_server.json +++ b/test/fixtures/hetzner/delete_server.json @@ -3,15 +3,15 @@ "command": "delete_server", "error": null, "finished": null, - "id": 611302701715371, + "id": 611311291703818, "progress": 0, "resources": [ { - "id": 120889518, + "id": 120895015, "type": "server" } ], - "started": "2026-02-13T05:00:36Z", + "started": "2026-02-13T06:37:17Z", "status": "running" } } diff --git a/test/fixtures/linode/_metadata.json b/test/fixtures/linode/_metadata.json index b8122a17d..0109f5b7f 100644 --- a/test/fixtures/linode/_metadata.json +++ b/test/fixtures/linode/_metadata.json @@ -1,13 +1,13 @@ { "cloud": "linode", - "recorded_at": "2026-02-13T05:00:35Z", + "recorded_at": "2026-02-13T06:37:17Z", "fixtures": { - "profile": {"endpoint": "/profile", "recorded_at": "2026-02-13T05:00:28Z"}, - "ssh_keys": {"endpoint": "/profile/sshkeys", "recorded_at": "2026-02-13T05:00:28Z"}, - "instances": {"endpoint": "/linode/instances", "recorded_at": "2026-02-13T05:00:28Z"}, - "types": {"endpoint": "/linode/types", "recorded_at": "2026-02-13T05:00:29Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T05:00:29Z"}, - "create_server": {"endpoint": "POST /linode/instances", "type": "live", "recorded_at": "2026-02-13T05:00:32Z"}, - "delete_server": {"endpoint": "DELETE /linode/instances/{id}", "type": "live", "recorded_at": "2026-02-13T05:00:35Z"} + "profile": {"endpoint": "/profile", "recorded_at": "2026-02-13T06:37:10Z"}, + "ssh_keys": {"endpoint": "/profile/sshkeys", "recorded_at": "2026-02-13T06:37:10Z"}, + "instances": {"endpoint": "/linode/instances", "recorded_at": "2026-02-13T06:37:10Z"}, + "types": {"endpoint": "/linode/types", "recorded_at": "2026-02-13T06:37:11Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T06:37:11Z"}, + "create_server": {"endpoint": "POST /linode/instances", "type": "live", "recorded_at": "2026-02-13T06:37:13Z"}, + "delete_server": {"endpoint": "DELETE /linode/instances/{id}", "type": "live", "recorded_at": "2026-02-13T06:37:17Z"} } } diff --git a/test/fixtures/vultr/_metadata.json b/test/fixtures/vultr/_metadata.json index 6eb5cace5..46fa29ce9 100644 --- a/test/fixtures/vultr/_metadata.json +++ b/test/fixtures/vultr/_metadata.json @@ -1,8 +1,8 @@ { "cloud": "vultr", - "recorded_at": "2026-02-13T05:00:32Z", + "recorded_at": "2026-02-13T06:37:13Z", "fixtures": { - "plans": {"endpoint": "/plans", "recorded_at": "2026-02-13T05:00:30Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T05:00:30Z"} + "plans": {"endpoint": "/plans", "recorded_at": "2026-02-13T06:37:12Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T06:37:12Z"} } } diff --git a/test/fixtures/vultr/plans.json b/test/fixtures/vultr/plans.json index 0e454de94..7794b0820 100644 --- a/test/fixtures/vultr/plans.json +++ b/test/fixtures/vultr/plans.json @@ -1068,7 +1068,6 @@ "lax", "atl", "ams", - "lhr", "sjc", "yto", "cdg", From 386b8d47e8e05662e14d79faeead898f549f514a Mon Sep 17 00:00:00 2001 From: Ahmed Abushagur Date: Tue, 10 Feb 2026 14:32:13 -0800 Subject: [PATCH 10/13] feat: testing --- test/fixtures/digitalocean/_metadata.json | 14 +- test/fixtures/digitalocean/regions.json | 119 ++++++--- test/fixtures/hetzner/_metadata.json | 14 +- test/fixtures/hetzner/create_server.json | 32 ++- test/fixtures/hetzner/delete_server.json | 6 +- test/fixtures/hetzner/servers.json | 283 +++++++++++++++++++++- test/fixtures/linode/_metadata.json | 14 +- test/fixtures/linode/regions.json | 109 +++------ test/fixtures/vultr/_metadata.json | 9 +- test/fixtures/vultr/plans.json | 80 +++--- test/record.sh | 62 +---- 11 files changed, 490 insertions(+), 252 deletions(-) diff --git a/test/fixtures/digitalocean/_metadata.json b/test/fixtures/digitalocean/_metadata.json index 3b9e27360..9b546f315 100644 --- a/test/fixtures/digitalocean/_metadata.json +++ b/test/fixtures/digitalocean/_metadata.json @@ -1,13 +1,11 @@ { "cloud": "digitalocean", - "recorded_at": "2026-02-13T06:37:18Z", + "recorded_at": "2026-02-10T21:55:29Z", "fixtures": { - "account": {"endpoint": "/account", "recorded_at": "2026-02-13T06:37:10Z"}, - "ssh_keys": {"endpoint": "/account/keys", "recorded_at": "2026-02-13T06:37:11Z"}, - "droplets": {"endpoint": "/droplets", "recorded_at": "2026-02-13T06:37:11Z"}, - "sizes": {"endpoint": "/sizes", "recorded_at": "2026-02-13T06:37:12Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T06:37:12Z"}, - "create_server": {"endpoint": "POST /droplets", "type": "live", "recorded_at": "2026-02-13T06:37:14Z"}, - "delete_server": {"endpoint": "DELETE /droplets/{id}", "type": "live", "recorded_at": "2026-02-13T06:37:18Z"} + "account": {"endpoint": "/account", "recorded_at": "2026-02-10T21:55:27Z"}, + "ssh_keys": {"endpoint": "/account/keys", "recorded_at": "2026-02-10T21:55:27Z"}, + "droplets": {"endpoint": "/droplets", "recorded_at": "2026-02-10T21:55:28Z"}, + "sizes": {"endpoint": "/sizes", "recorded_at": "2026-02-10T21:55:29Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-10T21:55:29Z"} } } diff --git a/test/fixtures/digitalocean/regions.json b/test/fixtures/digitalocean/regions.json index 3fed997ce..2ec9f844f 100644 --- a/test/fixtures/digitalocean/regions.json +++ b/test/fixtures/digitalocean/regions.json @@ -282,43 +282,32 @@ "c5-8vcpu-16gb-intel", "so1_5-4vcpu-32gb", "m-8vcpu-64gb", - "c-16", "g6_5-8vcpu-32gb", "g5_5-8vcpu-32gb-intel", - "c2-16vcpu-32gb", "m-8vcpu-64gb-intel", "m3-8vcpu-64gb", "c-16-intel", "m3-8vcpu-64gb-intel", "c2-16vcpu-32gb-intel", - "c5-16vcpu-32gb", - "g-16vcpu-64gb", "so-8vcpu-64gb-intel", "so-8vcpu-64gb", "m6-8vcpu-64gb", - "gd-16vcpu-64gb", "so1_5-8vcpu-64gb-intel", "g-16vcpu-64gb-intel", "gd-16vcpu-64gb-intel", "c5-16vcpu-32gb-intel", "so1_5-8vcpu-64gb", "m-16vcpu-128gb", - "c-32", - "g6_5-16vcpu-64gb", "g5_5-16vcpu-64gb-intel", - "c2-32vcpu-64gb", "m-16vcpu-128gb-intel", "m3-16vcpu-128gb", "c-32-intel", "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", - "c5-32vcpu-64gb", "m-24vcpu-192gb", - "g-32vcpu-128gb", "so-16vcpu-128gb-intel", "so-16vcpu-128gb", "m6-16vcpu-128gb", - "gd-32vcpu-128gb", "so1_5-16vcpu-128gb-intel", "m-24vcpu-192gb-intel", "g-32vcpu-128gb-intel", @@ -326,31 +315,22 @@ "gd-32vcpu-128gb-intel", "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", - "c-48-intel", "m3-24vcpu-192gb-intel", - "m-32vcpu-256gb", - "g6_5-32vcpu-128gb", - "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", "so-24vcpu-192gb-intel", "so-24vcpu-192gb", "m6-24vcpu-192gb", "m-32vcpu-256gb-intel", - "m3-32vcpu-256gb", "so1_5-24vcpu-192gb-intel", "m3-32vcpu-256gb-intel", "g-48vcpu-192gb-intel", "gd-48vcpu-192gb-intel", - "c5-48vcpu-96gb-intel", "so1_5-24vcpu-192gb", "so-32vcpu-256gb-intel", - "so-32vcpu-256gb", - "m6-32vcpu-256gb", "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", "gpu-h100x1-80gb", - "gpu-h200x1-141gb", - "so1_5-32vcpu-256gb" + "gpu-h200x1-141gb" ], "slug": "nyc2" }, @@ -409,6 +389,7 @@ "gd-2vcpu-8gb", "g-2vcpu-8gb-intel", "gd-2vcpu-8gb-intel", + "s-4vcpu-16gb-amd", "m-2vcpu-16gb", "c-4", "g6_5-2vcpu-8gb", @@ -420,6 +401,7 @@ "m3-2vcpu-16gb", "c-4-intel", "m3-2vcpu-16gb-intel", + "s-8vcpu-16gb-amd", "s-8vcpu-16gb-intel", "c2-4vcpu-8gb-intel", "c5-4vcpu-8gb", @@ -434,6 +416,7 @@ "gd-4vcpu-16gb-intel", "c5-4vcpu-8gb-intel", "so1_5-2vcpu-16gb", + "s-8vcpu-32gb-amd", "m-4vcpu-32gb", "c-8", "g6_5-4vcpu-16gb", @@ -488,7 +471,6 @@ "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", "c5-32vcpu-64gb", - "c-48", "m-24vcpu-192gb", "g-32vcpu-128gb", "so-16vcpu-128gb-intel", @@ -934,6 +916,7 @@ "gd-2vcpu-8gb", "g-2vcpu-8gb-intel", "gd-2vcpu-8gb-intel", + "s-4vcpu-16gb-amd", "m-2vcpu-16gb", "c-4", "g6_5-2vcpu-8gb", @@ -945,6 +928,7 @@ "m3-2vcpu-16gb", "c-4-intel", "m3-2vcpu-16gb-intel", + "s-8vcpu-16gb-amd", "s-8vcpu-16gb-intel", "c2-4vcpu-8gb-intel", "c5-4vcpu-8gb", @@ -959,6 +943,7 @@ "gd-4vcpu-16gb-intel", "c5-4vcpu-8gb-intel", "so1_5-2vcpu-16gb", + "s-8vcpu-32gb-amd", "m-4vcpu-32gb", "c-8", "g6_5-4vcpu-16gb", @@ -1026,18 +1011,36 @@ "m3-24vcpu-192gb", "g-40vcpu-160gb", "gd-32vcpu-128gb-intel", + "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", + "c-48-intel", "m3-24vcpu-192gb-intel", "gd-40vcpu-160gb", "g6_5-32vcpu-128gb", + "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", "so-24vcpu-192gb-intel", + "so-24vcpu-192gb", + "m6-24vcpu-192gb", "m-32vcpu-256gb-intel", + "c-60-intel", "so1_5-24vcpu-192gb-intel", "m3-32vcpu-256gb-intel", + "g-48vcpu-192gb-intel", + "c2-60vcpu-120gb-intel", + "gd-48vcpu-192gb-intel", + "c5-48vcpu-96gb-intel", "so-32vcpu-256gb-intel", + "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", - "gpu-h100x1-80gb" + "g-60vcpu-240gb-intel", + "m-48vcpu-384gb-intel", + "gd-60vcpu-240gb-intel", + "c5-60vcpu-120gb-intel", + "gpu-h100x1-80gb", + "m3-48vcpu-384gb-intel", + "g5_5-60vcpu-240gb-intel", + "so-48vcpu-384gb-intel" ], "slug": "ams3" }, @@ -1155,16 +1158,13 @@ "c5-16vcpu-32gb-intel", "so1_5-8vcpu-64gb", "m-16vcpu-128gb", - "c-32", "g6_5-16vcpu-64gb", "g5_5-16vcpu-64gb-intel", - "c2-32vcpu-64gb", "m-16vcpu-128gb-intel", "m3-16vcpu-128gb", "c-32-intel", "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", - "c5-32vcpu-64gb", "m-24vcpu-192gb", "g-32vcpu-128gb", "so-16vcpu-128gb-intel", @@ -1179,10 +1179,12 @@ "gd-32vcpu-128gb-intel", "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", + "c-48-intel", "m3-24vcpu-192gb-intel", "m-32vcpu-256gb", "gd-40vcpu-160gb", "g6_5-32vcpu-128gb", + "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", "so-24vcpu-192gb-intel", "so-24vcpu-192gb", @@ -1196,6 +1198,7 @@ "g-48vcpu-192gb-intel", "c2-60vcpu-120gb-intel", "gd-48vcpu-192gb-intel", + "c5-48vcpu-96gb-intel", "so1_5-24vcpu-192gb", "so-32vcpu-256gb-intel", "g5_5-48vcpu-192gb-intel", @@ -1332,19 +1335,26 @@ "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", "m-24vcpu-192gb", + "g-32vcpu-128gb", "so-16vcpu-128gb-intel", "so-16vcpu-128gb", "m6-16vcpu-128gb", + "gd-32vcpu-128gb", "so1_5-16vcpu-128gb-intel", + "gpu-l40sx1-48gb", "gpu-6000adax1-48gb", "m-24vcpu-192gb-intel", "g-32vcpu-128gb-intel", "m3-24vcpu-192gb", + "g-40vcpu-160gb", "gd-32vcpu-128gb-intel", "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", "c-48-intel", "m3-24vcpu-192gb-intel", + "m-32vcpu-256gb", + "gd-40vcpu-160gb", + "g6_5-32vcpu-128gb", "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", "so-24vcpu-192gb-intel", @@ -1352,14 +1362,18 @@ "m6-24vcpu-192gb", "m-32vcpu-256gb-intel", "c-60-intel", + "m3-32vcpu-256gb", "so1_5-24vcpu-192gb-intel", "m3-32vcpu-256gb-intel", + "g6_5-40vcpu-160gb", "g-48vcpu-192gb-intel", "c2-60vcpu-120gb-intel", "gd-48vcpu-192gb-intel", "c5-48vcpu-96gb-intel", "so1_5-24vcpu-192gb", "so-32vcpu-256gb-intel", + "so-32vcpu-256gb", + "m6-32vcpu-256gb", "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", "g-60vcpu-240gb-intel", @@ -1367,6 +1381,7 @@ "gd-60vcpu-240gb-intel", "c5-60vcpu-120gb-intel", "gpu-h100x1-80gb", + "so1_5-32vcpu-256gb", "m3-48vcpu-384gb-intel", "g5_5-60vcpu-240gb-intel", "so-48vcpu-384gb-intel" @@ -1448,41 +1463,78 @@ "so1_5-4vcpu-32gb-intel", "g-8vcpu-32gb-intel", "gd-8vcpu-32gb-intel", + "c5-8vcpu-16gb-intel", "so1_5-4vcpu-32gb", "m-8vcpu-64gb", "c-16", "g6_5-8vcpu-32gb", + "g5_5-8vcpu-32gb-intel", "c2-16vcpu-32gb", "m-8vcpu-64gb-intel", "m3-8vcpu-64gb", "c-16-intel", + "m3-8vcpu-64gb-intel", "c2-16vcpu-32gb-intel", "c5-16vcpu-32gb", "g-16vcpu-64gb", + "so-8vcpu-64gb-intel", "so-8vcpu-64gb", "m6-8vcpu-64gb", "gd-16vcpu-64gb", + "so1_5-8vcpu-64gb-intel", "g-16vcpu-64gb-intel", "gd-16vcpu-64gb-intel", + "c5-16vcpu-32gb-intel", "so1_5-8vcpu-64gb", "m-16vcpu-128gb", "c-32", "g6_5-16vcpu-64gb", + "g5_5-16vcpu-64gb-intel", "c2-32vcpu-64gb", "m-16vcpu-128gb-intel", "m3-16vcpu-128gb", "c-32-intel", + "m3-16vcpu-128gb-intel", + "c2-32vcpu-64gb-intel", "c-48", "m-24vcpu-192gb", "g-32vcpu-128gb", + "so-16vcpu-128gb-intel", "so-16vcpu-128gb", "m6-16vcpu-128gb", "gd-32vcpu-128gb", + "so1_5-16vcpu-128gb-intel", "c2-48vcpu-96gb", + "m-24vcpu-192gb-intel", "g-32vcpu-128gb-intel", "g-40vcpu-160gb", + "gd-32vcpu-128gb-intel", + "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", - "gd-40vcpu-160gb" + "c-48-intel", + "m3-24vcpu-192gb-intel", + "gd-40vcpu-160gb", + "c2-48vcpu-96gb-intel", + "g5_5-32vcpu-128gb-intel", + "so-24vcpu-192gb-intel", + "m-32vcpu-256gb-intel", + "c-60-intel", + "so1_5-24vcpu-192gb-intel", + "m3-32vcpu-256gb-intel", + "g-48vcpu-192gb-intel", + "c2-60vcpu-120gb-intel", + "gd-48vcpu-192gb-intel", + "c5-48vcpu-96gb-intel", + "so-32vcpu-256gb-intel", + "g5_5-48vcpu-192gb-intel", + "so1_5-32vcpu-256gb-intel", + "g-60vcpu-240gb-intel", + "m-48vcpu-384gb-intel", + "gd-60vcpu-240gb-intel", + "c5-60vcpu-120gb-intel", + "m3-48vcpu-384gb-intel", + "g5_5-60vcpu-240gb-intel", + "so-48vcpu-384gb-intel" ], "slug": "sfo2" }, @@ -1785,6 +1837,7 @@ "m3-16vcpu-128gb-intel", "c2-32vcpu-64gb-intel", "c5-32vcpu-64gb", + "c-48", "m-24vcpu-192gb", "g-32vcpu-128gb", "so-16vcpu-128gb-intel", @@ -1792,6 +1845,7 @@ "m6-16vcpu-128gb", "gd-32vcpu-128gb", "so1_5-16vcpu-128gb-intel", + "c2-48vcpu-96gb", "m-24vcpu-192gb-intel", "g-32vcpu-128gb-intel", "m3-24vcpu-192gb", @@ -1799,30 +1853,40 @@ "gd-32vcpu-128gb-intel", "c5-32vcpu-64gb-intel", "so1_5-16vcpu-128gb", + "c-48-intel", "m3-24vcpu-192gb-intel", "m-32vcpu-256gb", "gd-40vcpu-160gb", "g6_5-32vcpu-128gb", + "c2-48vcpu-96gb-intel", "g5_5-32vcpu-128gb-intel", + "c5-48vcpu-96gb", "so-24vcpu-192gb-intel", "so-24vcpu-192gb", "m6-24vcpu-192gb", "m-32vcpu-256gb-intel", + "c-60-intel", "m3-32vcpu-256gb", "so1_5-24vcpu-192gb-intel", "m3-32vcpu-256gb-intel", "g6_5-40vcpu-160gb", "g-48vcpu-192gb-intel", + "c2-60vcpu-120gb-intel", "gd-48vcpu-192gb-intel", + "c5-48vcpu-96gb-intel", "so1_5-24vcpu-192gb", "so-32vcpu-256gb-intel", "so-32vcpu-256gb", "m6-32vcpu-256gb", "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", + "g-60vcpu-240gb-intel", "m-48vcpu-384gb-intel", + "gd-60vcpu-240gb-intel", + "c5-60vcpu-120gb-intel", "so1_5-32vcpu-256gb", "m3-48vcpu-384gb-intel", + "g5_5-60vcpu-240gb-intel", "so-48vcpu-384gb-intel" ], "slug": "sfo3" @@ -2097,6 +2161,7 @@ "g5_5-48vcpu-192gb-intel", "so1_5-32vcpu-256gb-intel", "m-48vcpu-384gb-intel", + "gpu-h200x1-141gb", "m3-48vcpu-384gb-intel", "so-48vcpu-384gb-intel" ], diff --git a/test/fixtures/hetzner/_metadata.json b/test/fixtures/hetzner/_metadata.json index 12a0d7c8a..c5883a23a 100644 --- a/test/fixtures/hetzner/_metadata.json +++ b/test/fixtures/hetzner/_metadata.json @@ -1,12 +1,12 @@ { "cloud": "hetzner", - "recorded_at": "2026-02-13T06:37:18Z", + "recorded_at": "2026-02-10T21:55:26Z", "fixtures": { - "server_types": {"endpoint": "/server_types?per_page=50", "recorded_at": "2026-02-13T06:37:11Z"}, - "locations": {"endpoint": "/locations", "recorded_at": "2026-02-13T06:37:11Z"}, - "ssh_keys": {"endpoint": "/ssh_keys", "recorded_at": "2026-02-13T06:37:12Z"}, - "servers": {"endpoint": "/servers", "recorded_at": "2026-02-13T06:37:13Z"}, - "create_server": {"endpoint": "POST /servers", "type": "live", "recorded_at": "2026-02-13T06:37:15Z"}, - "delete_server": {"endpoint": "DELETE /servers/{id}", "type": "live", "recorded_at": "2026-02-13T06:37:18Z"} + "server_types": {"endpoint": "/server_types?per_page=50", "recorded_at": "2026-02-10T21:55:19Z"}, + "locations": {"endpoint": "/locations", "recorded_at": "2026-02-10T21:55:20Z"}, + "ssh_keys": {"endpoint": "/ssh_keys", "recorded_at": "2026-02-10T21:55:21Z"}, + "servers": {"endpoint": "/servers", "recorded_at": "2026-02-10T21:55:22Z"}, + "create_server": {"endpoint": "POST /servers", "type": "live", "recorded_at": "2026-02-10T21:55:24Z"}, + "delete_server": {"endpoint": "DELETE /servers/{id}", "type": "live", "recorded_at": "2026-02-10T21:55:26Z"} } } diff --git a/test/fixtures/hetzner/create_server.json b/test/fixtures/hetzner/create_server.json index 448f16ed9..74637d827 100644 --- a/test/fixtures/hetzner/create_server.json +++ b/test/fixtures/hetzner/create_server.json @@ -3,11 +3,11 @@ "command": "create_server", "error": null, "finished": null, - "id": 611311291703796, + "id": 610821665885201, "progress": 0, "resources": [ { - "id": 120895015, + "id": 120682780, "type": "server" }, { @@ -15,7 +15,7 @@ "type": "image" } ], - "started": "2026-02-13T06:37:14Z", + "started": "2026-02-10T21:55:23Z", "status": "running" }, "next_actions": [ @@ -23,23 +23,23 @@ "command": "start_server", "error": null, "finished": null, - "id": 611311291703797, - "parent_id": 611311291703796, + "id": 610821665885202, + "parent_id": 610821665885201, "progress": 0, "resources": [ { - "id": 120895015, + "id": 120682780, "type": "server" } ], - "started": "2026-02-13T06:37:14Z", + "started": "2026-02-10T21:55:23Z", "status": "running" } ], "root_password": null, "server": { "backup_window": null, - "created": "2026-02-13T06:37:14Z", + "created": "2026-02-10T21:55:23Z", "datacenter": { "description": "Nuremberg 1 virtual DC 3", "id": 2, @@ -65,7 +65,6 @@ 45, 93, 94, - 95, 96, 97, 98, @@ -92,7 +91,6 @@ 45, 93, 94, - 95, 96, 97, 98, @@ -168,7 +166,7 @@ ] } }, - "id": 120895015, + "id": 120682780, "image": { "architecture": "x86", "bound_to": null, @@ -207,7 +205,7 @@ "network_zone": "eu-central" }, "locked": false, - "name": "spawn-record-1770964633", + "name": "spawn-record-1770760522", "outgoing_traffic": 0, "placement_group": null, "primary_disk_size": 40, @@ -221,15 +219,15 @@ "floating_ips": [], "ipv4": { "blocked": false, - "dns_ptr": "static.81.176.245.188.clients.your-server.de", - "id": 117982494, - "ip": "188.245.176.81" + "dns_ptr": "static.197.210.245.188.clients.your-server.de", + "id": 117603379, + "ip": "188.245.210.197" }, "ipv6": { "blocked": false, "dns_ptr": [], - "id": 117982495, - "ip": "2a01:4f8:1c1a:f748::/64" + "id": 117603380, + "ip": "2a01:4f8:1c1e:bc55::/64" } }, "rescue_enabled": false, diff --git a/test/fixtures/hetzner/delete_server.json b/test/fixtures/hetzner/delete_server.json index 0c51bbad8..bbf825949 100644 --- a/test/fixtures/hetzner/delete_server.json +++ b/test/fixtures/hetzner/delete_server.json @@ -3,15 +3,15 @@ "command": "delete_server", "error": null, "finished": null, - "id": 611311291703818, + "id": 610821665885215, "progress": 0, "resources": [ { - "id": 120895015, + "id": 120682780, "type": "server" } ], - "started": "2026-02-13T06:37:17Z", + "started": "2026-02-10T21:55:26Z", "status": "running" } } diff --git a/test/fixtures/hetzner/servers.json b/test/fixtures/hetzner/servers.json index 8ff3b8b96..dd6315039 100644 --- a/test/fixtures/hetzner/servers.json +++ b/test/fixtures/hetzner/servers.json @@ -6,8 +6,287 @@ "page": 1, "per_page": 25, "previous_page": null, - "total_entries": 0 + "total_entries": 1 } }, - "servers": [] + "servers": [ + { + "backup_window": null, + "created": "2026-02-10T21:32:27Z", + "datacenter": { + "description": "Nuremberg 1 virtual DC 3", + "id": 2, + "location": { + "city": "Nuremberg", + "country": "DE", + "description": "Nuremberg DC Park 1", + "id": 2, + "latitude": 49.452102, + "longitude": 11.076665, + "name": "nbg1", + "network_zone": "eu-central" + }, + "name": "nbg1-dc3", + "server_types": { + "available": [ + 27, + 28, + 29, + 30, + 31, + 32, + 45, + 93, + 94, + 96, + 97, + 98, + 99, + 100, + 101, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117 + ], + "available_for_migration": [ + 27, + 28, + 29, + 30, + 31, + 32, + 45, + 93, + 94, + 96, + 97, + 98, + 99, + 100, + 101, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117 + ], + "supported": [ + 1, + 3, + 5, + 7, + 9, + 11, + 12, + 13, + 14, + 15, + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 104, + 105, + 106, + 107, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117 + ] + } + }, + "id": 120681182, + "image": { + "architecture": "x86", + "bound_to": null, + "created": "2024-04-25T13:26:27Z", + "created_from": null, + "deleted": null, + "deprecated": null, + "description": "Ubuntu 24.04", + "disk_size": 5, + "id": 161547269, + "image_size": null, + "labels": {}, + "name": "ubuntu-24.04", + "os_flavor": "ubuntu", + "os_version": "24.04", + "protection": { + "delete": false + }, + "rapid_deploy": true, + "status": "available", + "type": "system" + }, + "included_traffic": 21990232555520, + "ingoing_traffic": 0, + "iso": null, + "labels": {}, + "load_balancers": [], + "location": { + "city": "Nuremberg", + "country": "DE", + "description": "Nuremberg DC Park 1", + "id": 2, + "latitude": 49.452102, + "longitude": 11.076665, + "name": "nbg1", + "network_zone": "eu-central" + }, + "locked": false, + "name": "claude", + "outgoing_traffic": 0, + "placement_group": null, + "primary_disk_size": 40, + "private_net": [], + "protection": { + "delete": false, + "rebuild": false + }, + "public_net": { + "firewalls": [], + "floating_ips": [], + "ipv4": { + "blocked": false, + "dns_ptr": "static.157.65.225.46.clients.your-server.de", + "id": 117600167, + "ip": "46.225.65.157" + }, + "ipv6": { + "blocked": false, + "dns_ptr": [], + "id": 117600168, + "ip": "2a01:4f8:1c19:e41b::/64" + } + }, + "rescue_enabled": false, + "server_type": { + "architecture": "x86", + "category": "cost_optimized", + "cores": 2, + "cpu_type": "shared", + "deprecated": false, + "deprecation": null, + "description": "CX 23", + "disk": 40, + "id": 114, + "locations": [ + { + "deprecation": null, + "id": 1, + "name": "fsn1" + }, + { + "deprecation": null, + "id": 2, + "name": "nbg1" + }, + { + "deprecation": null, + "id": 3, + "name": "hel1" + } + ], + "memory": 4, + "name": "cx23", + "prices": [ + { + "included_traffic": 21990232555520, + "location": "fsn1", + "price_hourly": { + "gross": "0.0056000000000000", + "net": "0.0056000000" + }, + "price_monthly": { + "gross": "3.4900000000000000", + "net": "3.4900000000" + }, + "price_per_tb_traffic": { + "gross": "1.2000000000000000", + "net": "1.2000000000" + } + }, + { + "included_traffic": 21990232555520, + "location": "hel1", + "price_hourly": { + "gross": "0.0056000000000000", + "net": "0.0056000000" + }, + "price_monthly": { + "gross": "3.4900000000000000", + "net": "3.4900000000" + }, + "price_per_tb_traffic": { + "gross": "1.2000000000000000", + "net": "1.2000000000" + } + }, + { + "included_traffic": 21990232555520, + "location": "nbg1", + "price_hourly": { + "gross": "0.0056000000000000", + "net": "0.0056000000" + }, + "price_monthly": { + "gross": "3.4900000000000000", + "net": "3.4900000000" + }, + "price_per_tb_traffic": { + "gross": "1.2000000000000000", + "net": "1.2000000000" + } + } + ], + "storage_type": "local" + }, + "status": "running", + "volumes": [] + } + ] } diff --git a/test/fixtures/linode/_metadata.json b/test/fixtures/linode/_metadata.json index 0109f5b7f..029fa863d 100644 --- a/test/fixtures/linode/_metadata.json +++ b/test/fixtures/linode/_metadata.json @@ -1,13 +1,11 @@ { "cloud": "linode", - "recorded_at": "2026-02-13T06:37:17Z", + "recorded_at": "2026-02-10T21:55:36Z", "fixtures": { - "profile": {"endpoint": "/profile", "recorded_at": "2026-02-13T06:37:10Z"}, - "ssh_keys": {"endpoint": "/profile/sshkeys", "recorded_at": "2026-02-13T06:37:10Z"}, - "instances": {"endpoint": "/linode/instances", "recorded_at": "2026-02-13T06:37:10Z"}, - "types": {"endpoint": "/linode/types", "recorded_at": "2026-02-13T06:37:11Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T06:37:11Z"}, - "create_server": {"endpoint": "POST /linode/instances", "type": "live", "recorded_at": "2026-02-13T06:37:13Z"}, - "delete_server": {"endpoint": "DELETE /linode/instances/{id}", "type": "live", "recorded_at": "2026-02-13T06:37:17Z"} + "profile": {"endpoint": "/profile", "recorded_at": "2026-02-10T21:55:34Z"}, + "ssh_keys": {"endpoint": "/profile/sshkeys", "recorded_at": "2026-02-10T21:55:34Z"}, + "instances": {"endpoint": "/linode/instances", "recorded_at": "2026-02-10T21:55:35Z"}, + "types": {"endpoint": "/linode/types", "recorded_at": "2026-02-10T21:55:35Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-10T21:55:36Z"} } } diff --git a/test/fixtures/linode/regions.json b/test/fixtures/linode/regions.json index e8228ae3f..201fd3543 100644 --- a/test/fixtures/linode/regions.json +++ b/test/fixtures/linode/regions.json @@ -30,8 +30,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -76,8 +75,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -122,8 +120,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -168,8 +165,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -192,7 +188,6 @@ "Backups", "NodeBalancers", "Block Storage", - "Object Storage", "GPU Linodes", "Kubernetes", "Cloud Firewall", @@ -215,8 +210,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -239,7 +233,6 @@ "Backups", "NodeBalancers", "Block Storage", - "Object Storage", "GPU Linodes", "Kubernetes", "Cloud Firewall", @@ -261,8 +254,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -304,12 +296,10 @@ "label": "Melbourne, AU", "monitors": { "alerts": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -354,8 +344,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -401,8 +390,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -447,8 +435,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -494,8 +481,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -540,8 +526,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -586,8 +571,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -633,8 +617,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -679,8 +662,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -725,8 +707,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -770,8 +751,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -816,8 +796,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -862,8 +841,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -908,8 +886,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -950,12 +927,10 @@ "label": "Washington, DC", "monitors": { "alerts": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -994,12 +969,10 @@ "label": "Sydney, AU", "monitors": { "alerts": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -1042,8 +1015,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -1086,8 +1058,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -1129,8 +1100,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -1172,8 +1142,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -1217,8 +1186,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -1258,9 +1226,7 @@ "label": "Singapore, SG", "monitors": { "alerts": [], - "metrics": [ - "NodeBalancers" - ] + "metrics": [] }, "placement_group_limits": { "maximum_linodes_per_flexible_pg": 5, @@ -1297,9 +1263,7 @@ "label": "London, UK", "monitors": { "alerts": [], - "metrics": [ - "NodeBalancers" - ] + "metrics": [] }, "placement_group_limits": { "maximum_linodes_per_flexible_pg": 5, @@ -1342,8 +1306,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -1387,8 +1350,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { @@ -1430,8 +1392,7 @@ "Managed Databases" ], "metrics": [ - "Managed Databases", - "NodeBalancers" + "Managed Databases" ] }, "placement_group_limits": { diff --git a/test/fixtures/vultr/_metadata.json b/test/fixtures/vultr/_metadata.json index 46fa29ce9..96a70de8c 100644 --- a/test/fixtures/vultr/_metadata.json +++ b/test/fixtures/vultr/_metadata.json @@ -1,8 +1,11 @@ { "cloud": "vultr", - "recorded_at": "2026-02-13T06:37:13Z", + "recorded_at": "2026-02-10T21:55:33Z", "fixtures": { - "plans": {"endpoint": "/plans", "recorded_at": "2026-02-13T06:37:12Z"}, - "regions": {"endpoint": "/regions", "recorded_at": "2026-02-13T06:37:12Z"} + "account": {"endpoint": "/account", "recorded_at": "2026-02-10T21:55:30Z"}, + "ssh_keys": {"endpoint": "/ssh-keys", "recorded_at": "2026-02-10T21:55:31Z"}, + "instances": {"endpoint": "/instances", "recorded_at": "2026-02-10T21:55:31Z"}, + "plans": {"endpoint": "/plans", "recorded_at": "2026-02-10T21:55:33Z"}, + "regions": {"endpoint": "/regions", "recorded_at": "2026-02-10T21:55:33Z"} } } diff --git a/test/fixtures/vultr/plans.json b/test/fixtures/vultr/plans.json index 7794b0820..d4382630c 100644 --- a/test/fixtures/vultr/plans.json +++ b/test/fixtures/vultr/plans.json @@ -1,38 +1,12 @@ { "meta": { "links": { - "next": "bmV4dF9fdmNnLWE0MC02Yy0zMGctMTJ2cmFt", + "next": "bmV4dF9fdmNnLWExMDAtM2MtMzBnLTIwdnJhbQ==", "prev": "" }, - "total": 160 + "total": 159 }, "plans": [ - { - "bandwidth": 0, - "cpu_vendor": "Intel", - "deploy_ondemand": true, - "deploy_preemptible": false, - "disk": 10, - "disk_count": 1, - "disk_type": "SSD", - "hourly_cost": 0, - "hourly_cost_preemptible": 0, - "id": "vc2-1c-0.5gb-free", - "invoice_type": "monthly", - "location_cost": {}, - "locations": [ - "sea", - "fra", - "mia" - ], - "monthly_cost": 0, - "monthly_cost_preemptible": 0, - "ram": 512, - "storage_type": "local_storage", - "type": "vc2", - "vcpu_count": 1, - "vcpu_type": "thread" - }, { "bandwidth": 512, "cpu_vendor": "Intel", @@ -1068,6 +1042,7 @@ "lax", "atl", "ams", + "fra", "sjc", "yto", "cdg", @@ -1111,7 +1086,6 @@ "invoice_type": "monthly", "location_cost": {}, "locations": [ - "ewr", "dfw", "sea", "lax", @@ -1162,11 +1136,11 @@ "ams", "sjc", "yto", + "cdg", "nrt", "waw", "mad", "icn", - "mia", "sgp", "sto", "mex", @@ -3179,7 +3153,6 @@ "dfw", "sea", "lax", - "atl", "ams", "lhr", "fra", @@ -3295,7 +3268,6 @@ "dfw", "sea", "lax", - "atl", "ams", "lhr", "fra", @@ -3594,7 +3566,6 @@ "locations": [ "ewr", "ord", - "sea", "lax", "ams", "lhr", @@ -3648,7 +3619,6 @@ "dfw", "sea", "lax", - "atl", "ams", "lhr", "fra", @@ -4037,7 +4007,6 @@ "dfw", "sea", "lax", - "atl", "ams", "lhr", "fra", @@ -4154,7 +4123,6 @@ "ams", "lhr", "sjc", - "syd", "cdg", "nrt", "waw", @@ -4463,7 +4431,6 @@ "dfw", "sea", "lax", - "atl", "ams", "lhr", "fra", @@ -4679,7 +4646,6 @@ "dfw", "sea", "lax", - "atl", "ams", "lhr", "fra", @@ -4733,7 +4699,6 @@ "dfw", "sea", "lax", - "atl", "ams", "lhr", "fra", @@ -4837,7 +4802,6 @@ "dfw", "sea", "lax", - "atl", "ams", "lhr", "fra", @@ -5307,7 +5271,6 @@ "locations": [ "ewr", "lhr", - "syd", "nrt", "blr" ], @@ -5371,7 +5334,9 @@ "locations": [ "lhr", "fra", - "syd" + "syd", + "nrt", + "sgp" ], "monthly_cost": 210, "monthly_cost_preemptible": 210, @@ -5397,7 +5362,7 @@ "invoice_type": "hourly", "location_cost": {}, "locations": [ - "syd", + "ewr", "nrt" ], "monthly_cost": 315, @@ -5407,6 +5372,35 @@ "type": "vcg", "vcpu_count": 6, "vcpu_type": "thread" + }, + { + "bandwidth": 3072, + "cpu_vendor": "Intel", + "deploy_ondemand": true, + "deploy_preemptible": false, + "disk": 700, + "disk_count": 1, + "disk_type": "CLOUDGPU", + "gpu_type": "NVIDIA_A100", + "gpu_vram_gb": 20, + "hourly_cost": 0.616, + "hourly_cost_preemptible": 0.616, + "id": "vcg-a100-3c-30g-20vram", + "invoice_type": "hourly", + "location_cost": {}, + "locations": [ + "ewr", + "fra", + "sjc", + "nrt" + ], + "monthly_cost": 450, + "monthly_cost_preemptible": 450, + "ram": 30720, + "storage_type": "local_storage", + "type": "vcg", + "vcpu_count": 3, + "vcpu_type": "thread" } ] } diff --git a/test/record.sh b/test/record.sh index 4fcda7063..9f52bc90f 100644 --- a/test/record.sh +++ b/test/record.sh @@ -860,68 +860,10 @@ printf '\n' mkdir -p "$FIXTURES_DIR" -# Count clouds to decide sequential vs parallel -CLOUD_COUNT=0 -for _c in $CLOUDS_TO_RECORD; do - CLOUD_COUNT=$((CLOUD_COUNT + 1)) +for cloud in $CLOUDS_TO_RECORD; do + record_cloud "$cloud" done -# Parallel recording: each cloud runs in its own subshell when recording 2+ clouds -# Each cloud writes to its own fixture dir so there are no conflicts. -if [[ "$CLOUD_COUNT" -gt 1 ]] && [[ "$PROMPT_FOR_CREDS" != "true" ]]; then - printf '%b\n' "${CYAN}Recording ${CLOUD_COUNT} clouds in parallel...${NC}" - printf '\n' - - PARALLEL_DIR=$(mktemp -d /tmp/spawn-record-parallel-XXXXXX) - PIDS="" - - for cloud in $CLOUDS_TO_RECORD; do - ( - # Each subshell writes its own result file - RESULT_FILE="${PARALLEL_DIR}/${cloud}.result" - OUTPUT_FILE="${PARALLEL_DIR}/${cloud}.log" - - # Capture all output - record_cloud "$cloud" > "$OUTPUT_FILE" 2>&1 - - # Write counters to result file for aggregation - printf '%d %d %d\n' "$RECORDED" "$SKIPPED" "$ERRORS" > "$RESULT_FILE" - ) & - PIDS="${PIDS} $!" - done - - # Wait for all parallel recordings - for pid in $PIDS; do - wait "$pid" 2>/dev/null || true - done - - # Aggregate results and print output - for cloud in $CLOUDS_TO_RECORD; do - OUTPUT_FILE="${PARALLEL_DIR}/${cloud}.log" - RESULT_FILE="${PARALLEL_DIR}/${cloud}.result" - - # Print the captured output - if [[ -f "$OUTPUT_FILE" ]]; then - cat "$OUTPUT_FILE" - fi - - # Aggregate counters - if [[ -f "$RESULT_FILE" ]]; then - read -r r s e < "$RESULT_FILE" - RECORDED=$((RECORDED + r)) - SKIPPED=$((SKIPPED + s)) - ERRORS=$((ERRORS + e)) - fi - done - - rm -rf "$PARALLEL_DIR" -else - # Sequential recording: single cloud or interactive credential prompting - for cloud in $CLOUDS_TO_RECORD; do - record_cloud "$cloud" - done -fi - # --- Summary --- printf '%b\n' "${CYAN}===============================${NC}" TOTAL=$((RECORDED + SKIPPED + ERRORS)) From 7693ef64e5793e7e76ecd720e8ef2f9aa6c416ee Mon Sep 17 00:00:00 2001 From: Ahmed Abushagur Date: Thu, 12 Feb 2026 23:58:11 -0800 Subject: [PATCH 11/13] fix: local settings to git ignore --- .claude/settings.local.json | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 .claude/settings.local.json diff --git a/.claude/settings.local.json b/.claude/settings.local.json deleted file mode 100644 index d5f3891e3..000000000 --- a/.claude/settings.local.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "permissions": { - "allow": [ - "Bash(done)", - "Bash(grep:*)", - "Bash(for cloud in sprite hetzner digitalocean vultr linode)", - "Bash(do)", - "Bash(echo:*)", - "Bash(if ! grep -q \"setup_shell_environment\\\\|inject_env_vars\" \"/Users/ahmed/Documents/GitHub/spawn/$cloud/lib/common.sh\")", - "Bash(then)", - "Bash(fi)", - "Bash(if ! grep -q \"setup_claude_code_config\\\\|setup_openclaw_config\" \"/Users/ahmed/Documents/GitHub/spawn/$cloud/lib/common.sh\")", - "Bash(if ! grep -q \"run_server\\\\|upload_file\\\\|interactive_session\" \"/Users/ahmed/Documents/GitHub/spawn/$cloud/lib/common.sh\")", - "Bash( ls -d /Users/ahmed/Documents/GitHub/spawn/*/)", - "Bash(xargs:*)", - "Read(//private/tmp/**)", - "Bash(wc:*)", - "Bash(ls:*)", - "Bash(bash:*)" - ] - } -} From e91cbbea64e0553512fd79210f170b11a40ef69e Mon Sep 17 00:00:00 2001 From: A <6723574+louisgv@users.noreply.github.com> Date: Fri, 13 Feb 2026 09:59:36 +0000 Subject: [PATCH 12/13] fix: address review feedback on PR #833 1. Remove .claude/skills/.DS_Store binary file 2. Deduplicate .gitignore entries (settings.json x3, settings.local.json x3, .DS_Store x2) 3. Revert log_step -> log_warn regression across 17 agent scripts and 2 lib files (log_step is cyan for progress, log_warn is yellow for actual warnings per PR #440) Agent: pr-maintainer Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/skills/.DS_Store | Bin 6148 -> 0 bytes .gitignore | 6 -- aws-lightsail/claude.sh | 8 +-- binarylane/claude.sh | 8 +-- civo/claude.sh | 10 +-- daytona/claude.sh | 8 +-- digitalocean/claude.sh | 8 +-- e2b/claude.sh | 8 +-- gcp/claude.sh | 8 +-- genesiscloud/lib/common.sh | 144 ++++++++++++++----------------------- hetzner/claude.sh | 8 +-- ionos/claude.sh | 8 +-- kamatera/claude.sh | 10 +-- linode/claude.sh | 8 +-- modal/claude.sh | 8 +-- oracle/claude.sh | 8 +-- ovh/claude.sh | 8 +-- runpod/claude.sh | 8 +-- vastai/claude.sh | 8 +-- vultr/claude.sh | 8 +-- 20 files changed, 125 insertions(+), 165 deletions(-) delete mode 100644 .claude/skills/.DS_Store diff --git a/.claude/skills/.DS_Store b/.claude/skills/.DS_Store deleted file mode 100644 index 043c3ef82781bee9261019786b876a3c9c367c4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKPfNov6n~jhJ4Dz)VaI^iiPMdtcqwy!0V{e?nX4tPIf)-U8I@$>jz zl8G`scu-W{gO^|O{v^#WA!z_WbjE{SfGPkusDz~o79R-tNf)GKErde%F@kN#Ab}8u z3(;)&jSSG*Z9)nGuG<*iuHO)C39cvk5}4qc-az2z>Bx_ik6BOtC<@c8(Rdc6rSkI1 zsIal7T%Dt?gjnhs)xuDjmQjtHe_x-al$a~d|BNb=;Fb)!(5Do$ixjYZ!fyz5- z91l{R>lueraVouPZ93gMXxC+Pzcs7N>A_y3E?dp^Y*ukLw{{LsdUw%%tR4)_0(YdA zb&GR&#L5?DJn@n^QZeo4d5`m+7?}ZPfEoC!4A}F`S^KLK;O}Mzn1OF&fX)YrO6XcF z4CX3>-3f&lonl!g+cV72$PCvQiXkD2$PO>Y2#drg+Y@JLXV97*pY>O zp$I)X+NBN$;Tq(U8DIw9Gq7ZuRl5I=KY#zfPvRCczzqB=21L2*bvrmEd$%r3j_z8C sdW}j#afQLx6s+j07-Q)wuAyo{yQB)DYq2nh78L#w&@^zt4E!hqFS+wpO8@`> diff --git a/.gitignore b/.gitignore index 5f1f87994..0ff056993 100644 --- a/.gitignore +++ b/.gitignore @@ -17,10 +17,4 @@ id_ed25519 credentials.json service-account.json .claude/settings.json -.claude/settings.json -.claude/settings.json -.claude/settings.local.json .DS_Store -.claude/settings.local.json -.DS_Store -.claude/settings.local.json diff --git a/aws-lightsail/claude.sh b/aws-lightsail/claude.sh index 0c54a219f..7b353b9ae 100644 --- a/aws-lightsail/claude.sh +++ b/aws-lightsail/claude.sh @@ -30,9 +30,9 @@ verify_server_connectivity "${LIGHTSAIL_SERVER_IP}" wait_for_cloud_init "${LIGHTSAIL_SERVER_IP}" 60 # 5. Verify Claude Code is installed (fallback to manual install) -log_warn "Verifying Claude Code installation..." +log_step "Verifying Claude Code installation..." if ! run_server "${LIGHTSAIL_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_warn "Claude Code not found, installing manually..." + log_step "Claude Code not found, installing manually..." run_server "${LIGHTSAIL_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -46,7 +46,7 @@ else fi # 7. Inject environment variables into ~/.zshrc -log_warn "Setting up environment variables..." +log_step "Setting up environment variables..." inject_env_vars_ssh "${LIGHTSAIL_INSTANCE_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ @@ -67,7 +67,7 @@ log_info "Instance: ${SERVER_NAME} (IP: ${LIGHTSAIL_SERVER_IP})" echo "" # 9. Start Claude Code interactively -log_warn "Starting Claude Code..." +log_step "Starting Claude Code..." sleep 1 clear interactive_session "${LIGHTSAIL_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/binarylane/claude.sh b/binarylane/claude.sh index fda858c31..6ab29ff66 100644 --- a/binarylane/claude.sh +++ b/binarylane/claude.sh @@ -20,9 +20,9 @@ create_server "${SERVER_NAME}" verify_server_connectivity "${BINARYLANE_SERVER_IP}" wait_for_cloud_init "${BINARYLANE_SERVER_IP}" 60 -log_warn "Verifying Claude Code installation..." +log_step "Verifying Claude Code installation..." if ! run_server "${BINARYLANE_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_warn "Claude Code not found, installing manually..." + log_step "Claude Code not found, installing manually..." run_server "${BINARYLANE_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -34,7 +34,7 @@ else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) fi -log_warn "Setting up environment variables..." +log_step "Setting up environment variables..." inject_env_vars_ssh "${BINARYLANE_SERVER_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ @@ -52,7 +52,7 @@ log_info "BinaryLane server setup completed successfully!" log_info "Server: ${SERVER_NAME} (ID: ${BINARYLANE_SERVER_ID}, IP: ${BINARYLANE_SERVER_IP})" echo "" -log_warn "Starting Claude Code..." +log_step "Starting Claude Code..." sleep 1 clear interactive_session "${BINARYLANE_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/civo/claude.sh b/civo/claude.sh index a9dd9588c..2edd37092 100644 --- a/civo/claude.sh +++ b/civo/claude.sh @@ -19,13 +19,13 @@ SERVER_NAME=$(get_server_name) create_server "${SERVER_NAME}" verify_server_connectivity "${CIVO_SERVER_IP}" -log_warn "Waiting for cloud-init to complete..." +log_step "Waiting for cloud-init to complete..." # Civo uses init scripts, wait for completion marker generic_ssh_wait "root" "${CIVO_SERVER_IP}" "${SSH_OPTS} -o ConnectTimeout=5" "test -f /root/.cloud-init-complete" "cloud-init" 60 5 -log_warn "Verifying Claude Code installation..." +log_step "Verifying Claude Code installation..." if ! run_server "${CIVO_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_warn "Claude Code not found, installing manually..." + log_step "Claude Code not found, installing manually..." run_server "${CIVO_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -37,7 +37,7 @@ else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) fi -log_warn "Setting up environment variables..." +log_step "Setting up environment variables..." inject_env_vars_ssh "${CIVO_SERVER_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ @@ -55,7 +55,7 @@ log_info "Civo instance setup completed successfully!" log_info "Server: ${SERVER_NAME} (ID: ${CIVO_SERVER_ID}, IP: ${CIVO_SERVER_IP})" echo "" -log_warn "Starting Claude Code..." +log_step "Starting Claude Code..." sleep 1 clear interactive_session "${CIVO_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/daytona/claude.sh b/daytona/claude.sh index bbff6a2d1..0ceeb5ae5 100644 --- a/daytona/claude.sh +++ b/daytona/claude.sh @@ -26,9 +26,9 @@ create_server "${SERVER_NAME}" wait_for_cloud_init # 4. Verify Claude Code is installed (fallback to manual install) -log_warn "Verifying Claude Code installation..." +log_step "Verifying Claude Code installation..." if ! run_server "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_warn "Claude Code not found, installing manually..." + log_step "Claude Code not found, installing manually..." run_server "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -42,7 +42,7 @@ else fi # 6. Inject environment variables into ~/.zshrc -log_warn "Setting up environment variables..." +log_step "Setting up environment variables..." inject_env_vars_local upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ @@ -63,7 +63,7 @@ log_info "Sandbox: ${SERVER_NAME} (ID: ${DAYTONA_SANDBOX_ID})" echo "" # 8. Start Claude Code interactively -log_warn "Starting Claude Code..." +log_step "Starting Claude Code..." sleep 1 clear interactive_session "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/digitalocean/claude.sh b/digitalocean/claude.sh index f0322bf8a..480b46e02 100755 --- a/digitalocean/claude.sh +++ b/digitalocean/claude.sh @@ -31,9 +31,9 @@ verify_server_connectivity "${DO_SERVER_IP}" wait_for_cloud_init "${DO_SERVER_IP}" 60 # 5. Verify Claude Code is installed (fallback to manual install) -log_warn "Verifying Claude Code installation..." +log_step "Verifying Claude Code installation..." if ! run_server "${DO_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_warn "Claude Code not found, installing manually..." + log_step "Claude Code not found, installing manually..." run_server "${DO_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi @@ -54,7 +54,7 @@ else fi # 7. Inject environment variables into ~/.zshrc -log_warn "Setting up environment variables..." +log_step "Setting up environment variables..." inject_env_vars_ssh "${DO_SERVER_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ @@ -74,7 +74,7 @@ log_info "Droplet: ${DROPLET_NAME} (ID: ${DO_DROPLET_ID}, IP: ${DO_SERVER_IP})" echo "" # 9. Start Claude Code interactively -log_warn "Starting Claude Code..." +log_step "Starting Claude Code..." sleep 1 clear interactive_session "${DO_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/e2b/claude.sh b/e2b/claude.sh index 0d2c41f1d..a6436d7c8 100755 --- a/e2b/claude.sh +++ b/e2b/claude.sh @@ -26,9 +26,9 @@ create_server "${SERVER_NAME}" wait_for_cloud_init # 4. Verify Claude Code is installed (fallback to manual install) -log_warn "Verifying Claude Code installation..." +log_step "Verifying Claude Code installation..." if ! run_server "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_warn "Claude Code not found, installing manually..." + log_step "Claude Code not found, installing manually..." run_server "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -42,7 +42,7 @@ else fi # 6. Inject environment variables into ~/.zshrc -log_warn "Setting up environment variables..." +log_step "Setting up environment variables..." inject_env_vars_local upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ @@ -63,7 +63,7 @@ log_info "Sandbox: ${SERVER_NAME} (ID: ${E2B_SANDBOX_ID})" echo "" # 8. Start Claude Code interactively -log_warn "Starting Claude Code..." +log_step "Starting Claude Code..." sleep 1 clear interactive_session "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/gcp/claude.sh b/gcp/claude.sh index a7ff8621c..79012e143 100644 --- a/gcp/claude.sh +++ b/gcp/claude.sh @@ -33,9 +33,9 @@ verify_server_connectivity "${GCP_SERVER_IP}" wait_for_cloud_init "${GCP_SERVER_IP}" 60 # 5. Verify Claude Code is installed (fallback to manual install) -log_warn "Verifying Claude Code installation..." +log_step "Verifying Claude Code installation..." if ! run_server "${GCP_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_warn "Claude Code not found, installing manually..." + log_step "Claude Code not found, installing manually..." run_server "${GCP_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -49,7 +49,7 @@ else fi # 7. Inject environment variables into ~/.zshrc -log_warn "Setting up environment variables..." +log_step "Setting up environment variables..." inject_env_vars_ssh "${GCP_SERVER_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ @@ -70,7 +70,7 @@ log_info "Instance: ${GCP_INSTANCE_NAME_ACTUAL} (Zone: ${GCP_ZONE}, IP: ${GCP_SE echo "" # 9. Start Claude Code interactively -log_warn "Starting Claude Code..." +log_step "Starting Claude Code..." sleep 1 clear interactive_session "${GCP_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/genesiscloud/lib/common.sh b/genesiscloud/lib/common.sh index c6462bde0..bf52d52dd 100644 --- a/genesiscloud/lib/common.sh +++ b/genesiscloud/lib/common.sh @@ -63,10 +63,7 @@ ensure_genesis_token() { # Check if SSH key is registered with Genesis Cloud genesis_check_ssh_key() { - local fingerprint="$1" - local existing_keys - existing_keys=$(genesis_api GET "/ssh-keys") - echo "$existing_keys" | grep -q "$fingerprint" + check_ssh_key_by_fingerprint genesis_api "/ssh-keys" "$1" } # Register SSH key with Genesis Cloud @@ -101,14 +98,52 @@ ensure_ssh_key() { } get_server_name() { - local server_name - server_name=$(get_resource_name "GENESIS_SERVER_NAME" "Enter instance name: ") || return 1 + get_validated_server_name "GENESIS_SERVER_NAME" "Enter instance name: " +} - if ! validate_server_name "$server_name"; then - return 1 - fi +# Get all SSH key IDs from Genesis Cloud +_genesis_get_ssh_key_ids() { + local ssh_keys_response + ssh_keys_response=$(genesis_api GET "/ssh-keys") + echo "$ssh_keys_response" | python3 -c " +import json, sys +data = json.loads(sys.stdin.read()) +keys = data.get('ssh_keys', []) +ids = [k['id'] for k in keys] +print(json.dumps(ids)) +" +} + +# Build Genesis Cloud instance creation request body +# $1=name $2=instance_type $3=region $4=image $5=ssh_key_ids +_genesis_build_instance_body() { + local name="$1" instance_type="$2" region="$3" image="$4" ssh_key_ids="$5" - echo "$server_name" + local userdata + userdata=$(get_cloud_init_userdata) + + echo "$userdata" | python3 -c " +import json, sys +userdata = sys.stdin.read() +body = { + 'name': sys.argv[1], + 'type': sys.argv[2], + 'region': sys.argv[3], + 'image': sys.argv[4], + 'ssh_key_ids': json.loads(sys.argv[5]), + 'startup_script': userdata +} +print(json.dumps(body)) +" "$name" "$instance_type" "$region" "$image" "$ssh_key_ids" +} + +# Poll Genesis Cloud API until instance is active, then extract IP +# Sets GENESIS_SERVER_IP on success +_genesis_wait_for_instance() { + local server_id="$1" + generic_wait_for_instance genesis_api "/instances/${server_id}" \ + "active" "d['instance']['status']" "d['instance']['public_ip']" \ + GENESIS_SERVER_IP "Instance" 60 } create_server() { @@ -123,37 +158,13 @@ create_server() { # Image names may contain spaces (e.g. "Ubuntu 24.04") but must not contain quotes or shell metacharacters if [[ "$image" =~ [\'\"\`\$\;\\] ]]; then log_error "Invalid GENESIS_IMAGE: contains unsafe characters"; return 1; fi - log_warn "Creating Genesis Cloud instance '$name' (type: $instance_type, region: $region)..." + log_step "Creating Genesis Cloud instance '$name' (type: $instance_type, region: $region)..." - # Get all SSH key IDs - local ssh_keys_response - ssh_keys_response=$(genesis_api GET "/ssh-keys") local ssh_key_ids - ssh_key_ids=$(echo "$ssh_keys_response" | python3 -c " -import json, sys -data = json.loads(sys.stdin.read()) -keys = data.get('ssh_keys', []) -ids = [k['id'] for k in keys] -print(json.dumps(ids)) -") - - local userdata - userdata=$(get_cloud_init_userdata) + ssh_key_ids=$(_genesis_get_ssh_key_ids) local body - body=$(echo "$userdata" | python3 -c " -import json, sys -userdata = sys.stdin.read() -body = { - 'name': '$name', - 'type': '$instance_type', - 'region': '$region', - 'image': '$image', - 'ssh_key_ids': $ssh_key_ids, - 'startup_script': userdata -} -print(json.dumps(body)) -") + body=$(_genesis_build_instance_body "$name" "$instance_type" "$region" "$image" "$ssh_key_ids") local response response=$(genesis_api POST "/instances" "$body") @@ -164,11 +175,9 @@ print(json.dumps(body)) log_info "Instance created: ID=$GENESIS_SERVER_ID" else log_error "Failed to create Genesis Cloud instance" - local error_msg error_msg=$(echo "$response" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('error',{}).get('message', d.get('message','Unknown error')))" 2>/dev/null || echo "$response") log_error "API Error: $error_msg" - log_warn "Common issues:" log_warn " - Insufficient account balance" log_warn " - Instance type unavailable in region (try different GENESIS_INSTANCE_TYPE or GENESIS_REGION)" @@ -177,61 +186,18 @@ print(json.dumps(body)) return 1 fi - # Wait for instance to get an IP and become active - log_warn "Waiting for instance to become active..." - local max_attempts=60 - local attempt=1 - while [[ "$attempt" -le "$max_attempts" ]]; do - local status_response - status_response=$(genesis_api GET "/instances/$GENESIS_SERVER_ID") - local status - status=$(echo "$status_response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['instance']['status'])") - - if [[ "$status" == "active" ]]; then - GENESIS_SERVER_IP=$(echo "$status_response" | python3 -c "import json,sys; print(json.loads(sys.stdin.read())['instance']['public_ip'])") - export GENESIS_SERVER_IP - log_info "Instance active: IP=$GENESIS_SERVER_IP" - return 0 - fi - - log_warn "Instance status: $status ($attempt/$max_attempts)" - sleep "${INSTANCE_STATUS_POLL_DELAY}" - attempt=$((attempt + 1)) - done - - log_error "Instance did not become active in time" - return 1 -} - -verify_server_connectivity() { - local ip="$1" - local max_attempts=${2:-30} - # SSH_OPTS is defined in shared/common.sh - # shellcheck disable=SC2154 - generic_ssh_wait "root" "$ip" "$SSH_OPTS -o ConnectTimeout=5" "echo ok" "SSH connectivity" "$max_attempts" 5 -} - -run_server() { - local ip="$1"; local cmd="$2" - # shellcheck disable=SC2086 - ssh $SSH_OPTS "root@$ip" "$cmd" + _genesis_wait_for_instance "$GENESIS_SERVER_ID" } -upload_file() { - local ip="$1"; local local_path="$2"; local remote_path="$3" - # shellcheck disable=SC2086 - scp $SSH_OPTS "$local_path" "root@$ip:$remote_path" -} - -interactive_session() { - local ip="$1"; local cmd="$2" - # shellcheck disable=SC2086 - ssh -t $SSH_OPTS "root@$ip" "$cmd" -} +# SSH operations — delegates to shared helpers (SSH_USER defaults to root) +verify_server_connectivity() { ssh_verify_connectivity "$@"; } +run_server() { ssh_run_server "$@"; } +upload_file() { ssh_upload_file "$@"; } +interactive_session() { ssh_interactive_session "$@"; } destroy_server() { local server_id="$1" - log_warn "Destroying instance $server_id..." + log_step "Destroying instance $server_id..." genesis_api DELETE "/instances/$server_id" log_info "Instance $server_id destroyed" } diff --git a/hetzner/claude.sh b/hetzner/claude.sh index de7690721..78346bf2b 100755 --- a/hetzner/claude.sh +++ b/hetzner/claude.sh @@ -28,9 +28,9 @@ verify_server_connectivity "${HETZNER_SERVER_IP}" wait_for_cloud_init "${HETZNER_SERVER_IP}" 60 # 5. Verify Claude Code is installed (fallback to manual install) -log_warn "Verifying Claude Code installation..." +log_step "Verifying Claude Code installation..." if ! run_server "${HETZNER_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_warn "Claude Code not found, installing manually..." + log_step "Claude Code not found, installing manually..." run_server "${HETZNER_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi @@ -50,7 +50,7 @@ else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) fi -log_warn "Setting up environment variables..." +log_step "Setting up environment variables..." inject_env_vars_ssh "${HETZNER_SERVER_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ @@ -70,7 +70,7 @@ log_info "Server: ${SERVER_NAME} (ID: ${HETZNER_SERVER_ID}, IP: ${HETZNER_SERVER echo "" # 9. Start Claude Code interactively -log_warn "Starting Claude Code..." +log_step "Starting Claude Code..." sleep 1 clear interactive_session "${HETZNER_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/ionos/claude.sh b/ionos/claude.sh index 0808c4dea..8fa6f55be 100644 --- a/ionos/claude.sh +++ b/ionos/claude.sh @@ -30,9 +30,9 @@ verify_server_connectivity "${LAMBDA_SERVER_IP}" wait_for_cloud_init "${LAMBDA_SERVER_IP}" # 5. Verify Claude Code is installed (fallback to manual install) -log_warn "Verifying Claude Code installation..." +log_step "Verifying Claude Code installation..." if ! run_server "${LAMBDA_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_warn "Claude Code not found, installing manually..." + log_step "Claude Code not found, installing manually..." run_server "${LAMBDA_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -46,7 +46,7 @@ else fi # 7. Inject environment variables into ~/.zshrc -log_warn "Setting up environment variables..." +log_step "Setting up environment variables..." inject_env_vars_ssh "${LAMBDA_INSTANCE_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ @@ -67,7 +67,7 @@ log_info "Instance: ${SERVER_NAME} (IP: ${LAMBDA_SERVER_IP})" echo "" # 9. Start Claude Code interactively -log_warn "Starting Claude Code..." +log_step "Starting Claude Code..." sleep 1 clear interactive_session "${LAMBDA_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/kamatera/claude.sh b/kamatera/claude.sh index 388cf3a6d..4769472f7 100644 --- a/kamatera/claude.sh +++ b/kamatera/claude.sh @@ -19,12 +19,12 @@ SERVER_NAME=$(get_server_name) create_server "${SERVER_NAME}" verify_server_connectivity "${KAMATERA_SERVER_IP}" -log_warn "Waiting for init script to complete..." +log_step "Waiting for init script to complete..." generic_ssh_wait "root" "${KAMATERA_SERVER_IP}" "${SSH_OPTS} -o ConnectTimeout=5" "test -f /root/.cloud-init-complete" "init script" 60 5 -log_warn "Verifying Claude Code installation..." +log_step "Verifying Claude Code installation..." if ! run_server "${KAMATERA_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_warn "Claude Code not found, installing manually..." + log_step "Claude Code not found, installing manually..." run_server "${KAMATERA_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -36,7 +36,7 @@ else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) fi -log_warn "Setting up environment variables..." +log_step "Setting up environment variables..." inject_env_vars_ssh "${KAMATERA_SERVER_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ @@ -54,7 +54,7 @@ log_info "Kamatera server setup completed successfully!" log_info "Server: ${SERVER_NAME} (IP: ${KAMATERA_SERVER_IP})" echo "" -log_warn "Starting Claude Code..." +log_step "Starting Claude Code..." sleep 1 clear interactive_session "${KAMATERA_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/linode/claude.sh b/linode/claude.sh index 1296547bd..ea11c3526 100755 --- a/linode/claude.sh +++ b/linode/claude.sh @@ -12,16 +12,16 @@ SERVER_NAME=$(get_server_name) create_server "${SERVER_NAME}" verify_server_connectivity "${LINODE_SERVER_IP}" wait_for_cloud_init "${LINODE_SERVER_IP}" 60 -log_warn "Verifying Claude Code installation..." +log_step "Verifying Claude Code installation..." if ! run_server "${LINODE_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_warn "Claude Code not found, installing manually..." + log_step "Claude Code not found, installing manually..." run_server "${LINODE_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" echo "" if [[ -n "${OPENROUTER_API_KEY:-}" ]]; then log_info "Using OpenRouter API key from environment" else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180); fi -log_warn "Setting up environment variables..." +log_step "Setting up environment variables..." inject_env_vars_ssh "${LINODE_SERVER_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ @@ -36,7 +36,7 @@ echo "" log_info "Linode setup completed successfully!" log_info "Server: ${SERVER_NAME} (ID: ${LINODE_SERVER_ID}, IP: ${LINODE_SERVER_IP})" echo "" -log_warn "Starting Claude Code..." +log_step "Starting Claude Code..." sleep 1 clear interactive_session "${LINODE_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/modal/claude.sh b/modal/claude.sh index c4313e490..53f9c9640 100755 --- a/modal/claude.sh +++ b/modal/claude.sh @@ -32,9 +32,9 @@ fi wait_for_cloud_init # 4. Verify Claude Code is installed (fallback to manual install) -log_warn "Verifying Claude Code installation..." +log_step "Verifying Claude Code installation..." if ! run_server "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_warn "Claude Code not found, installing manually..." + log_step "Claude Code not found, installing manually..." run_server "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -48,7 +48,7 @@ else fi # 6. Inject environment variables into ~/.zshrc -log_warn "Setting up environment variables..." +log_step "Setting up environment variables..." inject_env_vars_local upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ @@ -68,7 +68,7 @@ log_info "Sandbox: ${SERVER_NAME} (ID: ${MODAL_SANDBOX_ID})" echo "" # 8. Start Claude Code interactively -log_warn "Starting Claude Code..." +log_step "Starting Claude Code..." sleep 1 clear interactive_session "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/oracle/claude.sh b/oracle/claude.sh index 207c64633..e8c37f600 100644 --- a/oracle/claude.sh +++ b/oracle/claude.sh @@ -33,9 +33,9 @@ verify_server_connectivity "${OCI_SERVER_IP}" wait_for_cloud_init "${OCI_SERVER_IP}" 60 # 5. Verify Claude Code is installed (fallback to manual install) -log_warn "Verifying Claude Code installation..." +log_step "Verifying Claude Code installation..." if ! run_server "${OCI_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_warn "Claude Code not found, installing manually..." + log_step "Claude Code not found, installing manually..." run_server "${OCI_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -49,7 +49,7 @@ else fi # 7. Inject environment variables into ~/.zshrc -log_warn "Setting up environment variables..." +log_step "Setting up environment variables..." inject_env_vars_ssh "${OCI_SERVER_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ @@ -70,7 +70,7 @@ log_info "Instance: ${OCI_INSTANCE_NAME_ACTUAL} (IP: ${OCI_SERVER_IP})" echo "" # 9. Start Claude Code interactively -log_warn "Starting Claude Code..." +log_step "Starting Claude Code..." sleep 1 clear interactive_session "${OCI_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/ovh/claude.sh b/ovh/claude.sh index e3644842c..71182c5bf 100644 --- a/ovh/claude.sh +++ b/ovh/claude.sh @@ -33,9 +33,9 @@ verify_server_connectivity "${OVH_SERVER_IP}" install_base_deps "${OVH_SERVER_IP}" # 7. Verify Claude Code is installed (fallback to manual install) -log_warn "Verifying Claude Code installation..." +log_step "Verifying Claude Code installation..." if ! run_ovh "${OVH_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_warn "Claude Code not found, installing manually..." + log_step "Claude Code not found, installing manually..." run_ovh "${OVH_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi @@ -55,7 +55,7 @@ else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) fi -log_warn "Setting up environment variables..." +log_step "Setting up environment variables..." inject_env_vars_ovh "${OVH_SERVER_IP}" \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ @@ -75,7 +75,7 @@ log_info "Instance: ${SERVER_NAME} (ID: ${OVH_INSTANCE_ID}, IP: ${OVH_SERVER_IP} echo "" # 10. Start Claude Code interactively -log_warn "Starting Claude Code..." +log_step "Starting Claude Code..." sleep 1 clear interactive_session "${OVH_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/runpod/claude.sh b/runpod/claude.sh index 2559eb80b..66ee62dd6 100644 --- a/runpod/claude.sh +++ b/runpod/claude.sh @@ -29,9 +29,9 @@ verify_server_connectivity install_base_tools # 5. Verify Claude Code is installed (fallback to manual install) -log_warn "Verifying Claude Code installation..." +log_step "Verifying Claude Code installation..." if ! run_server "${RUNPOD_POD_ID}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_warn "Claude Code not found, installing manually..." + log_step "Claude Code not found, installing manually..." run_server "${RUNPOD_POD_ID}" "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -45,7 +45,7 @@ else fi # 7. Inject environment variables -log_warn "Setting up environment variables..." +log_step "Setting up environment variables..." inject_env_vars_ssh "${RUNPOD_POD_ID}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ @@ -65,7 +65,7 @@ log_info "Pod: ${SERVER_NAME} (ID: ${RUNPOD_POD_ID})" echo "" # 9. Start Claude Code interactively -log_warn "Starting Claude Code..." +log_step "Starting Claude Code..." sleep 1 clear interactive_session "${RUNPOD_POD_ID}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/vastai/claude.sh b/vastai/claude.sh index c5ac3fbdc..ad9d11d0f 100644 --- a/vastai/claude.sh +++ b/vastai/claude.sh @@ -27,9 +27,9 @@ verify_server_connectivity install_base_tools # 4. Verify Claude Code is installed (fallback to manual install) -log_warn "Verifying Claude Code installation..." +log_step "Verifying Claude Code installation..." if ! run_server "${VASTAI_INSTANCE_ID}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_warn "Claude Code not found, installing manually..." + log_step "Claude Code not found, installing manually..." run_server "${VASTAI_INSTANCE_ID}" "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -43,7 +43,7 @@ else fi # 6. Inject environment variables -log_warn "Setting up environment variables..." +log_step "Setting up environment variables..." inject_env_vars_ssh "${VASTAI_INSTANCE_ID}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ @@ -63,7 +63,7 @@ log_info "Instance: ${SERVER_NAME} (ID: ${VASTAI_INSTANCE_ID})" echo "" # 8. Start Claude Code interactively -log_warn "Starting Claude Code..." +log_step "Starting Claude Code..." sleep 1 clear interactive_session "${VASTAI_INSTANCE_ID}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" diff --git a/vultr/claude.sh b/vultr/claude.sh index 6af2fbb55..076923c65 100755 --- a/vultr/claude.sh +++ b/vultr/claude.sh @@ -20,9 +20,9 @@ create_server "${SERVER_NAME}" verify_server_connectivity "${VULTR_SERVER_IP}" wait_for_cloud_init "${VULTR_SERVER_IP}" 60 -log_warn "Verifying Claude Code installation..." +log_step "Verifying Claude Code installation..." if ! run_server "${VULTR_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then - log_warn "Claude Code not found, installing manually..." + log_step "Claude Code not found, installing manually..." run_server "${VULTR_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash" fi log_info "Claude Code is installed" @@ -34,7 +34,7 @@ else OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180) fi -log_warn "Setting up environment variables..." +log_step "Setting up environment variables..." inject_env_vars_ssh "${VULTR_SERVER_IP}" upload_file run_server \ "OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \ "ANTHROPIC_BASE_URL=https://openrouter.ai/api" \ @@ -52,7 +52,7 @@ log_info "Vultr instance setup completed successfully!" log_info "Server: ${SERVER_NAME} (ID: ${VULTR_SERVER_ID}, IP: ${VULTR_SERVER_IP})" echo "" -log_warn "Starting Claude Code..." +log_step "Starting Claude Code..." sleep 1 clear interactive_session "${VULTR_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && source ~/.zshrc && claude" From 644231456d39fe3f81c2c8db6e17cc9822816b56 Mon Sep 17 00:00:00 2001 From: A <6723574+louisgv@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:40:06 +0000 Subject: [PATCH 13/13] fix: restore sprite mock state tracking and bash 3.x compat in test/run.sh - Replace ((var++)) with var=$((var + 1)) for macOS bash 3.x compat - Restore sprite mock stateful tracking (create/list coordination) - Clean up mock state files between tests to prevent cross-test pollution - Restore set -eo pipefail (was changed to -uo) Agent: pr-maintainer Co-Authored-By: Claude Opus 4.6 (1M context) --- test/run.sh | 208 ++++++++++++++++++++++++++++------------------------ 1 file changed, 111 insertions(+), 97 deletions(-) diff --git a/test/run.sh b/test/run.sh index 578666cd7..2a475813a 100644 --- a/test/run.sh +++ b/test/run.sh @@ -15,7 +15,7 @@ # bash test/run.sh claude # test one script # bash test/run.sh --remote # test remote source (from GitHub) -set -uo pipefail +set -eo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" TEST_DIR=$(mktemp -d) @@ -38,6 +38,8 @@ NC='\033[0m' cleanup() { rm -rf "${TEST_DIR}" + rm -f /tmp/sprite_mock_created 2>/dev/null || true + find /tmp -maxdepth 1 -name 'sprite_mock_created_*' -delete 2>/dev/null || true } trap 'cleanup' EXIT @@ -50,8 +52,18 @@ echo "sprite $*" >> "${MOCK_LOG}" case "$1" in org) exit 0 ;; # auth check passes - list) echo "existing-sprite"; exit 0 ;; # list returns no match for test sprite - create) exit 0 ;; + list) + echo "existing-sprite" + # After create, also return the test sprite name so provisioning poll succeeds + if [[ -f "/tmp/sprite_mock_created_$$" ]] || [[ -f "/tmp/sprite_mock_created" ]]; then + echo "${SPRITE_NAME:-}" + fi + exit 0 + ;; + create) + touch "/tmp/sprite_mock_created_$$" "/tmp/sprite_mock_created" + exit 0 + ;; exec) # If there's a -file flag, just pretend to upload if [[ "$*" == *"-file"* ]]; then @@ -95,12 +107,12 @@ assert_contains() { local file="$1" pattern="$2" msg="$3" if grep -qE "${pattern}" "${file}" 2>/dev/null; then printf '%b\n' " ${GREEN}✓${NC} ${msg}" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} ${msg}" printf '%b\n' " expected pattern: ${pattern}" printf '%b\n' " in: ${file}" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi } @@ -108,10 +120,10 @@ assert_not_contains() { local file="$1" pattern="$2" msg="$3" if ! grep -qE "${pattern}" "${file}" 2>/dev/null; then printf '%b\n' " ${GREEN}✓${NC} ${msg}" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} ${msg}" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi } @@ -119,10 +131,10 @@ assert_exit_code() { local actual="$1" expected="$2" msg="$3" if [[ "${actual}" -eq "${expected}" ]]; then printf '%b\n' " ${GREEN}✓${NC} ${msg}" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} ${msg} (got exit code ${actual}, expected ${expected})" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi } @@ -135,8 +147,10 @@ run_script_test() { echo "" printf '%b\n' "${YELLOW}━━━ Testing ${script_name}.sh ━━━${NC}" - # Reset mock log + # Reset mock log and state : > "${MOCK_LOG}" + rm -f /tmp/sprite_mock_created 2>/dev/null || true + find /tmp -maxdepth 1 -name 'sprite_mock_created_*' -delete 2>/dev/null || true # Run the script with mocked PATH and env vars (timeout 30s) local exit_code=0 @@ -185,7 +199,7 @@ run_script_test() { leaked_temps=$(find /tmp -maxdepth 1 -name "tmp.*" -newer "${MOCK_LOG}" 2>/dev/null | wc -l) if [[ "${leaked_temps}" -eq 0 ]]; then printf '%b\n' " ${GREEN}✓${NC} No temp files leaked" - ((PASSED++)) + PASSED=$((PASSED + 1)) fi } @@ -212,10 +226,10 @@ test_common_source() { missing=$(echo "${output}" | grep "^MISSING:" || true) if [[ -z "${missing}" ]]; then printf '%b\n' " ${GREEN}✓${NC} All functions defined" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} Missing functions: ${missing}" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 2: log functions write to stderr, not stdout @@ -224,10 +238,10 @@ test_common_source() { stderr=$(timeout 5 bash -c 'source "'"${REPO_ROOT}"'/sprite/lib/common.sh" && log_info "test"' &1 >/dev/null) if [[ -z "${stdout}" && -n "${stderr}" ]]; then printf '%b\n' " ${GREEN}✓${NC} Log functions write to stderr" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} Log functions should write to stderr only" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 3: get_sprite_name uses SPRITE_NAME env var @@ -235,10 +249,10 @@ test_common_source() { name=$(timeout 5 bash -c 'SPRITE_NAME=from-env; source "'"${REPO_ROOT}"'/sprite/lib/common.sh" && get_sprite_name' 2>/dev/null) if [[ "${name}" == "from-env" ]]; then printf '%b\n' " ${GREEN}✓${NC} get_sprite_name reads SPRITE_NAME env var" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} get_sprite_name should return 'from-env', got '${name}'" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 4: get_sprite_name fails gracefully without TTY or env var @@ -246,19 +260,19 @@ test_common_source() { timeout 5 bash -c 'SPRITE_NAME=""; source "'"${REPO_ROOT}"'/sprite/lib/common.sh" && get_sprite_name' /dev/null 2>&1 || rc=$? if [[ "${rc}" -ne 0 ]]; then printf '%b\n' " ${GREEN}✓${NC} get_sprite_name fails without TTY or env var" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} get_sprite_name should fail without input" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 5: Syntax check if bash -n "${REPO_ROOT}/sprite/lib/common.sh" 2>/dev/null; then printf '%b\n' " ${GREEN}✓${NC} Syntax valid" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} Syntax errors" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 6: Remote source (if --remote) @@ -270,10 +284,10 @@ test_common_source() { ' 2>/dev/null) if [[ "${remote_fns}" == "OK" ]]; then printf '%b\n' " ${GREEN}✓${NC} Remote source from GitHub works" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} Remote source from GitHub failed" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi fi } @@ -288,10 +302,10 @@ test_shared_common() { result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_model_id "anthropic/claude-3.5-sonnet" && echo "valid"' 2>/dev/null) if [[ "${result}" == "valid" ]]; then printf '%b\n' " ${GREEN}✓${NC} validate_model_id accepts valid model IDs" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} validate_model_id should accept 'anthropic/claude-3.5-sonnet'" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 2: validate_model_id rejects invalid characters @@ -299,30 +313,30 @@ test_shared_common() { bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_model_id "bad;model"' /dev/null 2>&1 || rc=$? if [[ "${rc}" -ne 0 ]]; then printf '%b\n' " ${GREEN}✓${NC} validate_model_id rejects invalid characters" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} validate_model_id should reject 'bad;model'" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 3: validate_model_id accepts empty string result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_model_id "" && echo "valid"' 2>/dev/null) if [[ "${result}" == "valid" ]]; then printf '%b\n' " ${GREEN}✓${NC} validate_model_id accepts empty string" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} validate_model_id should accept empty string" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 4: json_escape handles special characters result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && json_escape "test\"quote"' 2>/dev/null) if [[ "${result}" == *'\\"'* ]] || [[ "${result}" == *'\"'* ]]; then printf '%b\n' " ${GREEN}✓${NC} json_escape handles special characters" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} json_escape should escape quotes" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 5: generate_ssh_key_if_missing creates key if missing @@ -330,10 +344,10 @@ test_shared_common() { bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && generate_ssh_key_if_missing "'"${test_key}"'"' >/dev/null 2>&1 if [[ -f "${test_key}" && -f "${test_key}.pub" ]]; then printf '%b\n' " ${GREEN}✓${NC} generate_ssh_key_if_missing creates key" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} generate_ssh_key_if_missing should create key at ${test_key}" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 6: generate_ssh_key_if_missing skips if key exists @@ -345,29 +359,29 @@ test_shared_common() { mtime_after=$(stat -c %Y "${test_key}" 2>/dev/null || stat -f %m "${test_key}" 2>/dev/null) if [[ "${mtime_before}" == "${mtime_after}" ]]; then printf '%b\n' " ${GREEN}✓${NC} generate_ssh_key_if_missing skips existing key" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} generate_ssh_key_if_missing should not recreate existing key" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 7: get_ssh_fingerprint returns fingerprint result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && get_ssh_fingerprint "'"${test_key}.pub"'"' 2>/dev/null) if [[ -n "${result}" && "${result}" =~ ^[a-f0-9:]+$ ]]; then printf '%b\n' " ${GREEN}✓${NC} get_ssh_fingerprint returns valid fingerprint" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} get_ssh_fingerprint should return hex fingerprint, got '${result}'" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 8: Syntax check for shared/common.sh if bash -n "${REPO_ROOT}/shared/common.sh" 2>/dev/null; then printf '%b\n' " ${GREEN}✓${NC} shared/common.sh syntax valid" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} shared/common.sh has syntax errors" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 9: All logging functions exist in shared/common.sh @@ -380,10 +394,10 @@ test_shared_common() { missing=$(echo "${output}" | grep "^MISSING:" || true) if [[ -z "${missing}" ]]; then printf '%b\n' " ${GREEN}✓${NC} All logging functions exist in shared/common.sh" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} Missing logging functions: ${missing}" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 10: extract_ssh_key_ids parses JSON correctly @@ -391,10 +405,10 @@ test_shared_common() { result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && echo '"'${mock_json}'"' | extract_ssh_key_ids "$(cat)" "ssh_keys"' 2>/dev/null) if [[ "${result}" == "[123, 456]" ]] || [[ "${result}" == "[123,456]" ]]; then printf '%b\n' " ${GREEN}✓${NC} extract_ssh_key_ids parses JSON correctly" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} extract_ssh_key_ids should return [123, 456], got '${result}'" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 11: open_browser detects termux-open-url @@ -406,10 +420,10 @@ test_shared_common() { ' 2>/dev/null) if [[ "${result}" == "termux: https://example.com" ]]; then printf '%b\n' " ${GREEN}✓${NC} open_browser detects termux-open-url" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} open_browser should use termux-open-url, got '${result}'" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 14: open_browser detects macOS open @@ -421,10 +435,10 @@ test_shared_common() { ' 2>/dev/null) if [[ "${result}" == "macOS: https://example.com" ]]; then printf '%b\n' " ${GREEN}✓${NC} open_browser detects macOS open" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} open_browser should use macOS open, got '${result}'" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 15: open_browser handles missing browsers gracefully @@ -445,38 +459,38 @@ test_shared_common() { ' 2>&1 >/dev/null) if [[ "${stderr_output}" == *"Please open: https://example.com"* ]]; then printf '%b\n' " ${GREEN}✓${NC} open_browser shows fallback message when browsers unavailable" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} open_browser should show fallback message, got '${stderr_output}'" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 16: get_cloud_init_userdata returns valid YAML result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && get_cloud_init_userdata' 2>/dev/null) if [[ "${result}" == *"#cloud-config"* ]] && [[ "${result}" == *"package_update"* ]]; then printf '%b\n' " ${GREEN}✓${NC} get_cloud_init_userdata returns valid YAML" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} get_cloud_init_userdata should return cloud-init YAML" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 17: get_cloud_init_userdata includes required packages if [[ "${result}" == *"curl"* ]] && [[ "${result}" == *"git"* ]] && [[ "${result}" == *"zsh"* ]]; then printf '%b\n' " ${GREEN}✓${NC} get_cloud_init_userdata includes required packages" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} get_cloud_init_userdata should include curl, git, zsh" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 18: get_cloud_init_userdata includes Bun and Claude installation if [[ "${result}" == *"bun.sh/install"* ]] && [[ "${result}" == *"claude.ai/install"* ]]; then printf '%b\n' " ${GREEN}✓${NC} get_cloud_init_userdata includes Bun and Claude installation" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} get_cloud_init_userdata should include Bun and Claude install" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 19: wait_for_oauth_code returns success when file exists @@ -486,10 +500,10 @@ test_shared_common() { bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && wait_for_oauth_code "'"${code_test_file}"'" 1' >/dev/null 2>&1 || rc=$? if [[ "${rc}" -eq 0 ]]; then printf '%b\n' " ${GREEN}✓${NC} wait_for_oauth_code returns success when file exists" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} wait_for_oauth_code should return 0 when file exists" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 21: wait_for_oauth_code returns failure on timeout @@ -498,10 +512,10 @@ test_shared_common() { bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && wait_for_oauth_code "'"${missing_file}"'" 1' >/dev/null 2>&1 || rc=$? if [[ "${rc}" -ne 0 ]]; then printf '%b\n' " ${GREEN}✓${NC} wait_for_oauth_code returns failure on timeout" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} wait_for_oauth_code should return non-zero on timeout" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 22: cleanup_oauth_session removes directory @@ -510,10 +524,10 @@ test_shared_common() { bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && cleanup_oauth_session "" "'"${cleanup_test_dir}"'"' >/dev/null 2>&1 if [[ ! -d "${cleanup_test_dir}" ]]; then printf '%b\n' " ${GREEN}✓${NC} cleanup_oauth_session removes directory" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} cleanup_oauth_session should remove directory" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 23: generic_ssh_wait succeeds when command passes @@ -527,10 +541,10 @@ test_shared_common() { ' 2>/dev/null | tail -1) if [[ "${result}" == "0" ]]; then printf '%b\n' " ${GREEN}✓${NC} generic_ssh_wait succeeds when command passes" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} generic_ssh_wait should return 0 on success, got '${result}'" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 24: generic_ssh_wait fails after max attempts @@ -544,10 +558,10 @@ test_shared_common() { ' 2>/dev/null | tail -1) if [[ "${result}" == "1" ]]; then printf '%b\n' " ${GREEN}✓${NC} generic_ssh_wait fails after max attempts" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} generic_ssh_wait should return 1 after max attempts, got '${result}'" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 25: safe_read fails when no TTY available @@ -555,30 +569,30 @@ test_shared_common() { timeout 5 bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && safe_read "test: "' /dev/null 2>&1 || rc=$? if [[ "${rc}" -ne 0 ]]; then printf '%b\n' " ${GREEN}✓${NC} safe_read fails when no TTY available" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} safe_read should fail without TTY" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 26: validate_model_id accepts openrouter/auto result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_model_id "openrouter/auto" && echo "valid"' 2>/dev/null) if [[ "${result}" == "valid" ]]; then printf '%b\n' " ${GREEN}✓${NC} validate_model_id accepts openrouter/auto" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} validate_model_id should accept 'openrouter/auto'" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 27: validate_model_id accepts model IDs with colons result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_model_id "provider/model:version" && echo "valid"' 2>/dev/null) if [[ "${result}" == "valid" ]]; then printf '%b\n' " ${GREEN}✓${NC} validate_model_id accepts model IDs with colons" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} validate_model_id should accept colons in model IDs" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 28: validate_model_id rejects shell metacharacters @@ -591,24 +605,24 @@ test_shared_common() { local test_str test_str=$(printf 'bad%smodel' "${char}") bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_model_id '"$(printf '%q' "${test_str}")" /dev/null 2>&1 || rc=$? - [[ "${rc}" -ne 0 ]] && ((rejected_count++)) + [[ "${rc}" -ne 0 ]] && rejected_count=$((rejected_count + 1)) done if [[ "${rejected_count}" -eq "${#dangerous_chars[@]}" ]]; then printf '%b\n' " ${GREEN}✓${NC} validate_model_id rejects shell metacharacters" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} validate_model_id should reject all shell metacharacters (${rejected_count}/${#dangerous_chars[@]})" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 29: validate_server_name accepts valid names result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_server_name "dev-server-01" && echo "valid"' 2>/dev/null) if [[ "${result}" == "valid" ]]; then printf '%b\n' " ${GREEN}✓${NC} validate_server_name accepts valid names" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} validate_server_name should accept 'dev-server-01'" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 30: validate_server_name rejects names too short @@ -616,10 +630,10 @@ test_shared_common() { bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_server_name "ab"' /dev/null 2>&1 || rc=$? if [[ "${rc}" -ne 0 ]]; then printf '%b\n' " ${GREEN}✓${NC} validate_server_name rejects names too short" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} validate_server_name should reject names < 3 characters" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 31: validate_server_name rejects names too long @@ -629,10 +643,10 @@ test_shared_common() { bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_server_name "'"${long_name}"'"' /dev/null 2>&1 || rc=$? if [[ "${rc}" -ne 0 ]]; then printf '%b\n' " ${GREEN}✓${NC} validate_server_name rejects names too long" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} validate_server_name should reject names > 63 characters" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 32: validate_server_name rejects leading dash @@ -640,10 +654,10 @@ test_shared_common() { bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_server_name "-server"' /dev/null 2>&1 || rc=$? if [[ "${rc}" -ne 0 ]]; then printf '%b\n' " ${GREEN}✓${NC} validate_server_name rejects leading dash" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} validate_server_name should reject names starting with dash" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 33: validate_server_name rejects trailing dash @@ -651,10 +665,10 @@ test_shared_common() { bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_server_name "server-"' /dev/null 2>&1 || rc=$? if [[ "${rc}" -ne 0 ]]; then printf '%b\n' " ${GREEN}✓${NC} validate_server_name rejects trailing dash" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} validate_server_name should reject names ending with dash" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 34: validate_server_name rejects invalid characters @@ -662,10 +676,10 @@ test_shared_common() { bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_server_name "server_01"' /dev/null 2>&1 || rc=$? if [[ "${rc}" -ne 0 ]]; then printf '%b\n' " ${GREEN}✓${NC} validate_server_name rejects invalid characters" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} validate_server_name should reject underscore and special characters" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 35: validate_server_name rejects empty string @@ -673,10 +687,10 @@ test_shared_common() { bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && validate_server_name ""' /dev/null 2>&1 || rc=$? if [[ "${rc}" -ne 0 ]]; then printf '%b\n' " ${GREEN}✓${NC} validate_server_name rejects empty string" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} validate_server_name should reject empty string" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Test 36: check_openrouter_connectivity succeeds with real network @@ -685,10 +699,10 @@ test_shared_common() { result=$(bash -c 'source "'"${REPO_ROOT}"'/shared/common.sh" && check_openrouter_connectivity && echo "reachable"' 2>/dev/null) if [[ "${result}" == "reachable" ]] || [[ -z "${result}" ]]; then printf '%b\n' " ${GREEN}✓${NC} check_openrouter_connectivity handles connectivity check" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} check_openrouter_connectivity should return success or failure gracefully" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi else printf '%b\n' " ${YELLOW}⚠${NC} check_openrouter_connectivity test skipped (curl not available)" @@ -707,19 +721,19 @@ test_source_detection() { # Verify the source block checks for local file existence if grep -q 'if \[\[ -f "${SCRIPT_DIR}/lib/common.sh" \]\]' "${script_path}"; then printf '%b\n' " ${GREEN}✓${NC} ${script}.sh uses file-existence check for sourcing" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} ${script}.sh missing file-existence source check" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi # Verify syntax if bash -n "${script_path}" 2>/dev/null; then printf '%b\n' " ${GREEN}✓${NC} ${script}.sh syntax valid" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${RED}✗${NC} ${script}.sh syntax error" - ((FAILED++)) + FAILED=$((FAILED + 1)) fi done } @@ -757,7 +771,7 @@ run_shellcheck() { for script in "${all_scripts[@]}"; do [[ -f "${script}" ]] || continue - ((checked_count++)) + checked_count=$((checked_count + 1)) # Run shellcheck with warning severity, exclude some noisy checks # SC1090: Can't follow non-constant source @@ -766,7 +780,7 @@ run_shellcheck() { output=$(shellcheck --severity=warning --exclude=SC1090,SC2312 "${script}" 2>&1) if [[ -n "${output}" ]]; then - ((issue_count++)) + issue_count=$((issue_count + 1)) printf '%b\n' " ${YELLOW}⚠${NC} $(basename "${script}"): found issues" echo "${output}" | sed 's/^/ /' fi @@ -774,7 +788,7 @@ run_shellcheck() { if [[ "${issue_count}" -eq 0 ]]; then printf '%b\n' " ${GREEN}✓${NC} No issues found in ${checked_count} scripts" - ((PASSED++)) + PASSED=$((PASSED + 1)) else printf '%b\n' " ${YELLOW}⚠${NC} Found issues in ${issue_count}/${checked_count} scripts (advisory only)" # Don't fail the build, just warn