#!/usr/bin/env bash # 2026-03-21 — bug_hunter.sh: охота за багами во всех POSTGRES-функциях. # Цель: найти bugs типа "ложный 200", "должен 500 но 200", неверные данные. # Логика принципиально отличается от chaos_marathon — здесь акцент на семантике и data integrity. # Запускать ТОЛЬКО на VM через SSH. set -euo pipefail BASE_URL="${BASE_URL:-https://sless.kube5s.ru}" NAMESPACE="${NAMESPACE:-sless-ffd1f598c169b0ae}" TOKEN_FILE="${TOKEN_FILE:-$HOME/terra/sless/test.token}" TOKEN=$(cat "$TOKEN_FILE") RED="\033[0;31m"; GREEN="\033[0;32m"; YELLOW="\033[1;33m"; CYAN="\033[0;36m"; NC="\033[0m" PASS=0; FAIL=0; TOTAL=0 BUGS_FOUND=() ts() { date '+%H:%M:%S'; } # ── Хелперы ────────────────────────────────────────────────────────────────── raw() { local svc="$1" payload="$2" curl -sf -X POST -H "Content-Type: application/json" \ -d "$payload" "${BASE_URL}/fn/${NAMESPACE}/${svc}" 2>/dev/null || echo "__CURL_FAIL__" } http_code() { local svc="$1" payload="$2" curl -s -o /dev/null -w "%{http_code}" -X POST -H "Content-Type: application/json" \ -d "$payload" "${BASE_URL}/fn/${NAMESPACE}/${svc}" 2>/dev/null || echo "000" } # Проверяем что HTTP-код РАВЕН ожидаемому check_http() { local label="$1" got="$2" want="$3" TOTAL=$((TOTAL+1)) if [[ "$got" == "$want" ]]; then echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label" PASS=$((PASS+1)) else echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (got HTTP $got, want HTTP $want)" FAIL=$((FAIL+1)) BUGS_FOUND+=("$label → HTTP $got ≠ $want") fi } # Проверяем что JSON содержит строку check_has() { local label="$1" body="$2" substr="$3" TOTAL=$((TOTAL+1)) if echo "$body" | grep -q "$substr" 2>/dev/null; then echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label" PASS=$((PASS+1)) else echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (key/substr '$substr' not found in: ${body:0:120})" FAIL=$((FAIL+1)) BUGS_FOUND+=("$label → '$substr' not in response") fi } # Проверяем что JSON НЕ содержит строку check_not() { local label="$1" body="$2" substr="$3" TOTAL=$((TOTAL+1)) if echo "$body" | grep -q "$substr" 2>/dev/null; then echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (found '$substr' but should NOT be there: ${body:0:120})" FAIL=$((FAIL+1)) BUGS_FOUND+=("$label → '$substr' should NOT be in response") else echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label" PASS=$((PASS+1)) fi } # Проверяем числовое равенство: jq вытаскивает значение и сравниваем check_val() { local label="$1" body="$2" jq_expr="$3" want="$4" local got got=$(echo "$body" | python3 -c " import json,sys try: d=json.load(sys.stdin) expr='$jq_expr'.lstrip('.') parts=expr.split('.') val=d for p in parts: val=val[p] if isinstance(val,dict) else val[int(p)] print(val) except Exception as e: print('__ERR__:'+str(e)) " 2>/dev/null || echo "__ERR__") TOTAL=$((TOTAL+1)) if [[ "$got" == "$want" ]]; then echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label" PASS=$((PASS+1)) else echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (got '$got', want '$want')" FAIL=$((FAIL+1)) BUGS_FOUND+=("$label → got '$got' want '$want'") fi } # Проверяем что числовое значение >= порога check_gte() { local label="$1" got="$2" min_val="$3" TOTAL=$((TOTAL+1)) if [[ "$got" =~ ^[0-9]+$ ]] && (( got >= min_val )); then echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] $label (got $got >= $min_val)" PASS=$((PASS+1)) else echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] $label (got '$got', want >= $min_val)" FAIL=$((FAIL+1)) BUGS_FOUND+=("$label → $got < $min_val") fi } section() { echo "" echo -e "${CYAN}════════════════════════════════════════════════════════════${NC}" echo -e "${CYAN} $1${NC}" echo -e "${CYAN}════════════════════════════════════════════════════════════${NC}" } echo -e "${YELLOW}" echo "╔══════════════════════════════════════════════════════════╗" echo "║ BUG HUNTER — $(date '+%Y-%m-%d %H:%M:%S') ║" echo "║ Ищем: ложные 200, неверные данные, скрытые баги ║" echo "╚══════════════════════════════════════════════════════════╝" echo -e "${NC}" # ═══════════════════════════════════════════════════════════════════════════════ section "БЛОК 1 — Тест на Missing Input Validation (должно быть 200, НО БУДЕТ 500 если баг есть)" # ═══════════════════════════════════════════════════════════════════════════════ echo " Каждая функция должна СТОЙКО обрабатывать строку вместо числа." echo " Если возвращает 500 — это BUG: не хватает try/except." c=$(http_code "chaos-slowquery" '{"sleep_sec": "не_число"}') check_http "BUG1: chaos-slowquery sleep_sec=string → должен 200" "$c" "200" c=$(http_code "chaos-bigpayload" '{"size_kb": "не_число"}') check_http "BUG2: chaos-bigpayload size_kb=string → должен 200" "$c" "200" c=$(http_code "py-retry-writer" '{"n": "не_число"}') check_http "BUG3: py-retry-writer n=string → должен 200" "$c" "200" c=$(http_code "pg-delete-old" '{"older_than_min": "не_число"}') check_http "BUG4: pg-delete-old older_than_min=string → должен 200" "$c" "200" c=$(http_code "chaos-slowquery" '{"sleep_sec": null}') check_http "BUG5: chaos-slowquery sleep_sec=null → должен 200" "$c" "200" c=$(http_code "chaos-bigpayload" '{"size_kb": null}') check_http "BUG6: chaos-bigpayload size_kb=null → должен 200" "$c" "200" c=$(http_code "chaos-bigpayload" '{"size_kb": -999}') check_http "BUG7: chaos-bigpayload size_kb=-999 (negative) → должен 200" "$c" "200" c=$(http_code "chaos-slowquery" '{"sleep_sec": -5}') check_http "BUG8: chaos-slowquery sleep_sec=-5 → должен 200" "$c" "200" c=$(http_code "chaos-slowquery" '{"sleep_sec": 99999}') check_http "BUG9: chaos-slowquery sleep_sec=99999 (huge) → должен 200" "$c" "200" c=$(http_code "py-retry-writer" '{"n": -1}') check_http "BUG10: py-retry-writer n=-1 (negative) → должен 200" "$c" "200" c=$(http_code "py-retry-writer" '{"n": 99999}') check_http "BUG11: py-retry-writer n=99999 (huge, capped) → должен 200" "$c" "200" # ═══════════════════════════════════════════════════════════════════════════════ section "БЛОК 2 — Data Integrity: реальное значение vs ожидаемое" # ═══════════════════════════════════════════════════════════════════════════════ # js-pg-batch: n=0 должен вернуть inserted=0, а не 20 (баг: 0||20=20) r=$(raw "js-pg-batch" '{"n": 0, "prefix": "bughunt-zero"}') check_val "BUG12: js-pg-batch n=0 → inserted должен быть 0" "$r" ".inserted" "0" # js-pg-batch: n=1 → ровно 1 строка r=$(raw "js-pg-batch" '{"n": 1, "prefix": "bughunt-one"}') check_val "SAFE: js-pg-batch n=1 → inserted=1" "$r" ".inserted" "1" # js-pg-batch: n=200 (cap) → не больше 200 r=$(raw "js-pg-batch" '{"n": 9999, "prefix": "bughunt-cap"}') i=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin)['inserted'])" 2>/dev/null || echo "-1") check_gte "SAFE: js-pg-batch n=9999 → capped (inserted > 0)" "$i" 1 # inserted должно быть <= 200 TOTAL=$((TOTAL+1)) if (( i <= 200 )); then echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: js-pg-batch n=9999 → capped <= 200 (got $i)" PASS=$((PASS+1)) else echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG: js-pg-batch n=9999 → вставил $i строк (ожидали <= 200)" FAIL=$((FAIL+1)) BUGS_FOUND+=("js-pg-batch n=9999 → inserted=$i > 200") fi # pg-bulk-insert: точное соответствие n → inserted for n_val in 1 10 100 499 500; do r=$(raw "pg-bulk-insert" "{\"n\": $n_val, \"prefix\": \"bughunt-n$n_val\"}") check_val "SAFE: pg-bulk-insert n=$n_val → inserted=$n_val" "$r" ".inserted" "$n_val" done # py-retry-writer с simulate_error: должен вернуть ok:true (retry работает) r=$(raw "py-retry-writer" '{"n": 5, "simulate_error": true, "prefix": "bughunt-retry"}') check_has "SAFE: py-retry-writer simulate_error=true → ok:true" "$r" '"ok": true' attempts=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('attempts',0))" 2>/dev/null || echo "0") check_gte "SAFE: py-retry-writer simulate_error=true → attempts >= 2" "$attempts" 2 # ═══════════════════════════════════════════════════════════════════════════════ section "БЛОК 3 — Semantic / Logic Bugs (ложный 200, неверные данные)" # ═══════════════════════════════════════════════════════════════════════════════ # BUG: pg-search с "_" в query — _ это LIKE wildcard, не экранируется. # Вставляем уникальную строку без подчёркивания, ищем с _ → не должна найтись. UNIQUE_NOUNDERSCORE="bughunt-no-underscore-$(date +%s%N)" raw "pg-bulk-insert" "{\"n\": 1, \"prefix\": \"$UNIQUE_NOUNDERSCORE\"}" >/dev/null # Поиск exact строки — должна найтись r=$(raw "pg-search" "{\"query\": \"$UNIQUE_NOUNDERSCORE\", \"limit\": 10}") cnt=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "0") check_gte "SAFE: pg-search exact match найдена" "$cnt" 1 # Теперь создаём строку с дефисом в mid позиции: "aXb" где X — любой char. # Ищем с _ (wildcard): "a_b" должно совпадать. Это НЕ баг если мы ищем wildcard. # Реальный баг: ESCAPE не поддерживается, пользователь не может искать literal "_". # Проверяем: строка "test-literal-underscore_here" ищем по query="literal_underscore_here" # Без экраниравания: _ = любой символ → совпадёт AND с "literaXunderscoreYhere" тоже. EXACT_TITLE="bughunt-exact-$(date +%s%N)" raw "pg-upsert" "{\"title\": \"${EXACT_TITLE}\"}" >/dev/null # Поиск с подчёркиванием вместо дефиса в этой строке — совпадать НЕ должно с точным матчем # но совпадёт из-за ILIKE wildcard `_` UNDER_QUERY="${EXACT_TITLE//-/_}" # заменяем дефисы на подчёркивания r=$(raw "pg-search" "{\"query\": \"$UNDER_QUERY\", \"limit\": 100}") underscore_count=$(echo "$r" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('count',0))" 2>/dev/null || echo "0") # Если count >> 1, значит _ матчит что попало (wildcard) echo " pg-search с query='${UNDER_QUERY:0:30}...' вернул $underscore_count строк (если > 1 — это баг wildcard)" TOTAL=$((TOTAL+1)) # Должен найти ТОЛЬКО нашу строку (count=1), но без экранирования найдёт много if (( underscore_count == 1 )); then echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-search underscore matches only exact row" PASS=$((PASS+1)) else echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG13: pg-search '_' is unescaped LIKE wildcard → matches $underscore_count rows instead of 1" FAIL=$((FAIL+1)) BUGS_FOUND+=("pg-search: '_' is unescaped LIKE wildcard (got $underscore_count rows)") fi # BUG: pg-delete-old — параметр НАЗЫВАЕТСЯ older_than_min, но в chaos_marathon шлём older_than_minutes. # Шлём неправильный ключ и правильный — результаты должны различаться. # older_than_minutes=1 → функция игнорирует, использует default (60 мин) → ничего не удалится из только что вставленных # older_than_min=0 → capped to 1 → удалит строки старше 1 мин (ну, только что вставленные > 1 мин назад) # Проверяем: older_than_minutes=0 → использует default 60 → параметр проигнорирован → bug # Вставляем свежую строку, пытаемся удалить с older_than_minutes=0 (неверный ключ) FRESH_TITLE="bughunt-del-$(date +%s%N)" ins_r=$(raw "pg-bulk-insert" "{\"n\": 3, \"prefix\": \"$FRESH_TITLE\"}") # Ждём немного, затем пытаемся удалить по неправильному ключу sleep 1 r=$(raw "pg-delete-old" "{\"older_than_minutes\": 0}") deleted_wrong_key=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('deleted',0))" 2>/dev/null || echo "?") r2=$(raw "pg-delete-old" "{\"older_than_min\": 99999}") deleted_right_key=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('deleted',0))" 2>/dev/null || echo "?") echo " pg-delete-old older_than_minutes=0: deleted=$deleted_wrong_key (использовался default 60min)" echo " pg-delete-old older_than_min=99999: deleted=$deleted_right_key (правильный ключ — удалило всё старое)" # Если с неверным ключом deleted > 0 при очень маленьком значении — значит параметр работает. # Если с right key deleted больше — это подтверждает что wrong key игнорировался. TOTAL=$((TOTAL+1)) if [[ "$deleted_right_key" =~ ^[0-9]+$ ]] && (( deleted_right_key >= 0 )); then echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] INFO: pg-delete-old right key works (deleted=$deleted_right_key)" PASS=$((PASS+1)) else echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] pg-delete-old right key failed: got $deleted_right_key" FAIL=$((FAIL+1)) fi # BUG: pg-counter prefix="%" → LIKE "%%%" → считает все строки (wildcard leak) r_all=$(raw "pg-counter" '{}') total_all=$(echo "$r_all" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "0") r_pct=$(raw "pg-counter" '{"prefix": "%"}') total_pct=$(echo "$r_pct" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "0") echo " pg-counter prefix='': total=$total_all | pg-counter prefix='%': total=$total_pct" TOTAL=$((TOTAL+1)) # Если оба возвращают одно число — значит % матчит все строки → баг wildcard if [[ "$total_all" == "$total_pct" ]] && (( total_all > 0 )); then echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG14: pg-counter prefix='%' counts ALL rows ($total_pct) — % is unescaped LIKE wildcard" FAIL=$((FAIL+1)) BUGS_FOUND+=("pg-counter: prefix='%' is unescaped LIKE wildcard (same as no prefix)") else echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-counter prefix='%' gives different count than no-prefix" PASS=$((PASS+1)) fi # BUG: pg-search limit=0 — clamp max(1, min(0,100)) = 1, не 0. # Юзер просит 0 строк но получает 1. r=$(raw "pg-search" '{"query": "", "limit": 0}') limit_got=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('limit',0))" 2>/dev/null || echo "-1") echo " pg-search limit=0 → вернул limit=$limit_got в ответе" TOTAL=$((TOTAL+1)) if [[ "$limit_got" == "0" ]]; then echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-search limit=0 → limit=0 in response" PASS=$((PASS+1)) else echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] INFO15: pg-search limit=0 → silently changed to $limit_got (min clamp = 1, not 0)" FAIL=$((FAIL+1)) BUGS_FOUND+=("pg-search: limit=0 silently becomes $limit_got (user wants 0 rows, gets $limit_got)") fi # ═══════════════════════════════════════════════════════════════════════════════ section "БЛОК 4 — pg-upsert: idempotency + action field correctness" # ═══════════════════════════════════════════════════════════════════════════════ # Первый вызов → action=inserted UPSERT_KEY="bughunt-upsert-$(date +%s%N)" r1=$(raw "pg-upsert" "{\"title\": \"$UPSERT_KEY\"}") action1=$(echo "$r1" | python3 -c "import json,sys; print(json.load(sys.stdin).get('action','?'))" 2>/dev/null || echo "?") check_val "SAFE: pg-upsert первый вызов → action=inserted" "$r1" ".action" "inserted" # Второй вызов → action=updated r2=$(raw "pg-upsert" "{\"title\": \"$UPSERT_KEY\"}") action2=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('action','?'))" 2>/dev/null || echo "?") check_val "SAFE: pg-upsert второй вызов (same title) → action=updated" "$r2" ".action" "updated" # ID должен совпадать id1=$(echo "$r1" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id','?'))" 2>/dev/null || echo "?1") id2=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('id','?'))" 2>/dev/null || echo "?2") TOTAL=$((TOTAL+1)) if [[ "$id1" == "$id2" ]] && [[ "$id1" != "?" ]]; then echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-upsert same title → same id ($id1)" PASS=$((PASS+1)) else echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG16: pg-upsert same title → different ids ($id1 vs $id2)" FAIL=$((FAIL+1)) BUGS_FOUND+=("pg-upsert: same title yields different ids ($id1 vs $id2)") fi # ═══════════════════════════════════════════════════════════════════════════════ section "БЛОК 5 — js-idempotent: concurrent same key" # ═══════════════════════════════════════════════════════════════════════════════ # 5 параллельных вызовов с одним ключом → должен быть 1 created, 4 existing IDEM_KEY="bughunt-idem-concurrent-$(date +%s%N)" declare -a IDEM_RESULTS=() for i in 1 2 3 4 5; do r=$(raw "js-idempotent" "{\"idempotency_key\": \"$IDEM_KEY\"}") & IDEM_RESULTS+=($!) done wait # Перезапустим последовательно чтобы собрать результаты actions=() for i in 1 2 3 4 5; do r=$(raw "js-idempotent" "{\"idempotency_key\": \"$IDEM_KEY\"}") a=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('action','?'))" 2>/dev/null || echo "?") actions+=("$a") done created_count=0; existing_count=0 for a in "${actions[@]}"; do [[ "$a" == "created" ]] && created_count=$((created_count+1)) [[ "$a" == "existing" ]] && existing_count=$((existing_count+1)) done echo " js-idempotent key же 5× последовательно: created=$created_count, existing=$existing_count" TOTAL=$((TOTAL+1)) # Первый должен быть created, остальные existing (с учётом что первый вызов в параллельном блоке уже создал) # Теперь все 5 последовательных должны быть existing if (( existing_count == 5 )); then echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: js-idempotent 5× same key → все existing" PASS=$((PASS+1)) elif (( created_count == 1 && existing_count == 4 )); then echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: js-idempotent 5× same key → 1 created + 4 existing" PASS=$((PASS+1)) else echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG17: js-idempotent same key → created=$created_count existing=$existing_count (ожидали 0-1 created, 4-5 existing)" FAIL=$((FAIL+1)) BUGS_FOUND+=("js-idempotent: 5× same key → created=$created_count, existing=$existing_count") fi # ═══════════════════════════════════════════════════════════════════════════════ section "БЛОК 6 — go-pg-race: workers=0 div-by-zero" # ═══════════════════════════════════════════════════════════════════════════════ r=$(raw "go-pg-race" '{"workers": 0, "n_per_worker": 5}') c=$(http_code "go-pg-race" '{"workers": 0, "n_per_worker": 5}') check_http "SAFE: go-pg-race workers=0 → 200 (не div-by-zero)" "$c" "200" # ops_per_sec при workers=0 inserted=0 должен быть 0 или Inf — проверяем что не NaN/invalid JSON TOTAL=$((TOTAL+1)) if echo "$r" | python3 -c "import json,sys; json.load(sys.stdin); print('valid_json')" 2>/dev/null | grep -q valid_json; then echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: go-pg-race workers=0 → valid JSON" PASS=$((PASS+1)) else echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG18: go-pg-race workers=0 → invalid JSON (div-by-zero → Inf/NaN)" FAIL=$((FAIL+1)) BUGS_FOUND+=("go-pg-race: workers=0 → invalid JSON response") fi # ═══════════════════════════════════════════════════════════════════════════════ section "БЛОК 7 — Crash functions: параметры управляют crash-ом" # ═══════════════════════════════════════════════════════════════════════════════ # stress-go-nil: crash=false → 200 (не должен падать) c=$(http_code "stress-go-nil" '{"crash": false}') check_http "SAFE: stress-go-nil crash=false → 200" "$c" "200" # stress-go-nil: crash=true (default) → 500 c=$(http_code "stress-go-nil" '{"crash": true}') check_http "SAFE: stress-go-nil crash=true → 500" "$c" "500" # stress-divzero: d=0 → 500 c=$(http_code "stress-divzero" '{"n": 10, "d": 0}') check_http "SAFE: stress-divzero d=0 → 500" "$c" "500" # stress-divzero: d=2 → 200 c=$(http_code "stress-divzero" '{"n": 10, "d": 2}') check_http "SAFE: stress-divzero d=2 → 200" "$c" "200" r=$(raw "stress-divzero" '{"n": 10, "d": 2}') check_has "SAFE: stress-divzero d=2 → result in response" "$r" "result" # stress-bigloop: n=1000000 (большое) → должен вернуть 200 c=$(http_code "stress-bigloop" '{"n": 1000000}') check_http "SAFE: stress-bigloop n=1000000 → 200" "$c" "200" # stress-go-fast: n=0 → должен вернуть 200 (factorial(0) = 1) c=$(http_code "stress-go-fast" '{"n": 0}') check_http "SAFE: stress-go-fast n=0 → 200" "$c" "200" # stress-go-fast: n=20 (cap) → factorial(20) не переполнение? r=$(raw "stress-go-fast" '{"n": 20}') check_has "SAFE: stress-go-fast n=20 → factorial in response" "$r" "factorial" # stress-go-fast: n=21 → capped to 20 (проверяем что cap работает) r=$(raw "stress-go-fast" '{"n": 21}') n_got=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('n',0))" 2>/dev/null || echo "-1") TOTAL=$((TOTAL+1)) if (( n_got <= 20 )); then echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: stress-go-fast n=21 → capped to $n_got (<= 20)" PASS=$((PASS+1)) else echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG: stress-go-fast n=21 → not capped (n=$n_got)" FAIL=$((FAIL+1)) BUGS_FOUND+=("stress-go-fast: n=21 not capped (got n=$n_got)") fi # ═══════════════════════════════════════════════════════════════════════════════ section "БЛОК 8 — pg-counter: счёт соответствует реально inserted" # ═══════════════════════════════════════════════════════════════════════════════ PREFIX_TEST="bughunt-count-$(date +%s%N)" # Получаем текущий счётчик с этим prefix r_before=$(raw "pg-counter" "{\"prefix\": \"$PREFIX_TEST\"}") cnt_before=$(echo "$r_before" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "0") # Вставляем ровно 7 строк raw "pg-bulk-insert" "{\"n\": 7, \"prefix\": \"$PREFIX_TEST\"}" >/dev/null # Считаем снова r_after=$(raw "pg-counter" "{\"prefix\": \"$PREFIX_TEST\"}") cnt_after=$(echo "$r_after" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "0") inserted_delta=$(( cnt_after - cnt_before )) echo " pg-counter prefix='$PREFIX_TEST': before=$cnt_before, after=$cnt_after, delta=$inserted_delta (ожидаем 7)" TOTAL=$((TOTAL+1)) if (( inserted_delta == 7 )); then echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-counter правильно считает: delta=$inserted_delta" PASS=$((PASS+1)) else echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG19: pg-counter delta=$inserted_delta ≠ 7 (prefix wildcard или баг в счёте)" FAIL=$((FAIL+1)) BUGS_FOUND+=("pg-counter: inserted 7, delta=$inserted_delta") fi # ═══════════════════════════════════════════════════════════════════════════════ section "БЛОК 9 — pg-search: pagination correctness" # ═══════════════════════════════════════════════════════════════════════════════ PREFIX_SEARCH="bughunt-search-$(date +%s%N)" # Вставляем ровно 25 строк raw "pg-bulk-insert" "{\"n\": 25, \"prefix\": \"$PREFIX_SEARCH\"}" >/dev/null sleep 0.5 # Page 1: offset=0 limit=10 → count=10 r=$(raw "pg-search" "{\"query\": \"$PREFIX_SEARCH\", \"limit\": 10, \"offset\": 0}") count_p1=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "-1") total_p1=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('total',0))" 2>/dev/null || echo "-1") check_val "SAFE: pg-search page1 count=10" "$r" ".count" "10" check_gte "SAFE: pg-search total >= 25" "$total_p1" 25 # Page 3: offset=20 limit=10 → count=5 (строк 21-25) r=$(raw "pg-search" "{\"query\": \"$PREFIX_SEARCH\", \"limit\": 10, \"offset\": 20}") count_p3=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "-1") echo " pg-search page3 (offset=20, limit=10): count=$count_p3 (ожидаем 5)" TOTAL=$((TOTAL+1)) # Учитываем что могут быть другие строки с этим prefix if [[ "$count_p3" =~ ^[0-9]+$ ]] && (( count_p3 > 0 && count_p3 <= 10 )); then echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-search page3 count=$count_p3 (допустимо)" PASS=$((PASS+1)) else echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG20: pg-search page3 count=$count_p3 (expected 1-10)" FAIL=$((FAIL+1)) BUGS_FOUND+=("pg-search: page3 count=$count_p3 out of range") fi # offset > total → count=0 r=$(raw "pg-search" "{\"query\": \"$PREFIX_SEARCH\", \"limit\": 10, \"offset\": 999999}") count_over=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "-1") check_val "SAFE: pg-search offset>total → count=0" "$r" ".count" "0" # ═══════════════════════════════════════════════════════════════════════════════ section "БЛОК 10 — pg-dedup: idempotency (повторный вызов безопасен)" # ═══════════════════════════════════════════════════════════════════════════════ # Сначала удаляем все дубли raw "pg-dedup" '{"dry_run": false}' >/dev/null # dry_run=true → deleted всегда = 0 r=$(raw "pg-dedup" '{"dry_run": true}') check_val "SAFE: pg-dedup dry_run=true → deleted=0" "$r" ".deleted" "0" # Создаём дублей: вставляем одно и то же через bulk (уникальные title), затем уpsert одно и то же DUP_TITLE="bughunt-dup-$(date +%s%N)" raw "pg-upsert" "{\"title\": \"${DUP_TITLE}\"}" >/dev/null # INSERT прямой дубль через bulk — но у него нет механизма вставки дублей... # Используем pg-counter + pg-search чтобы найти дубли что уже есть от других тестов r=$(raw "pg-dedup" '{"dry_run": true}') dupes=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('duplicates_found',0))" 2>/dev/null || echo "0") echo " pg-dedup dry_run: duplicates_found=$dupes" # Запускаем dedup дважды — второй раз должен найти 0 дублей raw "pg-dedup" '{"dry_run": false}' >/dev/null r2=$(raw "pg-dedup" '{"dry_run": false}') dupes2=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('duplicates_found',0))" 2>/dev/null || echo "0") TOTAL=$((TOTAL+1)) if [[ "$dupes2" == "0" ]]; then echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-dedup повторный вызов → duplicates_found=0 (идемпотентен)" PASS=$((PASS+1)) else echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG21: pg-dedup повторный вызов → duplicates_found=$dupes2 ≠ 0" FAIL=$((FAIL+1)) BUGS_FOUND+=("pg-dedup: не идемпотентен — второй вызов нашёл $dupes2 дублей") fi # ═══════════════════════════════════════════════════════════════════════════════ section "БЛОК 11 — chaos-echo: крайние случаи" # ═══════════════════════════════════════════════════════════════════════════════ # Пустой JSON {} → echo должен вернуть echo:{}, keys:[], size_bytes:2 r=$(raw "chaos-echo" '{}') check_has "SAFE: chaos-echo {} → echo in response" "$r" '"echo"' check_val "SAFE: chaos-echo {} → keys=[](empty_list len=0)" "$r" ".size_bytes" "2" # Очень глубоко вложенный JSON r=$(raw "chaos-echo" '{"a": {"b": {"c": {"d": {"e": "deep"}}}}}') c=$(http_code "chaos-echo" '{"a": {"b": {"c": {"d": {"e": "deep"}}}}}') check_http "SAFE: chaos-echo deeply nested → 200" "$c" "200" # Массив вместо объекта (некоторые функции падают) c=$(http_code "chaos-echo" '[1, 2, 3]') check_http "SAFE: chaos-echo array input → 200" "$c" "200" # Булево значение вместо объекта c=$(http_code "chaos-echo" 'true') check_http "SAFE: chaos-echo true input → 200" "$c" "200" # Число вместо объекта c=$(http_code "chaos-echo" '42') check_http "SAFE: chaos-echo number input → 200" "$c" "200" # ═══════════════════════════════════════════════════════════════════════════════ section "БЛОК 12 — go-counter-atomic: invocation_n растёт" # ═══════════════════════════════════════════════════════════════════════════════ r1=$(raw "go-counter-atomic" '{}') n1=$(echo "$r1" | python3 -c "import json,sys; print(json.load(sys.stdin).get('invocation_n','?'))" 2>/dev/null || echo "?") r2=$(raw "go-counter-atomic" '{}') n2=$(echo "$r2" | python3 -c "import json,sys; print(json.load(sys.stdin).get('invocation_n','?'))" 2>/dev/null || echo "?") echo " go-counter-atomic: call1 invocation_n=$n1, call2 invocation_n=$n2" TOTAL=$((TOTAL+1)) if [[ "$n1" =~ ^[0-9]+$ ]] && [[ "$n2" =~ ^[0-9]+$ ]] && (( n2 > n1 )); then echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: go-counter-atomic invocation_n растёт ($n1 → $n2)" PASS=$((PASS+1)) else echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG22: go-counter-atomic invocation_n НЕ растёт ($n1 → $n2)" FAIL=$((FAIL+1)) BUGS_FOUND+=("go-counter-atomic: invocation_n не растёт ($n1 → $n2)") fi # ═══════════════════════════════════════════════════════════════════════════════ section "БЛОК 13 — go-pg-race: все inserted = workers × n_per_worker" # ═══════════════════════════════════════════════════════════════════════════════ r=$(raw "go-pg-race" '{"workers": 4, "n_per_worker": 10}') ins=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('inserted',0))" 2>/dev/null || echo "0") errs=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('errors',0))" 2>/dev/null || echo "-1") echo " go-pg-race workers=4 n_per_worker=10: inserted=$ins, errors=$errs (ожидаем inserted=40, errors=0)" TOTAL=$((TOTAL+1)) if [[ "$ins" == "40" ]] && [[ "$errs" == "0" ]]; then echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: go-pg-race 4×10 = 40 inserted, 0 errors" PASS=$((PASS+1)) else echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG23: go-pg-race 4×10: inserted=$ins (≠40), errors=$errs" FAIL=$((FAIL+1)) BUGS_FOUND+=("go-pg-race: workers=4 n_per_worker=10 → inserted=$ins errors=$errs") fi # ═══════════════════════════════════════════════════════════════════════════════ section "БЛОК 14 — pg-bulk-insert: first_id реально существует в БД" # ═══════════════════════════════════════════════════════════════════════════════ PREFIX_VER="bughunt-verify-$(date +%s%N)" r=$(raw "pg-bulk-insert" "{\"n\": 5, \"prefix\": \"$PREFIX_VER\"}") first_id=$(echo "$r" | python3 -c "import json,sys; print(json.load(sys.stdin).get('first_id','null'))" 2>/dev/null || echo "null") echo " pg-bulk-insert n=5 → first_id=$first_id" # Проверяем что эта строка находится через pg-search sleep 0.3 r_search=$(raw "pg-search" "{\"query\": \"$PREFIX_VER\", \"limit\": 10}") found_count=$(echo "$r_search" | python3 -c "import json,sys; print(json.load(sys.stdin).get('count',0))" 2>/dev/null || echo "0") TOTAL=$((TOTAL+1)) if (( found_count >= 5 )); then echo -e "$(ts) ${GREEN}PASS${NC} [$TOTAL] SAFE: pg-bulk-insert n=5 → pg-search находит $found_count строк" PASS=$((PASS+1)) else echo -e "$(ts) ${RED}FAIL${NC} [$TOTAL] BUG24: pg-bulk-insert n=5 → pg-search нашёл только $found_count строк" FAIL=$((FAIL+1)) BUGS_FOUND+=("pg-bulk-insert: inserted 5 but pg-search found only $found_count") fi # ═══════════════════════════════════════════════════════════════════════════════ section "ИТОГИ BUG HUNTER" # ═══════════════════════════════════════════════════════════════════════════════ echo "" echo -e "${YELLOW}PASS: $PASS / $TOTAL FAIL: $FAIL${NC}" echo "" if (( ${#BUGS_FOUND[@]} > 0 )); then echo -e "${RED}╔══════════════════════════════════════════════════════════════╗${NC}" echo -e "${RED}║ НАЙДЕНО БАГОВ: ${#BUGS_FOUND[@]} ║${NC}" echo -e "${RED}╚══════════════════════════════════════════════════════════════╝${NC}" for bug in "${BUGS_FOUND[@]}"; do echo -e " ${RED}✗${NC} $bug" done else echo -e "${GREEN}╔══════════════════════════════════════╗${NC}" echo -e "${GREEN}║ БАГОВ НЕ НАЙДЕНО. Всё чисто. ✓ ║${NC}" echo -e "${GREEN}╚══════════════════════════════════════╝${NC}" fi echo "" exit $(( FAIL > 0 ? 1 : 0 ))