eva-agent/scripts/e2e-hub.sh
Andrey Epifancev c711a119b9 eva-agent v2: версии в e2e, документация, решения плана (#7)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:59:56 +04:00

134 lines
7.3 KiB
Bash
Executable File
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 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" <<JSON
{"hub_url": "$HUB", "hub_token": "$AGENT_TOKEN",
"eva": {"base_url": "http://127.0.0.1:$EVA_PORT", "token": "$EVA_TOKEN"},
"verify_ssl": true, "ca_bundle": null, "timeout": 30, "poll_seconds": 0, "auto_update": false}
JSON
agent() { /usr/bin/python3 "$HERE/src/agent.py" --config "$TMP/agent.json" --journal "$TMP/journal.jsonl" "$@"; }
agent check || fail "agent.py check"
# --- проект с корнем вики -> пакет 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:<id>: $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"