test: lock bridge provider contract smoke checks

This commit is contained in:
Haitao Pan 2026-04-14 13:56:58 +08:00
parent 1059cc7f86
commit eaf0a6255f
3 changed files with 304 additions and 0 deletions

View File

@ -276,3 +276,8 @@ jobs:
- name: Validate deployed endpoints
run: bash ./scripts/github-actions/validate-deploy.sh "${{ needs.build.outputs.service_image_ref }}" "${BRIDGE_SERVER_URL}" "${OPENCLAW_URL}" "${CODEX_RPC_URL}" "${OPENCODE_RPC_URL}" "${GEMINI_RPC_URL}" "${INTERNAL_SERVICE_TOKEN}"
- name: Validate public ACP contract
env:
BRIDGE_AUTH_TOKEN: ${{ env.INTERNAL_SERVICE_TOKEN }}
run: bash ./scripts/github-actions/verify-public-rpc-contract.sh

View File

@ -4,6 +4,7 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
)
@ -194,6 +195,147 @@ func TestHandleRPCCapabilitiesStillReturnsJSONResult(t *testing.T) {
}
}
func TestHandleRPCCapabilitiesReturnsCanonicalProviderContract(t *testing.T) {
server := NewServer()
recorder := httptest.NewRecorder()
request := httptest.NewRequest(
http.MethodPost,
"http://127.0.0.1/acp/rpc",
strings.NewReader(`{"jsonrpc":"2.0","id":"cap-1","method":"acp.capabilities"}`),
)
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Authorization", "Bearer test")
server.HandleRPC(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", recorder.Code)
}
var envelope map[string]any
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
t.Fatalf("decode capabilities response: %v", err)
}
result := asMap(envelope["result"])
availableTargets := mustStringList(t, result["availableExecutionTargets"])
if !reflect.DeepEqual(availableTargets, []string{"agent", "gateway"}) {
t.Fatalf("expected canonical execution targets, got %#v", availableTargets)
}
providerCatalog := mustObjectList(t, result["providerCatalog"])
if len(providerCatalog) != 3 {
t.Fatalf("expected 3 providers, got %#v", providerCatalog)
}
wantAgentIDs := []string{"codex", "opencode", "gemini"}
wantAgentLabels := []string{"Codex", "OpenCode", "Gemini"}
for index, wantID := range wantAgentIDs {
if got := providerCatalog[index]["providerId"]; got != wantID {
t.Fatalf("expected provider %q at index %d, got %#v", wantID, index, providerCatalog)
}
if got := providerCatalog[index]["label"]; got != wantAgentLabels[index] {
t.Fatalf("expected label %q at index %d, got %#v", wantAgentLabels[index], index, providerCatalog)
}
if targets := mustStringList(t, providerCatalog[index]["targets"]); !reflect.DeepEqual(targets, []string{"agent"}) {
t.Fatalf("expected agent targets for %q, got %#v", wantID, targets)
}
}
gatewayProviders := mustObjectList(t, result["gatewayProviders"])
if len(gatewayProviders) != 1 {
t.Fatalf("expected exactly one gateway provider, got %#v", gatewayProviders)
}
if got := gatewayProviders[0]["providerId"]; got != "openclaw" {
t.Fatalf("expected gateway providerId openclaw, got %#v", gatewayProviders[0])
}
if got := gatewayProviders[0]["label"]; got != "OpenClaw" {
t.Fatalf("expected gateway label OpenClaw, got %#v", gatewayProviders[0])
}
if targets := mustStringList(t, gatewayProviders[0]["targets"]); !reflect.DeepEqual(targets, []string{"gateway"}) {
t.Fatalf("expected gateway targets, got %#v", targets)
}
}
func TestHandleRPCSessionStartSucceedsWithExplicitProvider(t *testing.T) {
externalServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.Header.Get("Authorization"); got != "Bearer internal-test-token" {
t.Fatalf("unexpected auth header: %q", got)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"jsonrpc": "2.0",
"id": "task-1",
"result": map[string]any{
"success": true,
"provider": "opencode",
"output": "pong",
},
})
}))
defer externalServer.Close()
t.Setenv("INTERNAL_SERVICE_TOKEN", "internal-test-token")
server := NewServer()
setTestBridgeProvider(server, syncedProvider{
ProviderID: "opencode",
Label: "OpenCode",
Endpoint: externalServer.URL,
AuthorizationHeader: "Bearer internal-test-token",
Enabled: true,
})
recorder := httptest.NewRecorder()
request := httptest.NewRequest(
http.MethodPost,
"http://127.0.0.1/acp/rpc",
strings.NewReader(`{"jsonrpc":"2.0","id":"task-1","method":"session.start","params":{"sessionId":"s1","threadId":"t1","taskPrompt":"Reply with exactly pong","workingDirectory":"`+t.TempDir()+`","routing":{"routingMode":"explicit","explicitExecutionTarget":"singleAgent","explicitProviderId":"opencode"}}}`),
)
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Authorization", "Bearer bridge-token")
server.HandleRPC(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", recorder.Code)
}
if !strings.Contains(recorder.Body.String(), `"output":"pong"`) {
t.Fatalf("expected pong output, got %q", recorder.Body.String())
}
if !strings.Contains(recorder.Body.String(), `"provider":"opencode"`) {
t.Fatalf("expected opencode provider, got %q", recorder.Body.String())
}
}
func mustObjectList(t *testing.T, value any) []map[string]any {
t.Helper()
raw, ok := value.([]any)
if !ok {
t.Fatalf("expected object list, got %#v", value)
}
items := make([]map[string]any, 0, len(raw))
for _, item := range raw {
items = append(items, asMap(item))
}
return items
}
func mustStringList(t *testing.T, value any) []string {
t.Helper()
switch typed := value.(type) {
case []string:
return typed
case []any:
items := make([]string, 0, len(typed))
for _, item := range typed {
items = append(items, strings.TrimSpace(item.(string)))
}
return items
default:
t.Fatalf("expected string list, got %#v", value)
return nil
}
}
func TestHandleWebSocketRequiresBearerAuthorization(t *testing.T) {
t.Setenv("ACP_ALLOWED_ORIGINS", "https://xworkmate.svc.plus")

View File

@ -0,0 +1,157 @@
#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${BRIDGE_SERVER_URL:-https://xworkmate-bridge.svc.plus}"
AUTH_TOKEN="${BRIDGE_AUTH_TOKEN:-${INTERNAL_SERVICE_TOKEN:-}}"
HTTP_TIMEOUT_SECONDS="${HTTP_TIMEOUT_SECONDS:-30}"
RPC_TIMEOUT_SECONDS="${RPC_TIMEOUT_SECONDS:-90}"
if [[ -z "${AUTH_TOKEN}" ]]; then
echo "BRIDGE_AUTH_TOKEN or INTERNAL_SERVICE_TOKEN is required" >&2
exit 1
fi
normalize_url() {
local raw="$1"
printf '%s\n' "${raw%/}"
}
json_rpc_call() {
local payload="$1"
shift
curl \
--silent \
--show-error \
--fail \
--location \
--max-time "${RPC_TIMEOUT_SECONDS}" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
"$@" \
--data "${payload}" \
"${resolved_base_url}/acp/rpc"
}
json_rpc_with_retry() {
local payload="$1"
shift
local attempt
for attempt in 1 2 3; do
if json_rpc_call "${payload}" "$@"; then
return 0
fi
if (( attempt == 3 )); then
return 1
fi
sleep 5
done
}
resolved_base_url="$(normalize_url "${BASE_URL}")"
unauthorized_status="$(
curl \
--silent \
--show-error \
--output /tmp/xworkmate-bridge-public-contract-unauthorized.json \
--write-out '%{http_code}' \
--location \
--max-time "${HTTP_TIMEOUT_SECONDS}" \
-H 'Accept: application/json' \
-H 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","id":"cap-unauthorized","method":"acp.capabilities"}' \
"${resolved_base_url}/acp/rpc"
)"
if [[ "${unauthorized_status}" != "401" ]]; then
echo "expected unauthorized capabilities request to return 401, got ${unauthorized_status}" >&2
exit 1
fi
capabilities_json="$(
json_rpc_with_retry \
'{"jsonrpc":"2.0","id":"cap-1","method":"acp.capabilities"}' \
-H "Authorization: Bearer ${AUTH_TOKEN}"
)"
RESPONSE_JSON="${capabilities_json}" python3 - <<'PY'
import json
import os
payload = json.loads(os.environ["RESPONSE_JSON"])
if payload.get("jsonrpc") != "2.0":
raise SystemExit("bridge capabilities response missing jsonrpc envelope")
result = payload.get("result")
if not isinstance(result, dict):
raise SystemExit("bridge capabilities response missing result payload")
expected_targets = ["agent", "gateway"]
if result.get("availableExecutionTargets") != expected_targets:
raise SystemExit(
f"expected availableExecutionTargets {expected_targets!r}, got {result.get('availableExecutionTargets')!r}"
)
provider_catalog = result.get("providerCatalog")
gateway_providers = result.get("gatewayProviders")
if not isinstance(provider_catalog, list):
raise SystemExit("providerCatalog is missing or invalid")
if not isinstance(gateway_providers, list):
raise SystemExit("gatewayProviders is missing or invalid")
expected_agent_ids = ["codex", "opencode", "gemini"]
expected_agent_labels = ["Codex", "OpenCode", "Gemini"]
if len(provider_catalog) != len(expected_agent_ids):
raise SystemExit(f"expected 3 agent providers, got {provider_catalog!r}")
for index, (provider_id, label) in enumerate(zip(expected_agent_ids, expected_agent_labels)):
item = provider_catalog[index]
if item.get("providerId") != provider_id:
raise SystemExit(f"expected providerId {provider_id!r} at index {index}, got {item!r}")
if item.get("label") != label:
raise SystemExit(f"expected label {label!r} at index {index}, got {item!r}")
if item.get("targets") != ["agent"]:
raise SystemExit(f"expected agent targets for {provider_id!r}, got {item!r}")
if len(gateway_providers) != 1:
raise SystemExit(f"expected one gateway provider, got {gateway_providers!r}")
gateway = gateway_providers[0]
if gateway.get("providerId") != "openclaw":
raise SystemExit(f"expected gateway providerId 'openclaw', got {gateway!r}")
if gateway.get("label") != "OpenClaw":
raise SystemExit(f"expected gateway label 'OpenClaw', got {gateway!r}")
if gateway.get("targets") != ["gateway"]:
raise SystemExit(f"expected gateway targets ['gateway'], got {gateway!r}")
PY
session_start_json="$(
json_rpc_with_retry \
'{"jsonrpc":"2.0","id":"task-1","method":"session.start","params":{"sessionId":"public-contract-smoke","threadId":"public-contract-smoke","taskPrompt":"Reply with exactly pong","workingDirectory":"/tmp","routing":{"routingMode":"explicit","explicitExecutionTarget":"singleAgent","explicitProviderId":"opencode"}}}' \
-H "Authorization: Bearer ${AUTH_TOKEN}"
)"
RESPONSE_JSON="${session_start_json}" python3 - <<'PY'
import json
import os
payload = json.loads(os.environ["RESPONSE_JSON"])
if payload.get("jsonrpc") != "2.0":
raise SystemExit("session.start response missing jsonrpc envelope")
result = payload.get("result")
if not isinstance(result, dict):
raise SystemExit(f"session.start missing result payload: {payload!r}")
if result.get("success") is not True:
raise SystemExit(f"session.start did not succeed: {result!r}")
if result.get("provider") != "opencode":
raise SystemExit(f"expected provider 'opencode', got {result!r}")
output = str(result.get("output", "")).strip().lower()
if output != "pong":
raise SystemExit(f"expected output 'pong', got {result!r}")
PY
printf 'public bridge RPC contract verified via %s\n' "${resolved_base_url}"