Решение по TD-STYLE-ANALYZERS: LF — инструменты проекта (Python/Node) пишут LF, CRLF-.sh не работают на Linux CI (sh scripts/ci.sh), большинство файлов уже были LF. Добавлен .gitattributes (* text=auto eol=lf, бинарные исключения), .editorconfig переведён на lf, 1029 файлов конвертированы, git add --renormalize. Из индекса убраны закравшиеся archive/**/__pycache__/*.pyc.
217 lines
8.9 KiB
Bash
217 lines
8.9 KiB
Bash
#!/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
|