ci: probe providers through bridge runtime
This commit is contained in:
parent
a205547677
commit
1059cc7f86
@ -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)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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":
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user