hugin/scripts/integration-test.sh
2026-10-27 08:33:21 +01:00

137 lines
8 KiB
Bash
Executable file

#!/usr/bin/env bash
# Auslieferung für Integrationen (→ KONZEPT.md 14.7) Ende zu Ende:
# Server starten, Integrations-Key anlegen, Pakete veröffentlichen und
# hugin-install.ps1 gegen ein Test-Repo laufen lassen.
#
# PWSH=/pfad/zu/pwsh scripts/integration-test.sh
#
# Hinweis: Läuft hier mit PowerShell 7. Ziel ist Windows PowerShell 5.1; dort
# einmal von Hand prüfen (siehe README, „Integration testen“).
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PWSH="${PWSH:-pwsh}"
WORK="$(mktemp -d)"
PORT="${PORT:-8766}"
URL="http://127.0.0.1:$PORT"
export HUGIN_DATABASE_URL="sqlite:///$WORK/hugin.db"
export HUGIN_ADMIN_TOKEN="integration-admin"
ADMIN=(-H "Authorization: Bearer $HUGIN_ADMIN_TOKEN")
cleanup() {
local rc=$?
[[ -n "${SERVER:-}" ]] && kill "$SERVER" 2>/dev/null && wait "$SERVER" 2>/dev/null || true
if [[ -z "${KEEP:-}" ]]; then rm -rf "$WORK"; else echo "Arbeitsordner: $WORK"; fi
exit $rc
}
trap cleanup EXIT
api() { curl -fsS -H "Content-Type: application/json" "$@"; }
key() { python3 -c 'import json,sys; print(json.load(sys.stdin)["key"])'; }
(cd "$ROOT/server" && uv run -q hugin-server migrate)
(cd "$ROOT/server" && exec uv run -q hugin-server run --host 127.0.0.1 --port "$PORT") >"$WORK/server.log" 2>&1 &
SERVER=$!
for _ in $(seq 50); do curl -fsS "$URL/readyz" >/dev/null 2>&1 && break; sleep 0.2; done
api "${ADMIN[@]}" "$URL/api/v1/admin/tenants" -d '{"slug":"plattform","name":"Plattform"}' >/dev/null
api "${ADMIN[@]}" "$URL/api/v1/admin/tenants/plattform/projects" -d '{"slug":"kern","name":"Kern"}' >/dev/null
api "${ADMIN[@]}" "$URL/api/v1/admin/tenants" -d '{"slug":"werk","name":"Werk"}' >/dev/null
AUTHOR=$(api "${ADMIN[@]}" "$URL/api/v1/admin/tenants/plattform/keys" -d '{"kind":"project","name":"autor","project":"kern","roles":["pub_author"]}' | key)
REVIEWER=$(api "${ADMIN[@]}" "$URL/api/v1/admin/tenants/plattform/keys" -d '{"kind":"project","name":"pruefer","project":"kern","roles":["pub_reviewer"]}' | key)
INTEGRATION=$(api "${ADMIN[@]}" "$URL/api/v1/admin/tenants/werk/keys" -d '{"kind":"integration","name":"Softwareverteilung"}' | key)
publish() {
local zip="$WORK/package.zip"
rm -f "$zip"; (cd "$ROOT/contract/examples/packages/$1" && zip -qr "$zip" .)
curl -fsS -H "Authorization: Bearer $AUTHOR" -H "Content-Type: application/zip" --data-binary "@$zip" "$URL/api/v1/drafts" >/dev/null
local base="$URL/api/v1/packages/${1%/*}/versions/${1#*/}"
api -H "Authorization: Bearer $AUTHOR" -X POST "$base/submit" >/dev/null
api -H "Authorization: Bearer $REVIEWER" "$base/approve" -d '{}' >/dev/null
}
for p in pub-munin-nutzen/1.4.0 pub-hausregeln/1.0.0 pub-munin-anbindung/1.0.0 pub-code-review/1.0.0; do publish "$p"; done
curl -fsS "${ADMIN[@]}" -X PUT -H "Content-Type: application/json" "$URL/api/v1/admin/mandatory/pub-munin-anbindung" -d '{"tenant":null}'
echo "== Status und Übersicht"
curl -fsS -H "Authorization: Bearer $INTEGRATION" "$URL/api/v1/delivery/status"; echo
curl -fsS -H "Authorization: Bearer $INTEGRATION" "$URL/api/v1/delivery/packages" | python3 -c 'import json,sys; print([(p["id"], p["channels"]) for p in json.load(sys.stdin)])'
code=$(curl -s -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $AUTHOR" "$URL/api/v1/delivery/status")
[[ $code == 403 ]] || { echo "Projekt-Key hätte abgelehnt werden müssen ($code)"; exit 1; }
echo "== Skript laden und ausführen"
curl -fsS -H "Authorization: Bearer $INTEGRATION" "$URL/api/v1/delivery/client.ps1" -o "$WORK/hugin-install.ps1"
REPO="$WORK/repo"; mkdir -p "$REPO"
printf '# Werk\r\n\r\nUnser Projekt.\r\n' > "$REPO/CLAUDE.md"
printf '{\n "model": "anthropic/claude-sonnet-5-5"\n}\n' > "$REPO/opencode.json"
run() {
HUGIN_KEY="$INTEGRATION" "$PWSH" -NoProfile -NonInteractive -File "$WORK/hugin-install.ps1" \
-Server "$URL" -Repo "$REPO" -Tools 'claude=2.1.302,opencode=1.19.3' -Client werk-pc-01 "$@"
}
run -Packages pub-munin-nutzen,pub-hausregeln,pub-code-review
check() { grep -q -- "$2" "$REPO/$1" || { echo "$1 enthält nicht: $2"; cat "$REPO/$1"; exit 1; }; }
check .claude/skills/pub-munin-nutzen/SKILL.md "pub-munin-nutzen 1.4.0"
check .claude/agents/pub-code-review.md "tools: Read, Grep, Glob"
check .opencode/agents/pub-code-review.md "webfetch: deny"
check CLAUDE.md "@.hugin/rules/pub-hausregeln.md"
check .mcp.json '"type": *"http"'
check opencode.json '.hugin/rules/\*.md'
python3 - "$REPO" <<'PY'
import json, sys, pathlib
repo = pathlib.Path(sys.argv[1])
claude = (repo / "CLAUDE.md").read_bytes()
assert claude.startswith(b"# Werk\r\n\r\nUnser Projekt.\r\n\r\n<!-- hugin:begin"), claude[:80]
assert b"\n" not in claude.replace(b"\r\n", b""), "Zeilenenden gemischt"
for f in ["opencode.json", ".mcp.json", "hugin.lock.json"]:
roh = (repo / f).read_bytes()
assert not roh.startswith(b"\xef\xbb\xbf"), f + " mit BOM"
json.loads(roh)
oc = json.loads((repo / "opencode.json").read_text())
assert oc["model"] == "anthropic/claude-sonnet-5-5" and oc["mcp"]["munin"]["type"] == "remote", oc
skill = (repo / ".claude/skills/pub-munin-nutzen/SKILL.md").read_bytes()
assert b"\r\n" not in skill and not skill.startswith(b"\xef\xbb\xbf")
print("Inhalte ok")
PY
echo "== Zweiter Lauf ändert nichts"
run -Packages pub-munin-nutzen,pub-hausregeln,pub-code-review | tee "$WORK/lauf2.txt"
grep -q "Dateien: 0 schreiben, 0 löschen" "$WORK/lauf2.txt" || { echo "zweiter Lauf hätte nichts ändern dürfen"; exit 1; }
echo "== Aktualisierung und Abbestellen"
publish pub-munin-nutzen/1.5.0
run -Packages pub-munin-nutzen
check .claude/skills/pub-munin-nutzen/SKILL.md "pub-munin-nutzen 1.5.0"
[[ ! -e "$REPO/.claude/skills/pub-munin-nutzen/referenz" ]] || { echo "referenz/ nicht entfernt"; exit 1; }
[[ ! -e "$REPO/.claude/agents/pub-code-review.md" ]] || { echo "Subagent nicht entfernt"; exit 1; }
grep -q "hugin:begin" "$REPO/CLAUDE.md" && { echo "Block nicht entfernt"; exit 1; }
cmp -s <(printf '# Werk\r\n\r\nUnser Projekt.\r\n') "$REPO/CLAUDE.md" || { echo "CLAUDE.md nicht wie vorher"; exit 1; }
python3 -c "import json,sys; oc=json.load(open('$REPO/opencode.json')); assert 'instructions' not in oc and 'munin' in oc['mcp'], oc"
echo "== Handänderung wird erkannt"
echo "von Hand" >> "$REPO/.claude/skills/pub-munin-nutzen/SKILL.md"
publish pub-hausregeln/1.0.0 2>/dev/null || true
set +e; run -Packages pub-munin-nutzen,pub-hausregeln > "$WORK/lauf4.txt" 2>&1; RC=$?; set -e
[[ $RC -ne 0 ]] && grep -q "von Hand geändert" "$WORK/lauf4.txt" || { cat "$WORK/lauf4.txt"; echo "Handänderung nicht erkannt"; exit 1; }
[[ ! -e "$REPO/.hugin/rules/pub-hausregeln.md" ]] || { echo "trotz Befund geschrieben"; exit 1; }
run -Packages pub-munin-nutzen,pub-hausregeln -Overwrite >/dev/null
check .hugin/rules/pub-hausregeln.md "Hausregeln"
echo "== JSON mit Kommentaren wird nicht angefasst"
REPO2="$WORK/repo2"; mkdir -p "$REPO2"
printf '{\n // eigenes\n "model": "x"\n}\n' > "$REPO2/opencode.json"
set +e; HUGIN_KEY="$INTEGRATION" "$PWSH" -NoProfile -NonInteractive -File "$WORK/hugin-install.ps1" \
-Server "$URL" -Repo "$REPO2" -Tools 'opencode=1.19.3' -Packages pub-hausregeln > "$WORK/lauf5.txt" 2>&1; RC=$?; set -e
[[ $RC -ne 0 ]] && grep -q "Kommentare" "$WORK/lauf5.txt" || { cat "$WORK/lauf5.txt"; echo "JSONC nicht erkannt"; exit 1; }
grep -q "// eigenes" "$REPO2/opencode.json" || { echo "opencode.json verändert"; exit 1; }
echo "== Kein Schreiben über Links nach draußen"
REPO3="$WORK/repo3"; OUTSIDE="$WORK/draussen"; mkdir -p "$REPO3" "$OUTSIDE"
ln -s "$OUTSIDE" "$REPO3/.opencode"
set +e; HUGIN_KEY="$INTEGRATION" "$PWSH" -NoProfile -NonInteractive -File "$WORK/hugin-install.ps1" \
-Server "$URL" -Repo "$REPO3" -Tools 'opencode=1.19.3' -Packages pub-code-review > "$WORK/lauf6.txt" 2>&1; RC=$?; set -e
[[ $RC -ne 0 ]] && grep -q "Link oder eine Junction" "$WORK/lauf6.txt" || { cat "$WORK/lauf6.txt"; echo "Link nicht erkannt"; exit 1; }
[[ -z "$(ls -A "$OUTSIDE")" ]] || { echo "außerhalb des Repos geschrieben"; ls -R "$OUTSIDE"; exit 1; }
echo "== Nutzung"
sqlite3 "$WORK/hugin.db" "select repo, package_id, version, tools from hugin_usage order by package_id" 2>/dev/null || true
echo "Integrationstest ok."