#!/usr/bin/env bash # test-local-integration.sh — End-to-end local Matrix/Conduit + Hermes integration test # Issue: #166 # # Spins up a local Conduit instance, registers a test user, and proves the # Hermes Matrix adapter can connect, sync, join rooms, and send messages. # # Usage: # cd infra/matrix # ./scripts/test-local-integration.sh set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" BASE_DIR="$(dirname "$SCRIPT_DIR")" COMPOSE_FILE="$BASE_DIR/docker-compose.test.yml" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' pass() { echo -e "${GREEN}[PASS]${NC} $*"; } fail() { echo -e "${RED}[FAIL]${NC} $*"; } info() { echo -e "${YELLOW}[INFO]${NC} $*"; } # Detect docker compose variant if docker compose version >/dev/null 2>&1; then COMPOSE_CMD="docker compose" elif docker-compose version >/dev/null 2>&1; then COMPOSE_CMD="docker-compose" else fail "Neither 'docker compose' nor 'docker-compose' found" exit 1 fi cleanup() { info "Cleaning up test environment..." $COMPOSE_CMD -f "$COMPOSE_FILE" down -v --remove-orphans 2>/dev/null || true } trap cleanup EXIT info "==================================================" info "Hermes Matrix Local Integration Test" info "Target: #166 | Environment: localhost" info "==================================================" # --- Start test environment --- info "Starting Conduit test environment..." $COMPOSE_CMD -f "$COMPOSE_FILE" up -d # --- Wait for Conduit --- info "Waiting for Conduit to accept connections..." for i in {1..30}; do if curl -sf http://localhost:8448/_matrix/client/versions >/dev/null 2>&1; then pass "Conduit is responding on localhost:8448" break fi sleep 1 done if ! curl -sf http://localhost:8448/_matrix/client/versions >/dev/null 2>&1; then fail "Conduit failed to start within 30 seconds" exit 1 fi # --- Register test user --- TEST_USER="hermes_test_$(date +%s)" TEST_PASS="testpass_$(openssl rand -hex 8)" HOMESERVER="http://localhost:8448" info "Registering test user: $TEST_USER" REG_PAYLOAD=$(cat </dev/null || echo '{}') ACCESS_TOKEN=$(echo "$REG_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('access_token',''))" 2>/dev/null || true) if [[ -z "$ACCESS_TOKEN" ]]; then # Try login if registration failed (user might already exist somehow) info "Registration response missing token, attempting login..." LOGIN_RESPONSE=$(curl -sf -X POST \ -H "Content-Type: application/json" \ -d "{\"type\":\"m.login.password\",\"user\":\"$TEST_USER\",\"password\":\"$TEST_PASS\"}" \ "$HOMESERVER/_matrix/client/v3/login" 2>/dev/null || echo '{}') ACCESS_TOKEN=$(echo "$LOGIN_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('access_token',''))" 2>/dev/null || true) fi if [[ -z "$ACCESS_TOKEN" ]]; then fail "Could not register or login test user" echo "Registration response: $REG_RESPONSE" exit 1 fi pass "Test user authenticated" # --- Create test room --- info "Creating test room..." ROOM_RESPONSE=$(curl -sf -X POST \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -d '{"preset":"public_chat","name":"Hermes Integration Test","topic":"Automated test room"}' \ "$HOMESERVER/_matrix/client/v3/createRoom" 2>/dev/null || echo '{}') ROOM_ID=$(echo "$ROOM_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('room_id',''))" 2>/dev/null || true) if [[ -z "$ROOM_ID" ]]; then fail "Could not create test room" echo "Room response: $ROOM_RESPONSE" exit 1 fi pass "Test room created: $ROOM_ID" # --- Run Hermes-style probe --- info "Running Hermes Matrix adapter probe..." export MATRIX_HOMESERVER="$HOMESERVER" export MATRIX_USER_ID="@$TEST_USER:localhost" export MATRIX_ACCESS_TOKEN="$ACCESS_TOKEN" export MATRIX_TEST_ROOM="$ROOM_ID" export MATRIX_ENCRYPTION="false" python3 <<'PYEOF' import asyncio import os import sys from datetime import datetime, timezone try: from nio import AsyncClient, SyncResponse, RoomSendResponse except ImportError: print("matrix-nio not installed. Installing...") import subprocess subprocess.check_call([sys.executable, "-m", "pip", "install", "--quiet", "matrix-nio"]) from nio import AsyncClient, SyncResponse, RoomSendResponse HOMESERVER = os.getenv("MATRIX_HOMESERVER", "").rstrip("/") USER_ID = os.getenv("MATRIX_USER_ID", "") ACCESS_TOKEN = os.getenv("MATRIX_ACCESS_TOKEN", "") ROOM_ID = os.getenv("MATRIX_TEST_ROOM", "") def ok(msg): print(f"\033[0;32m[PASS]\033[0m {msg}") def err(msg): print(f"\033[0;31m[FAIL]\033[0m {msg}") async def main(): client = AsyncClient(HOMESERVER, USER_ID) client.access_token = ACCESS_TOKEN client.user_id = USER_ID try: whoami = await client.whoami() if hasattr(whoami, "user_id"): ok(f"Whoami authenticated as {whoami.user_id}") else: err(f"Whoami failed: {whoami}") return 1 sync_resp = await client.sync(timeout=10000) if isinstance(sync_resp, SyncResponse): ok(f"Initial sync complete ({len(sync_resp.rooms.join)} joined rooms)") else: err(f"Initial sync failed: {sync_resp}") return 1 test_body = f"🔥 Hermes local integration probe | {datetime.now(timezone.utc).isoformat()}" send_resp = await client.room_send( ROOM_ID, "m.room.message", {"msgtype": "m.text", "body": test_body}, ) if isinstance(send_resp, RoomSendResponse): ok(f"Test message sent (event_id: {send_resp.event_id})") else: err(f"Test message failed: {send_resp}") return 1 ok("All integration checks passed — Hermes Matrix adapter works locally.") return 0 finally: await client.close() sys.exit(asyncio.run(main())) PYEOF PROBE_EXIT=$? if [[ $PROBE_EXIT -eq 0 ]]; then pass "Local integration test PASSED" info "==================================================" info "Result: #166 is execution-ready." info "The only remaining blocker is host/domain (#187)." info "==================================================" else fail "Local integration test FAILED" exit 1 fi