mirror of
https://github.com/prdlk/vortex.git
synced 2026-08-02 09:21:40 +00:00
Bootstrap now enables each bridge's provisioning API (persisted shared secret, matrix-token auth) and publishes /shared/bridges.json. The TUI bridge screen shows per-bridge login state and launches a step-driven wizard: flow picker, sequential input prompts (phone/cookies/password), in-terminal QR rendering with long-poll wait, cancel support. Discord (legacy bridge, no v3 API) keeps the bot-DM path via 'm'. verify.sh gains a provisioning API smoke check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
116 lines
4.8 KiB
Bash
116 lines
4.8 KiB
Bash
#!/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/5 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/5 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/5 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/5 bridge provisioning API (guided TUI login)"
|
|
docker compose exec -T mcp python - "$TOKEN" <<'EOF' || fail "provisioning API"
|
|
import json, sys, urllib.request, urllib.parse
|
|
tok = sys.argv[1]
|
|
bridges = json.load(open("/shared/bridges.json"))
|
|
net, info = next((n, i) for n, i in bridges.items() if i["kind"] == "v2")
|
|
url = f"{info['base_url']}/_matrix/provision/v3/login/flows?user_id=%40admin%3Alocalhost"
|
|
req = urllib.request.Request(url, headers={"Authorization": "Bearer " + tok})
|
|
flows = json.load(urllib.request.urlopen(req))["flows"]
|
|
assert flows, "no login flows"
|
|
print(f"{net} login flows: {[f['id'] for f in flows]}")
|
|
EOF
|
|
pass "provisioning API reachable with matrix-token auth"
|
|
|
|
echo "== 5/5 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"
|