#!/bin/bash # Smoke tests: stack health, appservice round-trip, MCP tools + draft gating. set -euo pipefail cd "$(dirname "$0")/.." set -a; source .env; set +a HS=http://localhost:8008 pass() { echo "PASS: $*"; } fail() { echo "FAIL: $*"; exit 1; } echo "== 1/4 containers" docker compose ps --format '{{.Service}} {{.State}} {{.Health}}' | tee /tmp/vortex-ps.txt for svc in postgres synapse mcp; do grep -q "^$svc running" /tmp/vortex-ps.txt || fail "$svc not running" done IFS=',' read -ra BRIDGES <<< "${BRIDGES_ENABLED:-}" for b in "${BRIDGES[@]}"; do grep -q "^$b running" /tmp/vortex-ps.txt || fail "bridge $b not running" done for oneshot in bootstrap provision; do st=$(docker compose ps -a --format '{{.Service}} {{.ExitCode}}' $oneshot | awk '{print $2}') [ "$st" = 0 ] || fail "$oneshot exit code $st" done pass "all services up, one-shots exited 0" echo "== 2/4 synapse health + login" curl -sf $HS/health >/dev/null || fail "synapse /health" TOKEN=$(curl -sf -X POST $HS/_matrix/client/v3/login -d "{ \"type\":\"m.login.password\", \"identifier\":{\"type\":\"m.id.user\",\"user\":\"$MATRIX_USER\"}, \"password\":\"$MATRIX_PASSWORD\",\"initial_device_display_name\":\"verify\"}" | python3 -c 'import json,sys;print(json.load(sys.stdin)["access_token"])') [ -n "$TOKEN" ] || fail "login" pass "synapse healthy, admin login works" echo "== 3/4 bridge bot round-trip" FIRST_BRIDGE=$(echo "${BRIDGES_ENABLED}" | cut -d, -f1) BOT="@${FIRST_BRIDGE}bot:${MATRIX_SERVER_NAME}" python3 - "$HS" "$TOKEN" "$BOT" <<'EOF' || fail "bridge bot did not respond" import json, sys, time, urllib.request hs, token, bot = sys.argv[1:4] def api(method, path, body=None): req = urllib.request.Request(hs + path, method=method, data=json.dumps(body).encode() if body is not None else None, headers={"Authorization": "Bearer " + token}) return json.load(urllib.request.urlopen(req)) room = api("POST", "/_matrix/client/v3/createRoom", {"invite": [bot], "is_direct": True, "preset": "trusted_private_chat"})["room_id"] api("PUT", f"/_matrix/client/v3/rooms/{room}/send/m.room.message/{time.time_ns()}", {"msgtype": "m.text", "body": "help"}) deadline = time.time() + 30 since = None while time.time() < deadline: q = f"/_matrix/client/v3/sync?timeout=5000" + (f"&since={since}" if since else "") resp = api("GET", q) since = resp["next_batch"] events = resp.get("rooms", {}).get("join", {}).get(room, {}).get("timeline", {}).get("events", []) for e in events: if e["sender"] == bot and e["type"] == "m.room.message": print("bot replied:", e["content"]["body"][:80].replace("\n", " ")) sys.exit(0) sys.exit(1) EOF pass "appservice round-trip via $BOT" echo "== 4/4 MCP tools + draft gating" docker compose exec -T mcp python - <<'EOF' || fail "mcp checks" import asyncio, json, os from fastmcp import Client async def main(): port = os.environ.get("MCP_HTTP_PORT", "8765") async with Client(f"http://localhost:{port}/mcp") as c: tools = {t.name for t in await c.list_tools()} assert {"list_rooms", "read_messages", "create_draft", "send_draft"} <= tools, tools rooms = (await c.call_tool("list_rooms")).data assert isinstance(rooms, list), rooms print(f"list_rooms: {len(rooms)} rooms") room_id = rooms[0]["room_id"] if rooms else "!dummy:localhost" d = (await c.call_tool("create_draft", {"room_id": room_id, "body": "verify draft"})).data did = d["draft_id"] drafts = (await c.call_tool("list_drafts")).data assert any(x["draft_id"] == did for x in drafts) try: await c.call_tool("send_draft", {"draft_id": did}) raise SystemExit("send_draft should have been blocked") except Exception as e: assert "disabled" in str(e).lower() or "human" in str(e).lower(), e print("send_draft correctly blocked:", str(e)[:80]) await c.call_tool("discard_draft", {"draft_id": did}) print("draft lifecycle OK") asyncio.run(main()) EOF pass "mcp list_rooms + draft lifecycle + send gating" echo echo "ALL CHECKS PASSED"