|
| 1 | +#!/usr/bin/env bats |
| 2 | + |
| 3 | +load "lib/container_mesh.bash" |
| 4 | + |
| 5 | +setup() { |
| 6 | + if ! mesh_require_docker; then |
| 7 | + skip "docker not available or daemon not running" |
| 8 | + fi |
| 9 | + |
| 10 | + if [[ ! -x "./bin/sam-node" || ! -x "./bin/sam-hub" || ! -x "./bin/mcp-client" ]]; then |
| 11 | + skip "missing binaries; run: make build" |
| 12 | + fi |
| 13 | + |
| 14 | + mesh_setup_env |
| 15 | +} |
| 16 | + |
| 17 | +teardown() { |
| 18 | + mesh_cleanup_env |
| 19 | + # Cleanup any additional containers started in the test |
| 20 | + docker rm -f http-service sse-client >/dev/null 2>&1 || true |
| 21 | +} |
| 22 | + |
| 23 | +@test "Datapath: HTTP and Stdio services are reachable across nodes" { |
| 24 | + run mesh_start_mock_oidc |
| 25 | + [[ "$status" -eq 0 ]] |
| 26 | + |
| 27 | + # Start Hub |
| 28 | + mesh_start_hub |
| 29 | + local hub_name="${MESH_PREFIX}-hub" |
| 30 | + local hub_peer_id |
| 31 | + hub_peer_id=$(cat "/tmp/${MESH_PREFIX}-hub-peer-id") |
| 32 | + |
| 33 | + # Start Node 1 |
| 34 | + echo "[$(date +%T)] Starting Node 1" |
| 35 | + run mesh_start_node 1 "--discovery-interval 100ms --log-level debug" |
| 36 | + [[ "$status" -eq 0 ]] |
| 37 | + local node1_name="${MESH_PREFIX}-node-1" |
| 38 | + mesh_wait_for_log "${node1_name}" "SAM Node Online" 20 |
| 39 | + mesh_wait_for_mcp_ready 1 20 |
| 40 | + |
| 41 | + local node1_peer_id |
| 42 | + node1_peer_id=$(docker logs "${node1_name}" 2>&1 | grep "PeerID:" | head -n 1 | awk '{print $2}' | tr -d '\r') |
| 43 | + |
| 44 | + # Start Node 2 |
| 45 | + echo "[$(date +%T)] Starting Node 2" |
| 46 | + run mesh_start_node 2 "--discovery-interval 100ms --log-level debug" |
| 47 | + [[ "$status" -eq 0 ]] |
| 48 | + local node2_name="${MESH_PREFIX}-node-2" |
| 49 | + mesh_wait_for_log "${node2_name}" "SAM Node Online" 20 |
| 50 | + mesh_wait_for_mcp_ready 2 20 |
| 51 | + |
| 52 | + local node2_peer_id |
| 53 | + node2_peer_id=$(docker logs "${node2_name}" 2>&1 | grep "PeerID:" | head -n 1 | awk '{print $2}' | tr -d '\r') |
| 54 | + |
| 55 | + # Explicitly connect Node 1 to Node 2 (DHT auto-discovery is slow/unreliable in this E2E setup) |
| 56 | + echo "[$(date +%T)] Explicitly connecting Node 1 to Node 2" |
| 57 | + local node2_addr="/dns4/sam-node-2/tcp/5002/p2p/${node2_peer_id}" |
| 58 | + run docker run --rm --network "${MESH_NETWORK}" -v "$(pwd)/bin/mcp-client:/mcp-client" python:3.12 /mcp-client -url "http://sam-node-1:8080/mcp/events" -tool "connect_peer" -args "{\"peer_addr\":\"${node2_addr}\"}" |
| 59 | + [[ "$status" -eq 0 ]] |
| 60 | + |
| 61 | + # Verify connection |
| 62 | + mesh_wait_for_peer_connection 1 "${node2_peer_id}" 20 |
| 63 | + [[ "$status" -eq 0 ]] |
| 64 | + |
| 65 | + # 1. Setup HTTP Service on Node 1 side |
| 66 | + # Start a dummy HTTP server in a separate container |
| 67 | + echo "[$(date +%T)] Starting dummy HTTP service" |
| 68 | + docker run -d \ |
| 69 | + --name http-service \ |
| 70 | + --network "${MESH_NETWORK}" \ |
| 71 | + python:3.12 python3 -c ' |
| 72 | +from http.server import HTTPServer, BaseHTTPRequestHandler |
| 73 | +class S(BaseHTTPRequestHandler): |
| 74 | + def do_GET(self): |
| 75 | + self.send_response(200) |
| 76 | + self.send_header("Content-type", "application/json") |
| 77 | + self.end_headers() |
| 78 | + self.wfile.write(b"{\"status\":\"success\"}") |
| 79 | +HTTPServer(("0.0.0.0", 8000), S).serve_forever() |
| 80 | +' |
| 81 | + MESH_CONTAINERS+=("http-service") |
| 82 | + |
| 83 | + # Register HTTP service on Node 1 |
| 84 | + echo "[$(date +%T)] Registering HTTP service on Node 1" |
| 85 | + run docker run --rm --network "${MESH_NETWORK}" python:3.12 python3 -c " |
| 86 | +import urllib.request |
| 87 | +import json |
| 88 | +
|
| 89 | +data = { |
| 90 | + \"service\": { |
| 91 | + \"type\": 1, |
| 92 | + \"name\": \"http-tool\", |
| 93 | + \"description\": \"test http service\" |
| 94 | + }, |
| 95 | + \"targetUrl\": \"http://http-service:8000\" |
| 96 | +} |
| 97 | +
|
| 98 | +req = urllib.request.Request( |
| 99 | + \"http://sam-node-1:8080/sam/service/register\", |
| 100 | + data=json.dumps(data).encode(\"utf-8\"), |
| 101 | + headers={ |
| 102 | + \"Authorization\": \"Bearer secret-token\", |
| 103 | + \"Content-Type\": \"application/json\" |
| 104 | + } |
| 105 | +) |
| 106 | +with urllib.request.urlopen(req) as response: |
| 107 | + print(response.read().decode(\"utf-8\")) |
| 108 | +" |
| 109 | + echo "Register HTTP output: $output" |
| 110 | + [[ "$status" -eq 0 ]] |
| 111 | + [[ "$output" == *"Service registered"* ]] |
| 112 | + |
| 113 | + # 2. Setup Stdio Service on Node 2 side |
| 114 | + # Register Stdio service (cat) on Node 2 |
| 115 | + echo "[$(date +%T)] Registering Stdio service on Node 2" |
| 116 | + run docker run --rm --network "${MESH_NETWORK}" python:3.12 python3 -c " |
| 117 | +import urllib.request |
| 118 | +import json |
| 119 | +
|
| 120 | +data = { |
| 121 | + 'service': { |
| 122 | + 'type': 1, |
| 123 | + 'name': 'stdio-tool', |
| 124 | + 'description': 'test stdio service' |
| 125 | + }, |
| 126 | + 'command': { |
| 127 | + 'command': ['sh', '-c', 'sleep 1; cat'] |
| 128 | + } |
| 129 | +} |
| 130 | +
|
| 131 | +req = urllib.request.Request( |
| 132 | + 'http://sam-node-2:8080/sam/service/register', |
| 133 | + data=json.dumps(data).encode('utf-8'), |
| 134 | + headers={ |
| 135 | + 'Authorization': 'Bearer secret-token', |
| 136 | + 'Content-Type': 'application/json' |
| 137 | + } |
| 138 | +) |
| 139 | +with urllib.request.urlopen(req) as response: |
| 140 | + print(response.read().decode('utf-8')) |
| 141 | +" |
| 142 | + if [[ "$status" -ne 0 ]]; then |
| 143 | + echo "Node 2 logs:" |
| 144 | + docker logs "${node2_name}" |
| 145 | + fi |
| 146 | + [[ "$status" -eq 0 ]] |
| 147 | + [[ "$output" == *"Service registered"* ]] |
| 148 | + |
| 149 | + # Wait for DHT propagation |
| 150 | + sleep 2 |
| 151 | + |
| 152 | + # 3. Test HTTP Datapath: Node 2 calls Node 1's HTTP service |
| 153 | + echo "[$(date +%T)] Testing HTTP Datapath from Node 2 to Node 1" |
| 154 | + run docker run --rm --network "${MESH_NETWORK}" python:3.12 python3 -c " |
| 155 | +import urllib.request |
| 156 | +req = urllib.request.Request( |
| 157 | + \"http://sam-node-2:8080/sam/${node1_peer_id}/mcp/http-tool/\", |
| 158 | + headers={\"Authorization\": \"Bearer secret-token\"} |
| 159 | +) |
| 160 | +with urllib.request.urlopen(req) as response: |
| 161 | + print(response.read().decode(\"utf-8\")) |
| 162 | +" |
| 163 | + echo "HTTP Call output: $output" |
| 164 | + [[ "$status" -eq 0 ]] |
| 165 | + [[ "$output" == *"{\"status\":\"success\"}"* ]] |
| 166 | + |
| 167 | + # 4. Test Stdio Datapath: Node 1 calls Node 2's Stdio service |
| 168 | + echo "[$(date +%T)] Testing Stdio Datapath from Node 1 to Node 2" |
| 169 | + |
| 170 | + # Start SSE client in background on Node 1 targeting Node 2's service |
| 171 | + docker run -d \ |
| 172 | + --name sse-client \ |
| 173 | + --network "${MESH_NETWORK}" \ |
| 174 | + python:3.12 python3 -c " |
| 175 | +import urllib.request |
| 176 | +req = urllib.request.Request( |
| 177 | + \"http://sam-node-1:8080/sam/${node2_peer_id}/mcp/stdio-tool/\", |
| 178 | + headers={\"Authorization\": \"Bearer secret-token\"} |
| 179 | +) |
| 180 | +try: |
| 181 | + with urllib.request.urlopen(req) as response: |
| 182 | + for line in response: |
| 183 | + print(line.decode(\"utf-8\").strip(), flush=True) |
| 184 | +except Exception as e: |
| 185 | + print(f\"Error: {e}\", flush=True) |
| 186 | +" |
| 187 | + MESH_CONTAINERS+=("sse-client") |
| 188 | + |
| 189 | + # Wait a bit for SSE stream to establish |
| 190 | + sleep 1 |
| 191 | + |
| 192 | + # Send message via POST from Node 1 to Node 2's service |
| 193 | + test_message="{\"jsonrpc\":\"2.0\",\"method\":\"ping\",\"id\":1}" |
| 194 | + run docker run --rm --network "${MESH_NETWORK}" -e MSG="${test_message}" python:3.12 python3 -c " |
| 195 | +import urllib.request |
| 196 | +import os |
| 197 | +req = urllib.request.Request( |
| 198 | + \"http://sam-node-1:8080/sam/${node2_peer_id}/mcp/stdio-tool/\", |
| 199 | + data=os.environ['MSG'].encode('utf-8'), |
| 200 | + headers={ |
| 201 | + \"Authorization\": \"Bearer secret-token\", |
| 202 | + \"Content-Type\": \"application/json\" |
| 203 | + } |
| 204 | +) |
| 205 | +with urllib.request.urlopen(req) as response: |
| 206 | + print(response.status) |
| 207 | +" |
| 208 | + echo "POST status: $output" |
| 209 | + [[ "$status" -eq 0 ]] |
| 210 | + [[ "$output" == *"200"* ]] |
| 211 | + |
| 212 | + # Wait for message to echo back |
| 213 | + sleep 1 |
| 214 | + |
| 215 | + # Check SSE client logs for the echoed message |
| 216 | + run docker logs sse-client |
| 217 | + echo "SSE client logs: $output" |
| 218 | + [[ "$output" == *"data: ${test_message}"* ]] |
| 219 | +} |
0 commit comments