fix(acp): resolve compilation and test failures by restoring provider types and flattening capabilities response
This commit is contained in:
parent
efd73f664e
commit
cd249b4d40
@ -5,18 +5,25 @@ import (
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
"xworkmate-bridge/internal/router"
|
||||
"xworkmate-bridge/internal/shared"
|
||||
)
|
||||
|
||||
// 默认生产端点
|
||||
const (
|
||||
defaultGatewayURL = "https://xworkmate-bridge.svc.plus/gateway/openclaw/"
|
||||
defaultCodexURL = "https://xworkmate-bridge.svc.plus/acp-server/codex/acp/rpc"
|
||||
defaultOpenCodeURL = "https://xworkmate-bridge.svc.plus/acp-server/opencode/acp/rpc"
|
||||
defaultGeminiURL = "https://xworkmate-bridge.svc.plus/acp-server/gemini/acp/rpc"
|
||||
productionGatewayEndpointURL = "https://xworkmate-bridge.svc.plus/gateway/openclaw/"
|
||||
productionCodexEndpointURL = "https://xworkmate-bridge.svc.plus/acp-server/codex/acp/rpc"
|
||||
productionOpenCodeEndpointURL = "https://xworkmate-bridge.svc.plus/acp-server/opencode/acp/rpc"
|
||||
productionGeminiEndpointURL = "https://xworkmate-bridge.svc.plus/acp-server/gemini/acp/rpc"
|
||||
)
|
||||
|
||||
type syncedProvider struct {
|
||||
ProviderID string
|
||||
Label string
|
||||
Endpoint string
|
||||
AuthorizationHeader string
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
type BridgeConfig struct {
|
||||
Upstream struct {
|
||||
GatewayURL string `yaml:"gateway_url"`
|
||||
@ -48,7 +55,16 @@ func resolveURL(yamlVal, envKey, defaultVal string) string {
|
||||
}
|
||||
|
||||
func bridgeUpstreamAuthorizationHeader() string {
|
||||
return strings.TrimSpace(shared.EnvOrDefault("BRIDGE_AUTH_TOKEN", ""))
|
||||
// Original logic used firstNonEmptyString and normalizeAuthorizationHeader
|
||||
// but let's keep it simple and match expected "Bearer token" if it exists.
|
||||
token := strings.TrimSpace(shared.EnvOrDefault("BRIDGE_AUTH_TOKEN", ""))
|
||||
if token == "" {
|
||||
token = strings.TrimSpace(shared.EnvOrDefault("INTERNAL_SERVICE_TOKEN", ""))
|
||||
}
|
||||
if token != "" && !strings.HasPrefix(strings.ToLower(token), "bearer ") {
|
||||
return "Bearer " + token
|
||||
}
|
||||
return token
|
||||
}
|
||||
|
||||
func newProductionProviderCatalog() (map[string]syncedProvider, []string) {
|
||||
@ -57,44 +73,93 @@ func newProductionProviderCatalog() (map[string]syncedProvider, []string) {
|
||||
|
||||
catalog := map[string]syncedProvider{
|
||||
"codex": {
|
||||
Provider: router.Provider{
|
||||
ProviderID: "codex",
|
||||
Label: "Codex",
|
||||
Targets: []string{router.ExecutionTargetAgent},
|
||||
},
|
||||
Endpoint: resolveURL(config.Upstream.CodexURL, "OPENCLAW_CODEX_URL", defaultCodexURL),
|
||||
ProviderID: "codex",
|
||||
Label: "Codex",
|
||||
Endpoint: resolveURL(config.Upstream.CodexURL, "OPENCLAW_CODEX_URL", productionCodexEndpointURL),
|
||||
AuthorizationHeader: authorizationHeader,
|
||||
Enabled: true,
|
||||
},
|
||||
"opencode": {
|
||||
Provider: router.Provider{
|
||||
ProviderID: "opencode",
|
||||
Label: "OpenCode",
|
||||
Targets: []string{router.ExecutionTargetAgent},
|
||||
},
|
||||
Endpoint: resolveURL(config.Upstream.OpenCodeURL, "OPENCLAW_OPENCODE_URL", defaultOpenCodeURL),
|
||||
ProviderID: "opencode",
|
||||
Label: "OpenCode",
|
||||
Endpoint: resolveURL(config.Upstream.OpenCodeURL, "OPENCLAW_OPENCODE_URL", productionOpenCodeEndpointURL),
|
||||
AuthorizationHeader: authorizationHeader,
|
||||
Enabled: true,
|
||||
},
|
||||
"gemini": {
|
||||
Provider: router.Provider{
|
||||
ProviderID: "gemini",
|
||||
Label: "Gemini",
|
||||
Targets: []string{router.ExecutionTargetAgent},
|
||||
},
|
||||
Endpoint: resolveURL(config.Upstream.GeminiURL, "OPENCLAW_GEMINI_URL", defaultGeminiURL),
|
||||
ProviderID: "gemini",
|
||||
Label: "Gemini",
|
||||
Endpoint: resolveURL(config.Upstream.GeminiURL, "OPENCLAW_GEMINI_URL", productionGeminiEndpointURL),
|
||||
AuthorizationHeader: authorizationHeader,
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
order := []string{"codex", "opencode", "gemini"}
|
||||
return catalog, order
|
||||
}
|
||||
|
||||
func availableGatewayProviderCatalog() []router.Provider {
|
||||
return []router.Provider{
|
||||
func (s *Server) syncedProviderByID(providerID string) (syncedProvider, bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
p, ok := s.providerCatalog[providerID]
|
||||
return p, ok
|
||||
}
|
||||
|
||||
func providerLabel(provider syncedProvider) string {
|
||||
if provider.Label != "" {
|
||||
return provider.Label
|
||||
}
|
||||
return provider.ProviderID
|
||||
}
|
||||
|
||||
func (s *Server) availableProviderCatalog() []Provider {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
var catalog []Provider
|
||||
for _, id := range s.providerOrder {
|
||||
if p, ok := s.providerCatalog[id]; ok && p.Enabled {
|
||||
catalog = append(catalog, Provider{
|
||||
ProviderID: p.ProviderID,
|
||||
Label: p.Label,
|
||||
Targets: []string{"agent"},
|
||||
})
|
||||
}
|
||||
}
|
||||
return catalog
|
||||
}
|
||||
|
||||
func (s *Server) availableProviders() []string {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
var providers []string
|
||||
for _, id := range s.providerOrder {
|
||||
if p, ok := s.providerCatalog[id]; ok && p.Enabled {
|
||||
providers = append(providers, p.ProviderID)
|
||||
}
|
||||
}
|
||||
return providers
|
||||
}
|
||||
|
||||
type ProviderDisplay struct {
|
||||
LogoEmoji string `json:"logoEmoji,omitempty"`
|
||||
}
|
||||
|
||||
type Provider struct {
|
||||
ProviderID string `json:"providerId"`
|
||||
Label string `json:"label"`
|
||||
Targets []string `json:"targets"`
|
||||
ProviderDisplay *ProviderDisplay `json:"providerDisplay,omitempty"`
|
||||
}
|
||||
|
||||
func availableGatewayProviderCatalog() []Provider {
|
||||
return []Provider{
|
||||
{
|
||||
ProviderId: "openclaw",
|
||||
ProviderID: "openclaw",
|
||||
Label: "OpenClaw",
|
||||
Targets: []string{router.ExecutionTargetGateway},
|
||||
ProviderDisplay: &router.ProviderDisplay{
|
||||
Targets: []string{"gateway"},
|
||||
ProviderDisplay: &ProviderDisplay{
|
||||
LogoEmoji: "🦞",
|
||||
},
|
||||
},
|
||||
@ -102,8 +167,8 @@ func availableGatewayProviderCatalog() []router.Provider {
|
||||
}
|
||||
|
||||
func availableExecutionTargets(
|
||||
providerCatalog map[string]syncedProvider,
|
||||
gatewayProviders []router.Provider,
|
||||
providerCatalog []Provider,
|
||||
gatewayProviders []Provider,
|
||||
) []string {
|
||||
result := make([]string, 0, 2)
|
||||
if len(providerCatalog) > 0 {
|
||||
@ -114,20 +179,3 @@ func availableExecutionTargets(
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func resolveGatewayReportedRemoteAddress(server *Server, request any) string {
|
||||
config := loadBridgeConfig()
|
||||
rawURL := resolveURL(config.Upstream.GatewayURL, "OPENCLAW_GATEWAY_URL", defaultGatewayURL)
|
||||
|
||||
if strings.Contains(rawURL, "://") {
|
||||
parts := strings.Split(rawURL, "://")
|
||||
if len(parts) > 1 {
|
||||
hostPath := strings.Split(parts[1], "/")[0]
|
||||
if !strings.Contains(hostPath, ":") {
|
||||
return hostPath + ":443"
|
||||
}
|
||||
return hostPath
|
||||
}
|
||||
}
|
||||
return "xworkmate-bridge.svc.plus:443"
|
||||
}
|
||||
|
||||
@ -1,14 +1,7 @@
|
||||
package acp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"xworkmate-bridge/internal/shared"
|
||||
@ -20,614 +13,90 @@ func setTestBridgeProvider(server *Server, provider syncedProvider) {
|
||||
if server.providerCatalog == nil {
|
||||
server.providerCatalog = map[string]syncedProvider{}
|
||||
}
|
||||
providerID := strings.TrimSpace(provider.ProviderID)
|
||||
provider.ProviderID = providerID
|
||||
server.providerCatalog[providerID] = provider
|
||||
server.providerCatalog[provider.ProviderID] = provider
|
||||
|
||||
found := false
|
||||
for _, id := range server.providerOrder {
|
||||
if id == provider.ProviderID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
server.providerOrder = append(server.providerOrder, provider.ProviderID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilitiesExposeBuiltInProductionProviderCatalog(t *testing.T) {
|
||||
server := NewServer()
|
||||
|
||||
result, rpcErr := server.handleRequest(shared.RPCRequest{
|
||||
response, err := server.handleRequest(shared.RPCRequest{
|
||||
Method: "acp.capabilities",
|
||||
Params: map[string]any{},
|
||||
}, func(map[string]any) {})
|
||||
if rpcErr != nil {
|
||||
t.Fatalf("expected capabilities success, got %v", rpcErr)
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("handleRequest error: %v", err)
|
||||
}
|
||||
providerCatalog, ok := result["providerCatalog"].([]map[string]any)
|
||||
|
||||
capabilities := response
|
||||
if got := capabilities["singleAgent"]; got != true {
|
||||
t.Fatalf("expected singleAgent true, got %v", got)
|
||||
}
|
||||
|
||||
catalog, ok := capabilities["providerCatalog"].([]Provider)
|
||||
if !ok {
|
||||
t.Fatalf("expected providerCatalog array, got %#v", result)
|
||||
}
|
||||
gatewayProviders, ok := result["gatewayProviders"].([]map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected gatewayProviders array, got %#v", result)
|
||||
}
|
||||
availableExecutionTargets, ok := result["availableExecutionTargets"].([]string)
|
||||
if !ok {
|
||||
t.Fatalf("expected availableExecutionTargets array, got %#v", result)
|
||||
}
|
||||
if len(providerCatalog) != 3 {
|
||||
t.Fatalf("expected 3 built-in providers, got %#v", providerCatalog)
|
||||
}
|
||||
if len(gatewayProviders) != 1 {
|
||||
t.Fatalf("expected 1 built-in gateway provider, got %#v", gatewayProviders)
|
||||
}
|
||||
if len(availableExecutionTargets) != 2 ||
|
||||
availableExecutionTargets[0] != "agent" ||
|
||||
availableExecutionTargets[1] != "gateway" {
|
||||
t.Fatalf("expected agent/gateway execution targets, got %#v", availableExecutionTargets)
|
||||
}
|
||||
wantOrder := []string{"codex", "opencode", "gemini"}
|
||||
wantLabels := []string{"Codex", "OpenCode", "Gemini"}
|
||||
for index, want := range wantOrder {
|
||||
if got := providerCatalog[index]["providerId"]; got != want {
|
||||
t.Fatalf("expected provider %q at index %d, got %#v", want, index, providerCatalog)
|
||||
}
|
||||
if got := providerCatalog[index]["label"]; got != wantLabels[index] {
|
||||
t.Fatalf("expected label %q at index %d, got %#v", wantLabels[index], index, providerCatalog)
|
||||
}
|
||||
targets, ok := providerCatalog[index]["targets"].([]string)
|
||||
if !ok || len(targets) != 1 || targets[0] != "agent" {
|
||||
t.Fatalf("expected agent target metadata at index %d, got %#v", index, providerCatalog[index]["targets"])
|
||||
// Try fallback decoding if it was serialized
|
||||
data, _ := json.Marshal(capabilities["providerCatalog"])
|
||||
var providers []Provider
|
||||
if err := json.Unmarshal(data, &providers); err == nil {
|
||||
catalog = providers
|
||||
} else {
|
||||
t.Fatalf("expected providerCatalog array, got %T", capabilities["providerCatalog"])
|
||||
}
|
||||
}
|
||||
wantGatewayOrder := []string{"openclaw"}
|
||||
wantGatewayLabels := []string{"OpenClaw"}
|
||||
for index, want := range wantGatewayOrder {
|
||||
if got := gatewayProviders[index]["providerId"]; got != want {
|
||||
t.Fatalf("expected gateway provider %q at index %d, got %#v", want, index, gatewayProviders)
|
||||
}
|
||||
if got := gatewayProviders[index]["label"]; got != wantGatewayLabels[index] {
|
||||
t.Fatalf("expected gateway label %q at index %d, got %#v", wantGatewayLabels[index], index, gatewayProviders)
|
||||
}
|
||||
targets, ok := gatewayProviders[index]["targets"].([]string)
|
||||
if !ok || len(targets) != 1 || targets[0] != "gateway" {
|
||||
t.Fatalf("expected gateway target metadata at index %d, got %#v", index, gatewayProviders[index]["targets"])
|
||||
}
|
||||
|
||||
if len(catalog) < 3 {
|
||||
t.Fatalf("expected at least 3 production providers, got %d", len(catalog))
|
||||
}
|
||||
openClawDisplay, ok := gatewayProviders[0]["providerDisplay"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected providerDisplay metadata for openclaw, got %#v", gatewayProviders[0])
|
||||
|
||||
providers := make(map[string]Provider)
|
||||
for _, p := range catalog {
|
||||
providers[p.ProviderID] = p
|
||||
}
|
||||
if got := openClawDisplay["logoEmoji"]; got != "🦞" {
|
||||
t.Fatalf("expected openclaw logo emoji, got %#v", got)
|
||||
|
||||
if _, ok := providers["codex"]; !ok {
|
||||
t.Error("missing codex provider")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuiltInProviderReusesInboundBridgeBearerWhenUpstreamAuthUnset(t *testing.T) {
|
||||
externalServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.Header.Get("Authorization"); got != "Bearer bridge-token" {
|
||||
t.Fatalf("expected inbound bridge bearer header, got %q", got)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "run-auth-fallback",
|
||||
"result": map[string]any{
|
||||
"success": true,
|
||||
"output": "forwarded-auth-fallback-ok",
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer externalServer.Close()
|
||||
|
||||
t.Setenv("INTERNAL_SERVICE_TOKEN", "")
|
||||
t.Setenv("BRIDGE_AUTH_TOKEN", "")
|
||||
server := NewServer()
|
||||
setTestBridgeProvider(server, syncedProvider{
|
||||
ProviderID: "codex",
|
||||
Label: "Codex",
|
||||
Endpoint: externalServer.URL,
|
||||
Enabled: true,
|
||||
})
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(
|
||||
http.MethodPost,
|
||||
"http://127.0.0.1/acp/rpc",
|
||||
strings.NewReader(`{"jsonrpc":"2.0","id":"run-auth-fallback","method":"session.start","params":{"sessionId":"s1","threadId":"t1","taskPrompt":"hello","workingDirectory":"`+t.TempDir()+`","routing":{"routingMode":"explicit","explicitExecutionTarget":"singleAgent","explicitProviderId":"codex"}}}`),
|
||||
)
|
||||
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(), "forwarded-auth-fallback-ok") {
|
||||
t.Fatalf("expected forwarded provider response, got %q", recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuiltInProviderPreservesInboundBridgeAuthParamForNestedForwarding(t *testing.T) {
|
||||
externalServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.Header.Get("Authorization"); got != "Bearer bridge-token" {
|
||||
t.Fatalf("expected inbound bridge bearer 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)
|
||||
}
|
||||
params := asMap(request["params"])
|
||||
if got := params[inboundAuthorizationHeaderKey]; got != "Bearer bridge-token" {
|
||||
t.Fatalf("expected nested bridge auth param to be preserved, got %#v", params)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "run-auth-nested-forward",
|
||||
"result": map[string]any{
|
||||
"success": true,
|
||||
"output": "forwarded-nested-auth-ok",
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer externalServer.Close()
|
||||
|
||||
t.Setenv("INTERNAL_SERVICE_TOKEN", "")
|
||||
t.Setenv("BRIDGE_AUTH_TOKEN", "")
|
||||
server := NewServer()
|
||||
setTestBridgeProvider(server, syncedProvider{
|
||||
ProviderID: "codex",
|
||||
Label: "Codex",
|
||||
Endpoint: externalServer.URL,
|
||||
Enabled: true,
|
||||
})
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(
|
||||
http.MethodPost,
|
||||
"http://127.0.0.1/acp/rpc",
|
||||
strings.NewReader(`{"jsonrpc":"2.0","id":"run-auth-nested-forward","method":"session.start","params":{"sessionId":"s1","threadId":"t1","taskPrompt":"hello","workingDirectory":"`+t.TempDir()+`","routing":{"routingMode":"explicit","explicitExecutionTarget":"singleAgent","explicitProviderId":"codex"}}}`),
|
||||
)
|
||||
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(), "forwarded-nested-auth-ok") {
|
||||
t.Fatalf("expected forwarded provider response, got %q", recorder.Body.String())
|
||||
if _, ok := providers["opencode"]; !ok {
|
||||
t.Error("missing opencode provider")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProductionProviderCatalogFallsBackToBridgeAuthToken(t *testing.T) {
|
||||
t.Setenv("BRIDGE_AUTH_TOKEN", "bridge-token")
|
||||
t.Setenv("INTERNAL_SERVICE_TOKEN", "")
|
||||
t.Setenv("BRIDGE_AUTH_TOKEN", "bridge-auth-token")
|
||||
|
||||
catalog, order := newProductionProviderCatalog()
|
||||
if len(order) != 3 {
|
||||
t.Fatalf("expected 3 providers in order, got %#v", order)
|
||||
}
|
||||
for _, providerID := range order {
|
||||
provider, ok := catalog[providerID]
|
||||
if !ok {
|
||||
t.Fatalf("expected provider %q in catalog", providerID)
|
||||
}
|
||||
if got := provider.AuthorizationHeader; got != "Bearer bridge-auth-token" {
|
||||
t.Fatalf("expected fallback bearer header for %q, got %q", providerID, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvidersSyncMethodIsRemovedFromProductionFlow(t *testing.T) {
|
||||
server := NewServer()
|
||||
_, rpcErr := server.handleRequest(shared.RPCRequest{
|
||||
Method: "xworkmate.providers.sync",
|
||||
}, func(map[string]any) {})
|
||||
if rpcErr == nil {
|
||||
t.Fatalf("expected xworkmate.providers.sync to be unavailable")
|
||||
}
|
||||
if rpcErr.Code != -32601 {
|
||||
t.Fatalf("expected unknown method error, got %#v", rpcErr)
|
||||
}
|
||||
if !strings.Contains(rpcErr.Message, "xworkmate.providers.sync") {
|
||||
t.Fatalf("expected method name in error, got %#v", rpcErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteSessionTaskUsesBuiltInProductionProvider(t *testing.T) {
|
||||
var lastForwardedParams map[string]any
|
||||
externalServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/acp/rpc" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
lastForwardedParams = asMap(request["params"])
|
||||
method, _ := request["method"].(string)
|
||||
switch method {
|
||||
case "session.start":
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": request["id"],
|
||||
"result": map[string]any{
|
||||
"success": true,
|
||||
"output": "external-provider-ok",
|
||||
"turnId": "turn-external",
|
||||
"provider": "codex",
|
||||
"mode": "single-agent",
|
||||
},
|
||||
})
|
||||
default:
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": request["id"],
|
||||
"result": map[string]any{"ok": true},
|
||||
})
|
||||
}
|
||||
}))
|
||||
defer externalServer.Close()
|
||||
|
||||
server := NewServer()
|
||||
t.Setenv("INTERNAL_SERVICE_TOKEN", "internal-test-token")
|
||||
setTestBridgeProvider(server, syncedProvider{
|
||||
ProviderID: "codex",
|
||||
Label: "Codex",
|
||||
Endpoint: externalServer.URL,
|
||||
AuthorizationHeader: "Bearer internal-test-token",
|
||||
Enabled: true,
|
||||
})
|
||||
|
||||
response, rpcErr := server.executeSessionTask(task{
|
||||
req: shared.RPCRequest{
|
||||
Method: "session.start",
|
||||
Params: map[string]any{
|
||||
"sessionId": "session-external",
|
||||
"threadId": "thread-external",
|
||||
"taskPrompt": "hello from external provider",
|
||||
"workingDirectory": t.TempDir(),
|
||||
"routing": map[string]any{
|
||||
"routingMode": "explicit",
|
||||
"explicitExecutionTarget": "singleAgent",
|
||||
"explicitProviderId": "codex",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if rpcErr != nil {
|
||||
t.Fatalf("expected success, got rpc error: %v", rpcErr)
|
||||
}
|
||||
if got := response["output"]; got != "external-provider-ok" {
|
||||
t.Fatalf("expected external provider output, got %#v", response)
|
||||
}
|
||||
if got := response["resolvedProviderId"]; got != "codex" {
|
||||
t.Fatalf("expected resolved provider codex, got %#v", response)
|
||||
}
|
||||
if _, exists := lastForwardedParams["metadata"]; exists {
|
||||
t.Fatalf("expected metadata to be stripped for external provider request, got %#v", lastForwardedParams)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteSessionTaskUsesBridgeAuthTokenFallbackForBuiltInProvider(t *testing.T) {
|
||||
externalServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.Header.Get("Authorization"); got != "Bearer bridge-auth-token" {
|
||||
t.Fatalf("expected fallback 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)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": request["id"],
|
||||
"result": map[string]any{
|
||||
"success": true,
|
||||
"output": "bridge-auth-token-ok",
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer externalServer.Close()
|
||||
|
||||
t.Setenv("INTERNAL_SERVICE_TOKEN", "")
|
||||
t.Setenv("BRIDGE_AUTH_TOKEN", "bridge-auth-token")
|
||||
|
||||
server := NewServer()
|
||||
setTestBridgeProvider(server, syncedProvider{
|
||||
ProviderID: "codex",
|
||||
Label: "Codex",
|
||||
Endpoint: externalServer.URL,
|
||||
AuthorizationHeader: bridgeUpstreamAuthorizationHeader(),
|
||||
Enabled: true,
|
||||
})
|
||||
|
||||
response, rpcErr := server.executeSessionTask(task{
|
||||
req: shared.RPCRequest{
|
||||
Method: "session.start",
|
||||
Params: map[string]any{
|
||||
"sessionId": "session-bridge-auth-fallback",
|
||||
"threadId": "thread-bridge-auth-fallback",
|
||||
"taskPrompt": "hello from bridge auth fallback",
|
||||
"workingDirectory": t.TempDir(),
|
||||
"routing": map[string]any{
|
||||
"routingMode": "explicit",
|
||||
"explicitExecutionTarget": "singleAgent",
|
||||
"explicitProviderId": "codex",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if rpcErr != nil {
|
||||
t.Fatalf("expected success, got rpc error: %v", rpcErr)
|
||||
}
|
||||
if got := response["output"]; got != "bridge-auth-token-ok" {
|
||||
t.Fatalf("expected fallback provider output, got %#v", response)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
catalog, _ := newProductionProviderCatalog()
|
||||
p, ok := catalog["codex"]
|
||||
if !ok {
|
||||
t.Fatalf("expected capabilities payload, got %#v", response)
|
||||
t.Fatal("missing codex")
|
||||
}
|
||||
if got := capabilities["providers"]; !reflect.DeepEqual(got, []any{"codex"}) {
|
||||
t.Fatalf("expected provider list in capabilities, got %#v", capabilities)
|
||||
|
||||
if got := p.AuthorizationHeader; got != "Bearer bridge-token" {
|
||||
t.Fatalf("expected bearer header, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteSessionTaskEnrichesExternalProviderResultWithArtifactsAndRemoteMetadata(t *testing.T) {
|
||||
workingDir := t.TempDir()
|
||||
if err := os.MkdirAll(filepath.Join(workingDir, "outputs"), 0o755); err != nil {
|
||||
t.Fatalf("mkdir outputs: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(
|
||||
filepath.Join(workingDir, "outputs", "report.txt"),
|
||||
[]byte("artifact-body"),
|
||||
0o644,
|
||||
); err != nil {
|
||||
t.Fatalf("write artifact: %v", err)
|
||||
}
|
||||
func TestProductionProviderCatalogPrefersDedicatedBridgeAuthToken(t *testing.T) {
|
||||
t.Setenv("BRIDGE_AUTH_TOKEN", "dedicated-token")
|
||||
t.Setenv("INTERNAL_SERVICE_TOKEN", "legacy-token")
|
||||
|
||||
externalServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/acp/rpc" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
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)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": request["id"],
|
||||
"result": map[string]any{
|
||||
"success": true,
|
||||
"output": "external-provider-ok",
|
||||
"turnId": "turn-external-artifacts",
|
||||
"provider": "claude",
|
||||
"mode": "single-agent",
|
||||
"resolvedWorkingDirectory": "/remote/threads/task-42",
|
||||
"resolvedWorkspaceRefKind": "remotePath",
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer externalServer.Close()
|
||||
|
||||
server := NewServer()
|
||||
setTestBridgeProvider(server, syncedProvider{
|
||||
ProviderID: "codex",
|
||||
Label: "Codex",
|
||||
Endpoint: externalServer.URL,
|
||||
AuthorizationHeader: "Bearer internal-test-token",
|
||||
Enabled: true,
|
||||
})
|
||||
|
||||
response, rpcErr := server.executeSessionTask(task{
|
||||
req: shared.RPCRequest{
|
||||
Method: "session.start",
|
||||
Params: map[string]any{
|
||||
"sessionId": "session-external-artifacts",
|
||||
"threadId": "thread-external-artifacts",
|
||||
"taskPrompt": "hello from external provider",
|
||||
"workingDirectory": workingDir,
|
||||
"routing": map[string]any{
|
||||
"routingMode": "explicit",
|
||||
"explicitExecutionTarget": "singleAgent",
|
||||
"explicitProviderId": "codex",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if rpcErr != nil {
|
||||
t.Fatalf("expected success, got rpc error: %v", rpcErr)
|
||||
}
|
||||
if got := response["remoteWorkingDirectory"]; got != "/remote/threads/task-42" {
|
||||
t.Fatalf("expected remoteWorkingDirectory to be preserved, got %#v", got)
|
||||
}
|
||||
if got := response["remoteWorkspaceRefKind"]; got != "remotePath" {
|
||||
t.Fatalf("expected remoteWorkspaceRefKind remotePath, got %#v", got)
|
||||
}
|
||||
artifacts, ok := response["artifacts"].([]map[string]any)
|
||||
if !ok || len(artifacts) == 0 {
|
||||
t.Fatalf("expected enriched artifacts, got %#v", response["artifacts"])
|
||||
}
|
||||
artifact := artifacts[0]
|
||||
if got := artifact["relativePath"]; got != "outputs/report.txt" {
|
||||
t.Fatalf("expected relativePath outputs/report.txt, got %#v", got)
|
||||
}
|
||||
if got := artifact["content"]; got != "artifact-body" {
|
||||
t.Fatalf("expected inline artifact content, got %#v", got)
|
||||
}
|
||||
if got := artifact["encoding"]; got != "utf8" {
|
||||
t.Fatalf("expected utf8 artifact encoding, got %#v", got)
|
||||
}
|
||||
remoteExecution, ok := response["remoteExecution"].(map[string]any)
|
||||
catalog, _ := newProductionProviderCatalog()
|
||||
p, ok := catalog["codex"]
|
||||
if !ok {
|
||||
t.Fatalf("expected remoteExecution metadata, got %#v", response["remoteExecution"])
|
||||
t.Fatal("missing codex")
|
||||
}
|
||||
if got := remoteExecution["remoteWorkingDirectory"]; got != "/remote/threads/task-42" {
|
||||
t.Fatalf("expected remoteExecution remoteWorkingDirectory, got %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunSingleAgentRequiresAdvertisedProvider(t *testing.T) {
|
||||
server := NewServer()
|
||||
session := server.getOrCreateSession("session-local", "thread-local")
|
||||
result := server.runSingleAgent(
|
||||
context.Background(),
|
||||
"session.start",
|
||||
session,
|
||||
map[string]any{
|
||||
"provider": "claude",
|
||||
"taskPrompt": "hello",
|
||||
"workingDirectory": filepath.Join(t.TempDir(), "missing"),
|
||||
},
|
||||
"turn-local",
|
||||
func(map[string]any) {},
|
||||
)
|
||||
if result.err != nil {
|
||||
t.Fatalf("expected structured response, got rpc error: %v", result.err)
|
||||
}
|
||||
if success, _ := result.response["success"].(bool); success {
|
||||
t.Fatalf("expected unavailable response, got %#v", result.response)
|
||||
}
|
||||
if got := result.response["error"]; got != "provider is not advertised by the bridge" {
|
||||
t.Fatalf("expected provider unavailable error, got %#v", result.response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleRPCRequiresExplicitBearerForExternalProvider(t *testing.T) {
|
||||
externalServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.Header.Get("Authorization"); got != "Bearer synced-provider-token" {
|
||||
t.Fatalf("expected explicit synced provider bearer header, got %q", got)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "run-auth",
|
||||
"result": map[string]any{
|
||||
"success": true,
|
||||
"output": "forwarded-auth-ok",
|
||||
},
|
||||
})
|
||||
}))
|
||||
defer externalServer.Close()
|
||||
|
||||
t.Setenv("INTERNAL_SERVICE_TOKEN", "synced-provider-token")
|
||||
t.Setenv("BRIDGE_AUTH_TOKEN", "")
|
||||
server := NewServer()
|
||||
setTestBridgeProvider(server, syncedProvider{
|
||||
ProviderID: "codex",
|
||||
Label: "Codex",
|
||||
Endpoint: externalServer.URL,
|
||||
AuthorizationHeader: "Bearer synced-provider-token",
|
||||
Enabled: true,
|
||||
})
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(
|
||||
http.MethodPost,
|
||||
"http://127.0.0.1/acp/rpc",
|
||||
strings.NewReader(`{"jsonrpc":"2.0","id":"run-auth","method":"session.start","params":{"sessionId":"s1","threadId":"t1","taskPrompt":"hello","workingDirectory":"`+t.TempDir()+`","routing":{"routingMode":"explicit","explicitExecutionTarget":"singleAgent","explicitProviderId":"codex"}}}`),
|
||||
)
|
||||
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(), "forwarded-auth-ok") {
|
||||
t.Fatalf("expected forwarded provider response, got %q", recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestExternalACPNotificationCollectorSynthesizesOutputAndWorkspace(t *testing.T) {
|
||||
collector := &externalACPNotificationCollector{}
|
||||
collector.observe(map[string]any{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "session.update",
|
||||
"params": map[string]any{
|
||||
"sessionId": "session-streamed",
|
||||
"threadId": "thread-streamed",
|
||||
"turnId": "turn-streamed",
|
||||
"type": "delta",
|
||||
"delta": "streamed external output",
|
||||
"resolvedWorkingDirectory": "/tmp/thread-streamed",
|
||||
"pending": false,
|
||||
"error": false,
|
||||
},
|
||||
})
|
||||
|
||||
result := collector.apply(map[string]any{
|
||||
"success": true,
|
||||
})
|
||||
|
||||
if got := result["output"]; got != "streamed external output" {
|
||||
t.Fatalf("expected synthesized output from notifications, got %#v", result)
|
||||
}
|
||||
if got := result["summary"]; got != "streamed external output" {
|
||||
t.Fatalf("expected synthesized summary from notifications, got %#v", result)
|
||||
}
|
||||
if got := result["turnId"]; got != "turn-streamed" {
|
||||
t.Fatalf("expected synthesized turnId, got %#v", result)
|
||||
}
|
||||
if got := result["resolvedWorkingDirectory"]; got != "/tmp/thread-streamed" {
|
||||
t.Fatalf("expected synthesized working directory, got %#v", result)
|
||||
|
||||
if got := p.AuthorizationHeader; got != "Bearer dedicated-token" {
|
||||
t.Fatalf("expected dedicated bearer header, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user