#!/usr/bin/env bash # Сквозной прогон eva-agent с настоящим хабом (модуль wiki) и фейковой Eva. Печатает «e2e: ok». # Хаб — код из HUB_SRC (по умолчанию основной checkout sdlc-hub workspace), ТОЛЬКО с временным SDLC_HUB_HOME и портом # не 8790; живой хаб и его данные не трогаем. set -euo pipefail HERE="$(cd "$(dirname "$0")/.." && pwd)" BRIDGE="$(cd "$(git -C "$HERE" rev-parse --path-format=absolute --git-common-dir)/../.." && pwd)" # workspace jira-bridge (из worktree тоже) HUB_SRC="${HUB_SRC:-$BRIDGE/sdlc-hub}" HUB_SRC="$(cd "$HUB_SRC" && pwd)" PORT="${E2E_PORT:-$(/usr/bin/python3 -c 'import socket; s = socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()')}" [ "$PORT" != "8790" ] || { echo "e2e: порт 8790 занят живым хабом" >&2; exit 2; } TMP="$(mktemp -d)" export SDLC_HUB_HOME="$TMP/hub-home" mkdir -p "$SDLC_HUB_HOME" PIDS=() stop_tree() { # остановить процесс и его детей (uv run запускает хаб дочерним процессом) local p="$1" c for c in $(pgrep -P "$p" 2>/dev/null || true); do stop_tree "$c"; done kill "$p" 2>/dev/null || true } cleanup() { trap - EXIT INT TERM for p in ${PIDS[@]+"${PIDS[@]}"}; do stop_tree "$p"; done for _ in 1 2 3 4 5 6 7 8 9 10; do alive=0 for p in ${PIDS[@]+"${PIDS[@]}"}; do kill -0 "$p" 2>/dev/null && alive=1; done [ "$alive" = 0 ] && break sleep 0.2 done for p in ${PIDS[@]+"${PIDS[@]}"}; do kill -9 "$p" 2>/dev/null || true; done rm -rf "$TMP" } trap cleanup EXIT INT TERM hub() { uv run --project "$HUB_SRC" --no-sync sdlc-hub "$@"; } fail() { echo "e2e: ОШИБКА — $*" >&2; echo "--- лог хаба ---" >&2; tail -n 30 "$TMP/hub.log" >&2 || true; exit 1; } jget() { /usr/bin/python3 -c 'import json,sys; d=json.load(sys.stdin) for k in sys.argv[1].split("."): d = d[int(k)] if isinstance(d, list) else d[k] print(d)' "$1"; } # --- токены (создают базу во временном каталоге) TOKENS="$(hub tokens)" CLIENT_TOKEN="$(printf '%s\n' "$TOKENS" | sed -n 's/^клиентский токен: //p')" AGENT_TOKEN="$(printf '%s\n' "$TOKENS" | sed -n 's/^агент wiki: //p')" [ -n "$CLIENT_TOKEN" ] && [ -n "$AGENT_TOKEN" ] || fail "токены не выданы: $TOKENS" # --- фейковая Eva EVA_TOKEN="eva-e2e-secret" /usr/bin/python3 "$HERE/scripts/fake_eva_serve.py" "$EVA_TOKEN" >"$TMP/eva.out" 2>"$TMP/eva.err" & EVA_PID=$! PIDS+=($EVA_PID) for _ in $(seq 1 50); do grep -q '^PORT ' "$TMP/eva.out" 2>/dev/null && break; sleep 0.1; done EVA_PORT="$(sed -n 's/^PORT //p' "$TMP/eva.out")" [ -n "$EVA_PORT" ] || fail "фейковая Eva не поднялась: $(cat "$TMP/eva.err")" # --- хаб hub serve --host 127.0.0.1 --port "$PORT" >"$TMP/hub.log" 2>&1 & PIDS+=($!) HUB="http://127.0.0.1:$PORT" for _ in $(seq 1 100); do nc -z 127.0.0.1 "$PORT" 2>/dev/null && break kill -0 "${PIDS[1]}" 2>/dev/null || fail "хаб завершился при старте" sleep 0.1 done nc -z 127.0.0.1 "$PORT" 2>/dev/null || fail "хаб не открыл порт $PORT" api() { # api METHOD PATH [json] local m="$1" p="$2"; shift 2 if [ $# -gt 0 ]; then curl -fsS -X "$m" -H "Authorization: Bearer $CLIENT_TOKEN" -H 'Content-Type: application/json' -d "$1" "$HUB$p" else curl -fsS -X "$m" -H "Authorization: Bearer $CLIENT_TOKEN" "$HUB$p" fi } # --- конфиг агента (прямой HTTP к тестовому хабу, без самообновления) cat >"$TMP/agent.json" < пакет wiki_sync api POST /projects '{"label":"E2E","name":"Сквозной прогон","wiki_root":"DOC-1"}' >"$TMP/project.json" || fail "POST /projects" BATCH="$(jget batch_id <"$TMP/project.json")" agent run --once || fail "agent.py run --once (wiki_sync)" STATUS="$(api GET "/wiki/status?project=E2E")" [ "$(printf '%s' "$STATUS" | jget pages)" = "5" ] || fail "wiki/status: ожидалось 5 страниц: $STATUS" TREE="$(api GET "/wiki/tree?project=E2E")" [ "$(printf '%s' "$TREE" | /usr/bin/python3 -c 'import json,sys; print(len(json.load(sys.stdin)["pages"]))')" = "5" ] || fail "wiki/tree: $TREE" PAGE="$(api GET "/wiki/pages/DOC-2?project=E2E")" printf '%s' "$PAGE" | jget markdown | grep -q 'attachment:[0-9]' || fail "в Markdown DOC-2 нет ссылки attachment:: $PAGE" ATT_ID="$(printf '%s' "$PAGE" | jget attachments.0.id)" # --- вложение по запросу -> wiki_fetch api POST "/wiki/attachments/$ATT_ID/fetch" '{}' >/dev/null || fail "POST attachments/$ATT_ID/fetch" agent run --once || fail "agent.py run --once (wiki_fetch)" api GET "/wiki/attachments/$ATT_ID/content" >"$TMP/content.bin" || fail "GET attachments/$ATT_ID/content" EXPECT="$(/usr/bin/python3 -c 'import sys; sys.path.insert(0, sys.argv[1]); import fake_eva_serve as f; sys.stdout.buffer.write(f.PNG)' "$HERE/scripts" | shasum -a 256 | cut -d' ' -f1)" GOT="$(shasum -a 256 <"$TMP/content.bin" | cut -d' ' -f1)" [ "$EXPECT" = "$GOT" ] || fail "содержимое вложения не совпало" # --- история правок: только если тестовый хаб её знает (change wiki-history хаба; P-6) nversions() { /usr/bin/python3 -c 'import json,sys; print(len(json.load(sys.stdin)["versions"]))'; } HAS_HISTORY="$(printf '%s' "$STATUS" | /usr/bin/python3 -c 'import json,sys; print("yes" if "history_at" in json.load(sys.stdin) else "no")')" if [ "$HAS_HISTORY" = "no" ]; then echo "e2e: хаб без истории правок — шаг версий не проверялся" else STATUS="$(api GET "/wiki/status?project=E2E")" [ "$(printf '%s' "$STATUS" | jget history_at)" != "None" ] || fail "wiki/status: history_at пуст: $STATUS" V1="$(api GET "/wiki/pages/DOC-1/versions?project=E2E")" [ "$(printf '%s' "$V1" | nversions)" = "2" ] || fail "версии DOC-1: ожидалось 2: $V1" V2="$(api GET "/wiki/pages/DOC-2/versions?project=E2E")" [ "$(printf '%s' "$V2" | nversions)" = "1" ] || fail "версии DOC-2: ожидалась 1: $V2" kill -USR1 "$EVA_PID" for _ in $(seq 1 50); do grep -q '^VERSION DOCH-14' "$TMP/eva.out" 2>/dev/null && break; sleep 0.1; done grep -q '^VERSION DOCH-14' "$TMP/eva.out" || fail "фейковая Eva не опубликовала DOCH-14" api POST /wiki/sync '{"project":"E2E"}' >/dev/null || fail "POST /wiki/sync" agent run --once || fail "agent.py run --once (догрузка версий)" V1="$(api GET "/wiki/pages/DOC-1/versions?project=E2E")" [ "$(printf '%s' "$V1" | nversions)" = "3" ] || fail "версии DOC-1 после догрузки: ожидалось 3: $V1" [ "$(printf '%s' "$V1" | jget versions.0.version)" = "DOCH-14" ] || fail "новейшая версия DOC-1 не DOCH-14: $V1" fi echo "e2e: ok"