Spaces:
Running
Running
| 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 | |