ci: probe providers through bridge runtime

This commit is contained in:
Haitao Pan 2026-04-14 11:51:11 +08:00
parent a205547677
commit 1059cc7f86
5 changed files with 146 additions and 28 deletions

View File

@ -168,6 +168,37 @@ func (s *Server) runSingleAgentViaExternalProvider(
return enrichSingleAgentResultArtifacts(collector.apply(result), forwardParams), nil
}
func (s *Server) probeExternalProvider(
ctx context.Context,
provider syncedProvider,
params map[string]any,
) (map[string]any, error) {
endpoint := resolveSingleAgentForwardEndpoint(provider)
if endpoint == "" {
return nil, fmt.Errorf("external provider endpoint is missing")
}
authorization := firstNonEmptyString(
strings.TrimSpace(provider.AuthorizationHeader),
strings.TrimSpace(shared.StringArg(params, inboundAuthorizationHeaderKey, "")),
)
response, err := requestExternalACP(
ctx,
endpoint,
authorization,
"acp.capabilities",
map[string]any{},
nil,
)
if err != nil {
return nil, err
}
result := asMap(response["result"])
if len(result) == 0 {
return nil, fmt.Errorf("external provider probe missing result payload")
}
return result, nil
}
func resolveSingleAgentForwardEndpoint(provider syncedProvider) string {
return strings.TrimSpace(provider.Endpoint)
}

View File

@ -7,6 +7,7 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
@ -312,6 +313,64 @@ func TestExecuteSessionTaskUsesBridgeAuthTokenFallbackForBuiltInProvider(t *test
}
}
func TestHandleRequestProviderProbeUsesBridgeForwardingPath(t *testing.T) {
externalServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.Header.Get("Authorization"); got != "Bearer probe-token" {
t.Fatalf("expected probe bearer auth header, got %q", got)
}
defer func() {
_ = r.Body.Close()
}()
var request map[string]any
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatalf("decode request: %v", err)
}
if got := request["method"]; got != "acp.capabilities" {
t.Fatalf("expected bridge probe to forward acp.capabilities, got %#v", request)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"jsonrpc": "2.0",
"id": request["id"],
"result": map[string]any{
"providers": []string{"codex"},
},
})
}))
defer externalServer.Close()
server := NewServer()
setTestBridgeProvider(server, syncedProvider{
ProviderID: "codex",
Label: "Codex",
Endpoint: externalServer.URL,
AuthorizationHeader: "Bearer probe-token",
Enabled: true,
})
response, rpcErr := server.handleRequest(shared.RPCRequest{
Method: "xworkmate.provider.probe",
Params: map[string]any{
"providerId": "codex",
},
}, func(map[string]any) {})
if rpcErr != nil {
t.Fatalf("expected success, got rpc error: %v", rpcErr)
}
if got := response["success"]; got != true {
t.Fatalf("expected provider probe success, got %#v", response)
}
if got := response["providerId"]; got != "codex" {
t.Fatalf("expected providerId codex, got %#v", response)
}
capabilities, ok := response["capabilities"].(map[string]any)
if !ok {
t.Fatalf("expected capabilities payload, got %#v", response)
}
if got := capabilities["providers"]; !reflect.DeepEqual(got, []any{"codex"}) {
t.Fatalf("expected provider list in capabilities, got %#v", capabilities)
}
}
func TestExecuteSessionTaskEnrichesExternalProviderResultWithArtifactsAndRemoteMetadata(t *testing.T) {
workingDir := t.TempDir()
if err := os.MkdirAll(filepath.Join(workingDir, "outputs"), 0o755); err != nil {

View File

@ -396,6 +396,36 @@ func (s *Server) handleRequest(
s.availableProviders(),
)
return mergeRoutingResponse(map[string]any{"ok": true}, result), nil
case "xworkmate.provider.probe":
providerID := strings.TrimSpace(shared.StringArg(request.Params, "providerId", ""))
if providerID == "" {
return nil, &shared.RPCError{
Code: -32602,
Message: "providerId is required",
}
}
provider, ok := s.syncedProviderByID(providerID)
if !ok {
return map[string]any{
"success": false,
"providerId": providerID,
"error": "provider is not advertised by the bridge",
}, nil
}
result, err := s.probeExternalProvider(context.Background(), provider, request.Params)
if err != nil {
return map[string]any{
"success": false,
"providerId": providerID,
"error": err.Error(),
}, nil
}
return map[string]any{
"success": true,
"providerId": providerID,
"probeMethod": "acp.capabilities",
"capabilities": result,
}, nil
case "xworkmate.mounts.reconcile":
return handleMountReconcile(request.Params), nil
case "xworkmate.gateway.connect":

View File

@ -139,7 +139,20 @@ case "${scenario}" in
printf '{"jsonrpc":"2.0","result":{"providers":["ok"]}}\n'
;;
https://xworkmate-bridge.svc.plus/acp/rpc)
printf '{"jsonrpc":"2.0","result":{"success":true,"output":"pong"}}\n'
if [[ "${data}" == *'"providerId":"codex"'* ]]; then
printf '{"jsonrpc":"2.0","result":{"success":true,"providerId":"codex","capabilities":{"providers":["codex"]}}}\n'
exit 0
fi
if [[ "${data}" == *'"providerId":"opencode"'* ]]; then
printf '{"jsonrpc":"2.0","result":{"success":true,"providerId":"opencode","capabilities":{"providers":["opencode"]}}}\n'
exit 0
fi
if [[ "${data}" == *'"providerId":"gemini"'* ]]; then
printf '{"jsonrpc":"2.0","result":{"success":true,"providerId":"gemini","capabilities":{"providers":["gemini"]}}}\n'
exit 0
fi
printf 'unexpected bridge probe payload in retry-success scenario: %s\n' "${data}" >&2
exit 1
;;
*)
printf 'unexpected url in retry-success scenario: %s\n' "${url}" >&2

View File

@ -5,7 +5,7 @@ IMAGE_REF="${1:?image_ref is required}"
RETRYABLE_TRANSPORT=10
RETRYABLE_NOT_READY=11
FAST_HTTP_TIMEOUT_SECONDS=20
BRIDGE_RPC_TIMEOUT_SECONDS=330
BRIDGE_RPC_TIMEOUT_SECONDS=60
normalize_url() {
local value="$1"
@ -215,15 +215,13 @@ jsonrpc_bridge_call() {
printf '%s\n' "${response}"
}
probe_bridge_single_agent_smoke_once() {
probe_bridge_provider_probe_once() {
local provider_id="$1"
local request_id="smoke-${provider_id}-$(date +%s)"
local session_id="validate-${provider_id}-$(date +%s)"
local payload
local response
payload="$(cat <<JSON
{"jsonrpc":"2.0","id":"${request_id}","method":"session.start","params":{"sessionId":"${session_id}","threadId":"${session_id}","taskPrompt":"Reply with exactly pong","routing":{"routingMode":"explicit","explicitExecutionTarget":"singleAgent","explicitProviderId":"${provider_id}"}}}
{"jsonrpc":"2.0","id":"probe-${provider_id}-$(date +%s)","method":"xworkmate.provider.probe","params":{"providerId":"${provider_id}"}}
JSON
)"
@ -247,32 +245,19 @@ except json.JSONDecodeError as exc:
if payload.get("jsonrpc") != "2.0":
raise SystemExit(f"{provider}: missing jsonrpc envelope")
if payload.get("error"):
raise SystemExit(f"{provider}: rpc error {payload['error']}")
result = payload.get("result")
if not isinstance(result, dict):
raise SystemExit(f"{provider}: missing result payload")
if result.get("success") is not True:
raise SystemExit(f"{provider}: success flag was not true: {result!r}")
raise SystemExit(f"{provider}: provider probe failed: {result!r}")
def first_text_candidate(data):
for key in ("output", "resultSummary", "summary", "message"):
value = data.get(key)
if isinstance(value, str) and value.strip():
return value
return ""
if result.get("providerId") != provider:
raise SystemExit(f"{provider}: providerId mismatch: {result!r}")
def normalize_text(value):
normalized = value.strip().strip("`").strip()
if len(normalized) >= 2 and normalized[0] == normalized[-1] and normalized[0] in {'"', "'"}:
normalized = normalized[1:-1].strip()
return normalized.lower()
text = first_text_candidate(result)
if normalize_text(text) != "pong":
raise SystemExit(f"{provider}: expected normalized pong output, got {text!r} from {result!r}")
capabilities = result.get("capabilities")
if not isinstance(capabilities, dict):
raise SystemExit(f"{provider}: probe did not return capabilities payload: {result!r}")
PY
}
@ -376,6 +361,6 @@ probe_safe_http_endpoint "${OPENCLAW_HTTP_PROBE_URL}"
run_with_retry "capabilities ${CODEX_RPC_URL}" 3 5 "${RETRYABLE_TRANSPORT}" probe_jsonrpc_capabilities_once "${CODEX_RPC_URL}"
run_with_retry "capabilities ${OPENCODE_RPC_URL}" 3 5 "${RETRYABLE_TRANSPORT}" probe_jsonrpc_capabilities_once "${OPENCODE_RPC_URL}"
run_with_retry "capabilities ${GEMINI_RPC_URL}" 3 5 "${RETRYABLE_TRANSPORT}" probe_jsonrpc_capabilities_once "${GEMINI_RPC_URL}"
run_with_retry "bridge single-agent smoke codex" 3 10 "${RETRYABLE_TRANSPORT}" probe_bridge_single_agent_smoke_once "codex"
run_with_retry "bridge single-agent smoke opencode" 3 10 "${RETRYABLE_TRANSPORT}" probe_bridge_single_agent_smoke_once "opencode"
run_with_retry "bridge single-agent smoke gemini" 3 10 "${RETRYABLE_TRANSPORT}" probe_bridge_single_agent_smoke_once "gemini"
run_with_retry "bridge provider probe codex" 3 10 "${RETRYABLE_TRANSPORT}" probe_bridge_provider_probe_once "codex"
run_with_retry "bridge provider probe opencode" 3 10 "${RETRYABLE_TRANSPORT}" probe_bridge_provider_probe_once "opencode"
run_with_retry "bridge provider probe gemini" 3 10 "${RETRYABLE_TRANSPORT}" probe_bridge_provider_probe_once "gemini"