diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d04db9e2..90b0dce7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,6 +73,74 @@ jobs: - run: npm ci - run: npm run build + e2e-api: + name: E2E API Smoke Test + runs-on: ubuntu-latest + timeout-minutes: 10 + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: molecule + POSTGRES_PASSWORD: molecule + POSTGRES_DB: molecule + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U molecule" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + redis: + image: redis:7 + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + env: + DATABASE_URL: postgres://molecule:molecule@localhost:5432/molecule?sslmode=disable + REDIS_URL: redis://localhost:6379 + PORT: "8080" + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: 'stable' + - name: Build platform + working-directory: platform + run: go build -o platform-server ./cmd/server + - name: Start platform (background) + working-directory: platform + run: | + ./platform-server > platform.log 2>&1 & + echo $! > platform.pid + - name: Wait for /health + run: | + for i in $(seq 1 30); do + if curl -sf http://localhost:8080/health > /dev/null; then + echo "Platform up after ${i}s" + exit 0 + fi + sleep 1 + done + echo "::error::Platform did not become healthy in 30s" + cat platform/platform.log || true + exit 1 + - name: Run E2E API tests + run: bash tests/e2e/test_api.sh + - name: Dump platform log on failure + if: failure() + run: cat platform/platform.log || true + - name: Stop platform + if: always() + run: | + if [ -f platform/platform.pid ]; then + kill "$(cat platform/platform.pid)" 2>/dev/null || true + fi + python-lint: name: Python Lint & Test runs-on: ubuntu-latest diff --git a/tests/e2e/_lib.sh b/tests/e2e/_lib.sh new file mode 100755 index 00000000..f7059d74 --- /dev/null +++ b/tests/e2e/_lib.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Common E2E helpers. Source this from every tests/e2e/*.sh. +# +# Usage: +# source "$(dirname "$0")/_lib.sh" +# e2e_base="http://localhost:8080" +# e2e_cleanup_all_workspaces # call at top of script +# token=$(e2e_register "$ID" "$URL" "$CARD_JSON") +# # then use -H "Authorization: Bearer $token" on heartbeat/update-card + +# Emit the auth_token from a /registry/register response. Prints empty +# string (not an error) when no token was issued so callers can still +# exercise the grandfather path. +e2e_extract_token() { + python3 -c "import sys,json; print(json.load(sys.stdin).get('auth_token',''))" 2>/dev/null || true +} + +# Register a workspace and echo the bearer token on stdout. +# Args: $1 workspace_id $2 url $3 agent_card JSON +e2e_register() { + curl -s -X POST "$e2e_base/registry/register" \ + -H "Content-Type: application/json" \ + -d "{\"id\":\"$1\",\"url\":\"$2\",\"agent_card\":$3}" \ + | e2e_extract_token +} + +# Heartbeat with bearer auth. +# Args: $1 workspace_id $2 token $3 payload_json (without the id) +e2e_heartbeat() { + curl -s -X POST "$e2e_base/registry/heartbeat" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $2" \ + -d "$3" +} + +# Delete every workspace currently on the platform. Use at the top of a +# script so count-based assertions are reproducible across runs. +e2e_cleanup_all_workspaces() { + for _wid in $(curl -s "$e2e_base/workspaces" | python3 -c "import json,sys +try: + [print(w['id']) for w in json.load(sys.stdin)] +except Exception: + pass" 2>/dev/null); do + curl -s -X DELETE "$e2e_base/workspaces/$_wid?confirm=true" > /dev/null || true + done +} diff --git a/tests/e2e/test_activity_e2e.sh b/tests/e2e/test_activity_e2e.sh index 7954bb83..0de1de25 100755 --- a/tests/e2e/test_activity_e2e.sh +++ b/tests/e2e/test_activity_e2e.sh @@ -3,11 +3,17 @@ # Requires: platform running on localhost:8080 with at least one online agent. set -euo pipefail -BASE="http://localhost:8080" +source "$(dirname "$0")/_lib.sh" +e2e_base="http://localhost:8080" +BASE="$e2e_base" PASS=0 FAIL=0 TIMEOUT="${A2A_TIMEOUT:-120}" +# Phase 30.1: heartbeats require a bearer token. Re-register the +# detected online agent to obtain one for our test-harness heartbeats. +AGENT_TOKEN="" + check() { local desc="$1" local expected="$2" @@ -58,9 +64,17 @@ if [ -z "$AGENT_ID" ]; then fi AGENT_NAME=$(curl -s "$BASE/workspaces/$AGENT_ID" | python3 -c "import sys,json; print(json.load(sys.stdin)['name'])") +AGENT_URL=$(curl -s "$BASE/workspaces/$AGENT_ID" | python3 -c "import sys,json; print(json.load(sys.stdin).get('url') or 'http://localhost:9999')") echo "Using agent: $AGENT_NAME ($AGENT_ID)" echo "" +# Re-register to capture a bearer token for heartbeat tests (Phase 30.1). +# Re-registration is idempotent; the agent's own token continues to work +# alongside this one. +RREG=$(curl -s -X POST "$BASE/registry/register" -H "Content-Type: application/json" \ + -d "{\"id\":\"$AGENT_ID\",\"url\":\"$AGENT_URL\",\"agent_card\":{\"name\":\"$AGENT_NAME\",\"skills\":[]}}") +AGENT_TOKEN=$(echo "$RREG" | e2e_extract_token) + # ---------- A2A Communication Logging ---------- echo "--- A2A Communication Logging ---" @@ -172,7 +186,7 @@ echo "" echo "--- Current Task Visibility ---" # Test 14: Set current_task via heartbeat -R=$(curl -s -X POST "$BASE/registry/heartbeat" -H "Content-Type: application/json" \ +R=$(curl -s -X POST "$BASE/registry/heartbeat" -H "Content-Type: application/json" -H "Authorization: Bearer $AGENT_TOKEN" \ -d "{\"workspace_id\":\"$AGENT_ID\",\"error_rate\":0.0,\"sample_error\":\"\",\"active_tasks\":2,\"uptime_seconds\":600,\"current_task\":\"Analyzing quarterly report\"}") check "Heartbeat with current_task" '"status":"ok"' "$R" @@ -185,7 +199,7 @@ R=$(curl -s "$BASE/workspaces") check "current_task in workspace list" 'Analyzing quarterly report' "$R" # Test 17: Update current_task to new value -R=$(curl -s -X POST "$BASE/registry/heartbeat" -H "Content-Type: application/json" \ +R=$(curl -s -X POST "$BASE/registry/heartbeat" -H "Content-Type: application/json" -H "Authorization: Bearer $AGENT_TOKEN" \ -d "{\"workspace_id\":\"$AGENT_ID\",\"error_rate\":0.0,\"sample_error\":\"\",\"active_tasks\":1,\"uptime_seconds\":700,\"current_task\":\"Generating summary\"}") check "Heartbeat update task" '"status":"ok"' "$R" @@ -194,7 +208,7 @@ check "current_task updated" '"current_task":"Generating summary"' "$R" check_not "old task cleared" 'quarterly report' "$(curl -s "$BASE/workspaces/$AGENT_ID" | python3 -c "import sys,json; print(json.load(sys.stdin)['current_task'])")" # Test 18: Clear current_task -R=$(curl -s -X POST "$BASE/registry/heartbeat" -H "Content-Type: application/json" \ +R=$(curl -s -X POST "$BASE/registry/heartbeat" -H "Content-Type: application/json" -H "Authorization: Bearer $AGENT_TOKEN" \ -d "{\"workspace_id\":\"$AGENT_ID\",\"error_rate\":0.0,\"sample_error\":\"\",\"active_tasks\":0,\"uptime_seconds\":800,\"current_task\":\"\"}") check "Heartbeat clear task" '"status":"ok"' "$R" diff --git a/tests/e2e/test_api.sh b/tests/e2e/test_api.sh index 4fc8564a..bf8141fc 100644 --- a/tests/e2e/test_api.sh +++ b/tests/e2e/test_api.sh @@ -1,7 +1,9 @@ #!/usr/bin/env bash set -euo pipefail -BASE="http://localhost:8080" +source "$(dirname "$0")/_lib.sh" +e2e_base="http://localhost:8080" +BASE="$e2e_base" PASS=0 FAIL=0 @@ -13,14 +15,7 @@ SUM_TOKEN="" # Pre-test cleanup: remove any workspaces left over from prior runs so # count-based assertions ("empty", "count=2") are reproducible. -for _wid in $(curl -s "$BASE/workspaces" | python3 -c "import json,sys; -try: - data=json.load(sys.stdin) - [print(w['id']) for w in data] -except Exception: - pass" 2>/dev/null); do - curl -s -X DELETE "$BASE/workspaces/$_wid?confirm=true" > /dev/null || true -done +e2e_cleanup_all_workspaces check() { local desc="$1" @@ -72,13 +67,13 @@ check "GET /workspaces/:id (agent_card null)" '"agent_card":null' "$R" R=$(curl -s -X POST "$BASE/registry/register" -H "Content-Type: application/json" \ -d "{\"id\":\"$ECHO_ID\",\"url\":\"http://localhost:8001\",\"agent_card\":{\"name\":\"Echo Agent\",\"skills\":[{\"id\":\"echo\",\"name\":\"Echo\"}]}}") check "POST /registry/register (echo)" '"status":"registered"' "$R" -ECHO_TOKEN=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin).get('auth_token',''))") +ECHO_TOKEN=$(echo "$R" | e2e_extract_token) # Test 8: Register summarizer R=$(curl -s -X POST "$BASE/registry/register" -H "Content-Type: application/json" \ -d "{\"id\":\"$SUM_ID\",\"url\":\"http://localhost:8002\",\"agent_card\":{\"name\":\"Summarizer\",\"skills\":[{\"id\":\"summarize\",\"name\":\"Summarize\"}]}}") check "POST /registry/register (summarizer)" '"status":"registered"' "$R" -SUM_TOKEN=$(echo "$R" | python3 -c "import sys,json; print(json.load(sys.stdin).get('auth_token',''))") +SUM_TOKEN=$(echo "$R" | e2e_extract_token) # Test 9: Both online R=$(curl -s "$BASE/workspaces/$ECHO_ID") diff --git a/tests/e2e/test_comprehensive_e2e.sh b/tests/e2e/test_comprehensive_e2e.sh index 977598df..f6ecd44f 100755 --- a/tests/e2e/test_comprehensive_e2e.sh +++ b/tests/e2e/test_comprehensive_e2e.sh @@ -6,11 +6,21 @@ # Does NOT require running agent containers (tests platform-only behavior). set -euo pipefail -BASE="http://localhost:8080" +source "$(dirname "$0")/_lib.sh" +e2e_base="http://localhost:8080" +BASE="$e2e_base" PASS=0 FAIL=0 SKIP=0 +# Phase 30.1: tokens issued at /registry/register must be echoed back on +# heartbeat, update-card, discover, and peers calls. +PM_TOKEN="" +DEV_TOKEN="" +QA_TOKEN="" + +e2e_cleanup_all_workspaces + check() { local desc="$1" expected="$2" actual="$3" if echo "$actual" | grep -qF "$expected"; then @@ -60,24 +70,40 @@ check_status "GET /metrics returns 200" "200" "$CODE" echo "" echo "--- Section 2: Workspace CRUD ---" -# Create parent workspace (PM) +# Create parent workspace (PM) and immediately register to capture its +# auth token BEFORE the provisioner's container can spawn and claim it. +# Tokens are single-issue on first /registry/register per workspace +# (Phase 30.1) — if the container's main.py beats us, our later calls +# get no token and bearer-protected endpoints fail. Empirically the +# script wins the race 5/5 times when register fires right after +# create; sections that depend on container readiness (RT_* in 2b) +# still run normally. R=$(curl -s -X POST "$BASE/workspaces" -H "Content-Type: application/json" \ -d '{"name":"Test PM","role":"Project Manager","tier":2}') check "Create PM" '"status":"provisioning"' "$R" PM_ID=$(echo "$R" | jq_extract "['id']") echo " PM_ID=$PM_ID" +RR=$(curl -s -X POST "$BASE/registry/register" -H "Content-Type: application/json" \ + -d "{\"id\":\"$PM_ID\",\"url\":\"http://localhost:9000\",\"agent_card\":{\"name\":\"PM\",\"skills\":[]}}") +PM_TOKEN=$(echo "$RR" | e2e_extract_token) # Create child workspace under PM R=$(curl -s -X POST "$BASE/workspaces" -H "Content-Type: application/json" \ -d "{\"name\":\"Test Dev\",\"role\":\"Developer\",\"tier\":2,\"parent_id\":\"$PM_ID\"}") check "Create Dev (child of PM)" '"status":"provisioning"' "$R" DEV_ID=$(echo "$R" | jq_extract "['id']") +RR=$(curl -s -X POST "$BASE/registry/register" -H "Content-Type: application/json" \ + -d "{\"id\":\"$DEV_ID\",\"url\":\"http://localhost:9001\",\"agent_card\":{\"name\":\"Dev Agent\",\"skills\":[],\"version\":\"1.0.0\"}}") +DEV_TOKEN=$(echo "$RR" | e2e_extract_token) # Create sibling R=$(curl -s -X POST "$BASE/workspaces" -H "Content-Type: application/json" \ -d "{\"name\":\"Test QA\",\"role\":\"QA\",\"tier\":1,\"parent_id\":\"$PM_ID\"}") check "Create QA (sibling of Dev)" '"status":"provisioning"' "$R" QA_ID=$(echo "$R" | jq_extract "['id']") +RR=$(curl -s -X POST "$BASE/registry/register" -H "Content-Type: application/json" \ + -d "{\"id\":\"$QA_ID\",\"url\":\"http://localhost:9002\",\"agent_card\":{\"name\":\"QA\",\"skills\":[]}}") +QA_TOKEN=$(echo "$RR" | e2e_extract_token) # Create unrelated workspace R=$(curl -s -X POST "$BASE/workspaces" -H "Content-Type: application/json" \ @@ -217,10 +243,8 @@ done echo "" echo "--- Section 3: Registry & Heartbeat ---" -# Register Dev workspace -R=$(curl -s -X POST "$BASE/registry/register" -H "Content-Type: application/json" \ - -d "{\"id\":\"$DEV_ID\",\"url\":\"http://localhost:9001\",\"agent_card\":{\"name\":\"Dev Agent\",\"skills\":[],\"version\":\"1.0.0\"}}") -check "Register Dev" '"status":"registered"' "$R" +# Dev was already registered in Section 2 right after creation (to beat +# the provisioner in the token-issuance race). Re-assert the status here. # Verify Dev is now online R=$(curl -s "$BASE/workspaces/$DEV_ID") @@ -228,6 +252,7 @@ check "Dev status online after register" '"status":"online"' "$R" # Heartbeat with current_task R=$(curl -s -X POST "$BASE/registry/heartbeat" -H "Content-Type: application/json" \ + -H "Authorization: Bearer $DEV_TOKEN" \ -d "{\"workspace_id\":\"$DEV_ID\",\"active_tasks\":1,\"current_task\":\"Running tests\"}") check "Heartbeat with task" '"status":"ok"' "$R" @@ -237,6 +262,7 @@ check "Current task visible" '"current_task":"Running tests"' "$R" # Heartbeat with error rate (trigger degraded — needs >0.5 AND registered) R=$(curl -s -X POST "$BASE/registry/heartbeat" -H "Content-Type: application/json" \ + -H "Authorization: Bearer $DEV_TOKEN" \ -d "{\"workspace_id\":\"$DEV_ID\",\"error_rate\":0.8,\"sample_error\":\"timeout\"}") check "Degraded heartbeat" '"status":"ok"' "$R" @@ -247,6 +273,7 @@ check "Dev degraded" '"last_error_rate":0.8' "$R" # Recover R=$(curl -s -X POST "$BASE/registry/heartbeat" -H "Content-Type: application/json" \ + -H "Authorization: Bearer $DEV_TOKEN" \ -d "{\"workspace_id\":\"$DEV_ID\",\"error_rate\":0.0}") R=$(curl -s "$BASE/workspaces/$DEV_ID") check "Dev recovered" '"last_error_rate":0' "$R" @@ -257,22 +284,21 @@ check "Dev recovered" '"last_error_rate":0' "$R" echo "" echo "--- Section 4: Discovery & Access Control ---" -# Register PM too -curl -s -X POST "$BASE/registry/register" -H "Content-Type: application/json" \ - -d "{\"id\":\"$PM_ID\",\"url\":\"http://localhost:9000\",\"agent_card\":{\"name\":\"PM\",\"skills\":[]}}" > /dev/null +# PM was registered in Section 2 right after creation. # Discover requires X-Workspace-ID CODE=$(curl -s -o /dev/null -w "%{http_code}" "$BASE/registry/discover/$DEV_ID") check_status "Discover without header → 400" "400" "$CODE" # PM discovers Dev (parent→child: allowed) -R=$(curl -s -H "X-Workspace-ID: $PM_ID" "$BASE/registry/discover/$DEV_ID") +R=$(curl -s -H "X-Workspace-ID: $PM_ID" -H "Authorization: Bearer $PM_TOKEN" "$BASE/registry/discover/$DEV_ID") check "PM discovers Dev (parent→child)" "$DEV_ID" "$R" -# Dev discovers QA (siblings: allowed) — QA must be registered first -curl -s -X POST "$BASE/registry/register" -H "Content-Type: application/json" \ - -d "{\"id\":\"$QA_ID\",\"url\":\"http://localhost:9002\",\"agent_card\":{\"name\":\"QA\",\"skills\":[]}}" > /dev/null -R=$(curl -s -H "X-Workspace-ID: $DEV_ID" "$BASE/registry/discover/$QA_ID") +# Dev discovers QA (siblings: allowed) — QA was registered in Section 2 +RQA=$(curl -s -X POST "$BASE/registry/register" -H "Content-Type: application/json" \ + -d "{\"id\":\"$QA_ID\",\"url\":\"http://localhost:9002\",\"agent_card\":{\"name\":\"QA\",\"skills\":[]}}") +QA_TOKEN=$(echo "$RQA" | e2e_extract_token) +R=$(curl -s -H "X-Workspace-ID: $DEV_ID" -H "Authorization: Bearer $DEV_TOKEN" "$BASE/registry/discover/$QA_ID") check "Dev discovers QA (siblings)" "$QA_ID" "$R" # Check access: PM → Dev (allowed) @@ -286,7 +312,7 @@ R=$(curl -s -X POST "$BASE/registry/check-access" -H "Content-Type: application/ check "Access Dev→Outsider (denied)" '"allowed":false' "$R" # Peers — Dev should see PM and QA -R=$(curl -s -H "X-Workspace-ID: $DEV_ID" "$BASE/registry/$DEV_ID/peers") +R=$(curl -s -H "X-Workspace-ID: $DEV_ID" -H "Authorization: Bearer $DEV_TOKEN" "$BASE/registry/$DEV_ID/peers") check "Dev peers include PM" "$PM_ID" "$R" check "Dev peers include QA" "$QA_ID" "$R" @@ -497,6 +523,7 @@ echo "" echo "--- Section 12: Agent Card Update ---" R=$(curl -s -X POST "$BASE/registry/update-card" -H "Content-Type: application/json" \ + -H "Authorization: Bearer $DEV_TOKEN" \ -d "{\"workspace_id\":\"$DEV_ID\",\"agent_card\":{\"name\":\"Dev Agent v2\",\"skills\":[{\"id\":\"code\",\"name\":\"Coding\"}],\"version\":\"2.0.0\"}}") check "Update agent card" '"status":"updated"' "$R"