Files
Deal/scripts/dev-smoke.sh
T
Rustam Khalimov 9e07568ddd Инициализировать репозиторий «Дейл»
Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы
ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ,
инструкция пользователя, техдокументация, код-стайл), бэклог,
скрипты развёртывания и архив прототипа LeadRadar.
2026-09-11 02:50:17 +03:00

217 lines
9.1 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env sh
# dev-smoke.sh — сквозной smoke полного dev-стека Дейла (план Task 20, Ruling 12).
#
# Поднимает ВЕСЬ стек deploy/compose.dev.yml (postgres+minio+ml/ai/telegram+core) в сквозном
# gRPC-режиме (Services__*__UseLocal=false заданы в compose) и проверяет:
# 1) health ВСЕХ контейнеров (docker healthchecks: postgres/core/ml/ai/telegram, minio — running);
# 2) auth: login admin/admin (дефолт TenantBootstrapService; при переопределённом пароле — правка);
# 3) /api/tg/status — живой статус через GrpcTelegramClient (idle-форма: telegram-service без сессии);
# 4) POST /api/cards → локальная карточка (планово, c_…);
# 5) POST /api/cards/{id}/trash — обучающий сигнал spam 1.0 → строка MlOutbox;
# 6) флашер MlOutboxFlushScheduler (10 с) → TrainBatch в ml-service: /api/ml/status показывает
# reachable:true, stats.outbox:0 и класс "spam" в модели сервиса (learned ≥ 1).
#
# Живые проверки Telegram-входа (api_id/api_hash/QR) и реальных LLM-вызовов — НЕ выполняются
# (нужны креды; см. техдок §13, «ручные проверки»). Скрипт сам гасит стек: trap → docker compose down
# (volumes БЕЗ -v — данные dev сохраняются между прогонами). Запуск: scripts/dev-smoke.sh
set -u
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
COMPOSE_FILE="deploy/compose.dev.yml"
BASE_URL="http://localhost:5080"
CONTAINERS="deal-postgres deal-minio deal-telegram-service deal-ai-service deal-ml-service deal-core"
UP_TIMEOUT_TICKS=120 # 120 × 5 с = 10 мин максимум на здоровье всех контейнеров
TICK_SECONDS=5
ML_POLL_TICKS=60 # 60 × 5 с = 5 мин на флаш outbox + refresh статуса сервиса
WORK=$(mktemp -d)
JAR="$WORK/cookies.txt"
OUT="$WORK/out.txt"
PASS=0
FAIL=0
FAILED_NAMES=""
check() { # имя, ожидание HTTP-кода, [фрагменты...]
local name="$1" code="$2"
shift 2
if grep -q "\[HTTP:$code\]" "$OUT"; then
for frag in "$@"; do
if ! grep -qF "$frag" "$OUT"; then
echo " [FAIL] $name (нет фрагмента: $frag)"
cat "$OUT"
FAIL=$((FAIL + 1))
FAILED_NAMES="$FAILED_NAMES|$name"
return
fi
done
echo " [PASS] $name"
PASS=$((PASS + 1))
else
echo " [FAIL] $name (ожидался HTTP $code)"
cat "$OUT"
FAIL=$((FAIL + 1))
FAILED_NAMES="$FAILED_NAMES|$name"
fi
}
container_ok() { # имя → 0: running и (если есть healthcheck) healthy; 1: плох; 2: нет контейнера
local name="$1" state health
state=$(docker inspect -f '{{.State.Status}}' "$name" 2>/dev/null) || return 2
[ "$state" = "running" ] || return 1
health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$name" 2>/dev/null)
[ "$health" = "none" ] || [ "$health" = "healthy" ] || return 1
return 0
}
cleanup() {
echo
echo "== очистка: docker compose down (volumes сохраняются) =="
(cd "$REPO_DIR" && docker compose -f "$COMPOSE_FILE" down) 2>/dev/null || true
rm -rf "$WORK"
echo "== очистка завершена =="
}
trap cleanup EXIT INT TERM
if ! docker info >/dev/null 2>&1; then
echo "Docker engine недоступен (Docker Desktop выключен?) — запустите Docker и повторите smoke."
exit 1
fi
cd "$REPO_DIR" || exit 1
echo "== 0. compose config валиден =="
if docker compose -f "$COMPOSE_FILE" config --quiet; then
echo " [PASS] config"
PASS=$((PASS + 1))
else
echo " [FAIL] docker compose config (файл невалиден)"
exit 1
fi
echo
echo "== 1. подъём стека (первый прогон собирает 4 docker-образа — долго) =="
docker compose -f "$COMPOSE_FILE" up -d --build || {
echo " [FAIL] docker compose up (см. вывод выше)"
exit 1
}
echo " [ok] docker compose up -d --build завершён"
echo
echo "== 2. ожидание health всех контейнеров =="
ALL_OK=""
i=0
while [ $i -lt $UP_TIMEOUT_TICKS ]; do
ALL_OK=1
for c in $CONTAINERS; do
if ! container_ok "$c"; then ALL_OK=""; break; fi
done
[ -n "$ALL_OK" ] && break
i=$((i + 1))
sleep $TICK_SECONDS
done
for c in $CONTAINERS; do
if container_ok "$c"; then
echo " [PASS] $c готов"
PASS=$((PASS + 1))
else
echo " [FAIL] $c не поднялся/не healthy"
FAIL=$((FAIL + 1))
FAILED_NAMES="$FAILED_NAMES|$c"
fi
done
if [ -z "$ALL_OK" ]; then
echo " подробности:"
docker compose -f "$COMPOSE_FILE" ps
docker compose -f "$COMPOSE_FILE" logs --tail=40 core
exit 1
fi
echo
echo "== 3. HTTP-проверки core :5080 =="
echo
echo "-- 3.1 login admin/admin --"
curl -s -m 10 -w "\n[HTTP:%{http_code}]" -c "$JAR" -X POST "$BASE_URL/api/auth/login" \
-H "Content-Type: application/json" -d '{"login":"admin","password":"admin"}' > "$OUT"
check "login → 200 ok:true" 200 '"ok":true'
echo
echo "-- 3.2 GET /api/tg/status — живой статус через GrpcTelegramClient (telegram-service без сессии) --"
curl -s -m 15 -w "\n[HTTP:%{http_code}]" -b "$JAR" "$BASE_URL/api/tg/status" > "$OUT"
# Структурная проверка полей §4.9 (точные значения phase/monitored/keysSet зависят от состояния
# volume БД: fresh-стек ожидает idle/monitored:0/keysSet:false; после реального Telegram-входа может
# быть ready и ненулевой monitored). Живость gRPC-стека доказывают health контейнеров и флашер ML.
check "status — idle-форма/поля §4.9 (200)" 200 \
'"phase":"' '"connected":' '"monitored":' '"keysSet":'
echo
echo "-- 3.3 GET /api/containers?space=dashboard → реестр колонок --"
curl -s -m 15 -w "\n[HTTP:%{http_code}]" -b "$JAR" "$BASE_URL/api/containers?space=dashboard" > "$OUT"
check "containers dashboard → 200, есть inbox" 200 '"id":"inbox"'
echo
echo "-- 3.4 POST /api/cards → локальная карточка (c_…, planned) --"
curl -s -m 20 -w "\n[HTTP:%{http_code}]" -b "$JAR" -X POST "$BASE_URL/api/cards" \
-H "Content-Type: application/json" \
-d '{"title":"SMOKE card","containerId":"planned"}' > "$OUT"
check "create card → 200, карточка c_… в planned" 200 '"containerId":"planned"'
CARD_ID=$(sed -n 's/.*"id":"\(c_[a-zA-Z0-9_]*\)".*/\1/p' "$OUT" | head -1)
if [ -z "$CARD_ID" ]; then
echo " [FAIL] id карточки не извлечён из ответа create card"
FAIL=$((FAIL + 1))
FAILED_NAMES="$FAILED_NAMES|create card id"
else
echo " карточка: $CARD_ID"
fi
echo
echo "-- 3.4a GET /api/cards?containerId=planned → карточка в стадии --"
if [ -n "$CARD_ID" ]; then
curl -s -m 15 -w "\n[HTTP:%{http_code}]" -b "$JAR" "$BASE_URL/api/cards?containerId=planned" > "$OUT"
check "list planned → 200, карточка найдена" 200 "\"id\":\"$CARD_ID\""
fi
echo
echo "-- 3.5 POST /api/cards/$CARD_ID/trash → обучающий сигнал spam (MlOutbox) --"
if [ -n "$CARD_ID" ]; then
curl -s -m 20 -w "\n[HTTP:%{http_code}]" -b "$JAR" -X POST "$BASE_URL/api/cards/$CARD_ID/trash" > "$OUT"
check "trash → 200 ok:true" 200 '"ok":true'
fi
echo
echo "-- 3.6 флашер MlOutbox: ждём TrainBatch → ml-service (outbox:0 + класс spam в модели) --"
FLUSHED=""
i=0
while [ $i -lt $ML_POLL_TICKS ]; do
curl -s -m 15 -w "\n[HTTP:%{http_code}]" -b "$JAR" "$BASE_URL/api/ml/status" > "$OUT"
# reachable — живой Status RPC ml-service; outbox — локальная очередь (убывает после успешного
# TrainBatch); "spam" — класс в модели сервиса (появляется после рефреша кэша статуса, 15 с).
if grep -q '"reachable":true' "$OUT" && grep -q '"outbox":0' "$OUT" && grep -qF '"spam":' "$OUT"; then
FLUSHED=1
break
fi
i=$((i + 1))
sleep $TICK_SECONDS
done
if [ -n "$FLUSHED" ]; then
echo " [PASS] ML-флашер: outbox выгружен в ml-service (класс spam обучен)"
PASS=$((PASS + 1))
cat "$OUT"
else
echo " [FAIL] ML-флашер: outbox не выгружен / ml-service недоступен за отведённое время"
cat "$OUT"
FAIL=$((FAIL + 1))
FAILED_NAMES="$FAILED_NAMES|ml flusher"
docker compose -f "$COMPOSE_FILE" logs --tail=40 core | grep -i "флашер\|outbox\|TrainBatch" || true
fi
echo
echo "== ИТОГ: PASS=$PASS FAIL=$FAIL =="
if [ "$FAIL" = "0" ]; then
echo "SMOKE ПРОЙДЕН — полный dev-стек работает по gRPC"
exit 0
fi
echo "Провалы:$FAILED_NAMES"
exit 1