name: Docker Integration Test on: workflow_dispatch: concurrency: group: docker-integration-${{ github.ref }} cancel-in-progress: true jobs: laravel-to-ai-services: runs-on: ubuntu-latest timeout-minutes: 90 steps: - name: Checkout uses: actions/checkout@v4 with: lfs: true - name: Pull and verify Git LFS files run: | git lfs version git lfs pull git lfs checkout git lfs ls-files python - <<'PY' from pathlib import Path paths = [ "ai_apps/generation/bundle/models/affinity/config.pkl", "ai_apps/generation/bundle/models/affinity/model.pt", "ai_apps/generation/bundle/models/generator/egfr_generator.chkpt", "ai_apps/generation/bundle/models/generator/reinvent.prior", ] for p in paths: path = Path(p) if not path.exists(): raise SystemExit(f"Missing required artifact: {p}") data = path.read_bytes()[:80] size = path.stat().st_size print(f"{p}: {size} bytes") if data.startswith(b"version https://git-lfs.github.com"): raise SystemExit(f"{p} is still a Git LFS pointer file") PY - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build and start stack run: | # Clean up volumes from previous runs docker compose down -v 2>/dev/null || true docker builder prune -af || true docker system df COMPOSE_PARALLEL_LIMIT=1 docker compose build docker system df docker compose up -d --wait # Run fresh migrations to ensure clean database state docker compose exec -T laravel php artisan migrate:fresh --force - name: Seed verified test user run: | echo "=== Creating verified test user via PHP script ===" docker compose exec -T laravel php -r " require 'vendor/autoload.php'; \$app = require_once 'bootstrap/app.php'; \$kernel = \$app->make(Illuminate\Contracts\Console\Kernel::class); \$kernel->bootstrap(); use App\Models\User; use Illuminate\Support\Facades\Hash; \$user = User::firstOrCreate( ['email' => 'salehyasmeen080@gmail.com'], [ 'name' => 'Test User', 'password' => Hash::make('123456789'), 'role' => 'normal', 'is_verified' => true, 'email_verified_at' => now(), 'created_at' => now(), 'updated_at' => now() ] ); echo 'User ID: ' . \$user->id . PHP_EOL; echo 'Email: ' . \$user->email . PHP_EOL; echo 'Verified: ' . (\$user->is_verified ? 'YES' : 'NO') . PHP_EOL; " - name: Login and get Bearer token run: | echo "POST /api/user/login" HTTP_CODE=$(curl -s -o login.json -w "%{http_code}" -X POST http://localhost:8080/api/user/login \ -H "Content-Type: application/json" \ -d '{"email":"salehyasmeen080@gmail.com","password":"123456789"}') echo "HTTP Status: $HTTP_CODE" cat login.json echo "" if [ "$HTTP_CODE" != "200" ]; then echo "Login failed with HTTP $HTTP_CODE" docker compose logs laravel exit 1 fi python3 - <<'PY' import json, sys try: data = json.load(open("login.json")) except json.JSONDecodeError as e: print("Invalid JSON response:", e) with open("login.json") as f: print("Raw response:", f.read()) sys.exit(1) if not data.get("success"): print("Login failed:", json.dumps(data, indent=2)) sys.exit(1) token = data.get("data", {}).get("token") if not token: print("No token in response:", json.dumps(data, indent=2)) sys.exit(1) with open("token.txt", "w") as f: f.write(token) print("Login OK - token saved") PY - name: Show service status if: always() run: docker compose ps -a - name: Wait for Laravel API run: | for i in $(seq 1 60); do if curl -sf http://localhost:8080/ > /dev/null 2>&1; then echo "Laravel is up" exit 0 fi echo "Waiting for Laravel ($i/60)..." sleep 5 done docker compose logs laravel exit 1 - name: Wait for ADMET Service to be Healthy run: | echo "Waiting for ADMET service to be healthy..." for i in $(seq 1 120); do if curl -sf http://localhost:8002/health > /dev/null 2>&1; then echo "ADMET service is healthy and responding on port 8002" echo "Waiting additional 10 seconds for service to fully initialize..." sleep 10 exit 0 fi if docker exec ailixir-admet python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" > /dev/null 2>&1; then echo "ADMET service is healthy internally inside container" echo "Waiting additional 10 seconds for service to fully initialize..." sleep 10 exit 0 fi echo "Attempt $i/120: ADMET service not ready yet..." sleep 5 done echo "ADMET service failed to become healthy in time" docker compose logs admet exit 1 - name: Test aggregated AI health via Laravel run: | TOKEN=$(cat token.txt) echo "GET /api/ai-services/health" curl -sf http://localhost:8080/api/ai-services/health \ -H "Authorization: Bearer $TOKEN" | tee health.json python3 - <<'PY' import json, sys data = json.load(open("health.json")) if not data.get("success"): print("AI services health check failed:", json.dumps(data, indent=2)) sys.exit(1) print("All AI services healthy via Laravel") PY - name: Test ADMET prediction via run: | TOKEN=$(cat token.txt) echo "POST /api/admet/predict" HTTP_CODE=$(curl -s -o admet.json -w "%{http_code}" -X POST http://localhost:8080/api/admet/predict \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"smiles": "c1ccccc1, CCO, CCC"}') echo "HTTP Status: $HTTP_CODE" cat admet.json echo "" if [ "$HTTP_CODE" != "200" ]; then echo "ADMET prediction request failed with HTTP $HTTP_CODE" docker compose logs laravel exit 1 fi python3 - <<'PY' import json, sys try: data = json.load(open("admet.json")) except json.JSONDecodeError as e: print("Invalid JSON response:", e) with open("admet.json") as f: print("Raw response:", f.read()) sys.exit(1) if not data.get("success"): print("ADMET prediction test failed:", json.dumps(data, indent=2)) sys.exit(1) results = data.get("data", {}).get("results", []) if len(results) < 3: print(f"Expected at least 3 results, got {len(results)}") sys.exit(1) print(f"ADMET prediction OK - {len(results)} compounds processed") PY - name: Test Chemical RAG proxy via Laravel run: | TOKEN=$(cat token.txt) echo "POST /api/ai-services/test/chemical-search" HTTP_CODE=$(curl -s -o chemical.json -w "%{http_code}" -X POST http://localhost:8080/api/ai-services/test/chemical-search \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"query": "malaria drug"}') echo "HTTP Status: $HTTP_CODE" cat chemical.json echo "" python3 - <<'PY' import json, sys data = json.load(open("chemical.json")) if not data.get("success"): print("Chemical RAG proxy test failed:", json.dumps(data, indent=2)) sys.exit(1) print("Chemical RAG proxy OK") PY - name: Test Drug Repurposing proxy via Laravel run: | TOKEN=$(cat token.txt) echo "GET /api/ai-services/test/drug-repurposing" HTTP_CODE=$(curl -s -o drug.json -w "%{http_code}" http://localhost:8080/api/ai-services/test/drug-repurposing \ -H "Authorization: Bearer $TOKEN") echo "HTTP Status: $HTTP_CODE" cat drug.json echo "" python3 - <<'PY' import json, sys data = json.load(open("drug.json")) if not data.get("success"): print("Drug Repurposing proxy test failed:", json.dumps(data, indent=2)) sys.exit(1) print("Drug Repurposing proxy OK") PY - name: Dump logs on failure if: failure() run: | docker compose logs --no-color - name: Tear down if: always() run: docker compose down -v drug-repurposing-integration-test: runs-on: ubuntu-latest timeout-minutes: 45 steps: - name: Checkout uses: actions/checkout@v4 with: lfs: true - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build and start stack run: | docker compose down -v 2>/dev/null || true docker builder prune -af || true docker system df COMPOSE_PARALLEL_LIMIT=1 docker compose build docker system df docker compose up -d --wait - name: Verify drug-repurposing reachable from Laravel container run: | echo "=== Laravel → drug-repurposing DNS ===" docker compose exec -T laravel getent hosts drug-repurposing echo "=== Health check from Laravel container ===" docker compose exec -T laravel curl -sf http://drug-repurposing:8000/health | tee /tmp/drug-repurposing-health.json python3 - <<'PY' import json, sys data = json.load(open("/tmp/drug-repurposing-health.json")) if data.get("status") != "healthy": print("Expected healthy status:", data) sys.exit(1) print("drug-repurposing reachable from Laravel:", data.get("service")) PY echo "=== DRUG_REPURPOSING_URL in Laravel env ===" docker compose exec -T laravel php -r 'echo "DRUG_REPURPOSING_URL=" . getenv("DRUG_REPURPOSING_URL") . PHP_EOL;' - name: Run drug repurposing integration tests run: | # Match phpunit.xml: sync queue + sqlite so jobs run inside the test process docker compose exec -T \ -e QUEUE_CONNECTION=sync \ laravel php artisan test --filter=DrugRepurposingIntegrationTest - name: Dump logs on failure if: failure() run: | docker compose logs --no-color laravel drug-repurposing queue - name: Tear down if: always() run: docker compose down -v docking-integration-test: runs-on: ubuntu-latest timeout-minutes: 45 steps: - name: Checkout uses: actions/checkout@v4 with: lfs: true - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build and start stack run: | docker compose down -v 2>/dev/null || true docker compose build --build-arg COMPOSER_DEV="" laravel docker compose up -d laravel - name: Verify RDKit, Vina, and Open Babel in container run: | echo "Verifying python3 import rdkit" docker compose exec -T laravel python3 -c "import rdkit; print(rdkit.__version__)" echo "Verifying vina --help" docker compose exec -T laravel vina --help echo "Verifying obabel -V" docker compose exec -T laravel obabel -V - name: Run Laravel Feature Tests run: | docker compose exec -T laravel php artisan test --filter=DockingSubmitTest - name: Tear down if: always() run: docker compose down -v rag-integration-test: runs-on: ubuntu-latest timeout-minutes: 90 steps: - name: Checkout uses: actions/checkout@v4 with: lfs: true - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Build and start stack run: | docker compose down -v 2>/dev/null || true docker builder prune -af || true docker system df COMPOSE_PARALLEL_LIMIT=1 docker compose build docker system df docker compose up -d --wait # Run fresh migrations to ensure clean database state docker compose exec -T laravel php artisan migrate:fresh --force - name: Seed verified test user run: | echo "=== Creating verified test user via PHP script ===" docker compose exec -T laravel php -r " require 'vendor/autoload.php'; \$app = require_once 'bootstrap/app.php'; \$kernel = \$app->make(Illuminate\Contracts\Console\Kernel::class); \$kernel->bootstrap(); use App\Models\User; use Illuminate\Support\Facades\Hash; \$user = User::firstOrCreate( ['email' => 'salehyasmeen080@gmail.com'], [ 'name' => 'Test User', 'password' => Hash::make('123456789'), 'role' => 'normal', 'is_verified' => true, 'email_verified_at' => now(), 'created_at' => now(), 'updated_at' => now() ] ); echo 'User ID: ' . \$user->id . PHP_EOL; echo 'Email: ' . \$user->email . PHP_EOL; echo 'Verified: ' . (\$user->is_verified ? 'YES' : 'NO') . PHP_EOL; " - name: Login and get Bearer token run: | echo "POST /api/user/login" HTTP_CODE=$(curl -s -o login.json -w "%{http_code}" -X POST http://localhost:8080/api/user/login \ -H "Content-Type: application/json" \ -d '{"email":"salehyasmeen080@gmail.com","password":"123456789"}') echo "HTTP Status: $HTTP_CODE" cat login.json echo "" if [ "$HTTP_CODE" != "200" ]; then echo "Login failed with HTTP $HTTP_CODE" docker compose logs laravel exit 1 fi python3 - <<'PY' import json, sys try: data = json.load(open("login.json")) except json.JSONDecodeError as e: print("Invalid JSON response:", e) with open("login.json") as f: print("Raw response:", f.read()) sys.exit(1) if not data.get("success"): print("Login failed:", json.dumps(data, indent=2)) sys.exit(1) token = data.get("data", {}).get("token") if not token: print("No token in response:", json.dumps(data, indent=2)) sys.exit(1) with open("token.txt", "w") as f: f.write(token) print("Login OK - token saved") PY - name: Show service status if: always() run: docker compose ps -a - name: Wait for Laravel API run: | for i in $(seq 1 60); do if curl -sf http://localhost:8080/ > /dev/null 2>&1; then echo "Laravel is up" exit 0 fi echo "Waiting for Laravel ($i/60)..." sleep 5 done docker compose logs laravel exit 1 - name: Test retrieval-only chemical search run: | TOKEN=$(cat token.txt) echo "POST /api/chemical-search (retrieval-only)" HTTP_CODE=$(curl -s -o chemical_search.json -w "%{http_code}" -X POST http://localhost:8080/api/chemical-search \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"smiles":"CC(=O)O"}') echo "HTTP Status: $HTTP_CODE" cat chemical_search.json echo "" python3 - <<'PY' import json, sys data = json.load(open("chemical_search.json")) if not data.get("success"): print("Chemical search (retrieval-only) failed:", json.dumps(data, indent=2)) sys.exit(1) if not isinstance(data.get("compounds"), list): print("Missing 'compounds' array in response") sys.exit(1) if data.get("metadata", {}).get("source") != "retrieval": print("Expected source='retrieval', got:", data.get("metadata", {}).get("source")) sys.exit(1) print("Chemical search (retrieval-only) OK - found", len(data["compounds"]), "compounds") PY - name: Test full RAG chemical search run: | TOKEN=$(cat token.txt) echo "POST /api/chemical-search/full-rag" HTTP_CODE=$(curl -s -o chemical_rag.json -w "%{http_code}" -X POST http://localhost:8080/api/chemical-search/full-rag \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"smiles":"CC(=O)O"}') echo "HTTP Status: $HTTP_CODE" cat chemical_rag.json echo "" python3 - <<'PY' import json, sys data = json.load(open("chemical_rag.json")) if not data.get("success"): print("Chemical search (full RAG) failed:", json.dumps(data, indent=2)) sys.exit(1) if not isinstance(data.get("compounds"), list): print("Missing 'compounds' array in response") sys.exit(1) if data.get("metadata", {}).get("source") != "full_rag": print("Expected source='full_rag', got:", data.get("metadata", {}).get("source")) sys.exit(1) # Verify explanations exist for full RAG for compound in data["compounds"]: if not compound.get("explanation"): print("Missing explanation for compound:", compound.get("name")) sys.exit(1) print("Chemical search (full RAG) OK - found", len(data["compounds"]), "compounds with explanations") PY - name: Dump logs on failure if: failure() run: | docker compose logs --no-color - name: Tear down if: always() run: docker compose down -v