#!/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