230 lines
5.7 KiB
Go
230 lines
5.7 KiB
Go
package acp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
func TestResolveSingleAgentForwardEndpointFromExampleConfig(t *testing.T) {
|
|
// Set the config path to example/config.yaml relative to this test file
|
|
t.Setenv("BRIDGE_CONFIG_PATH", "../../example/config.yaml")
|
|
|
|
_, catalog, order := newProductionProviderCatalog()
|
|
if len(order) == 0 {
|
|
t.Fatal("Expected non-empty provider order from example/config.yaml")
|
|
}
|
|
|
|
expectedEndpoints := map[string]string{
|
|
"codex": "ws://127.0.0.1:9001/acp",
|
|
"opencode": "http://127.0.0.1:38992/acp/rpc",
|
|
"gemini": "http://127.0.0.1:8791/acp/rpc",
|
|
"hermes": "ws://127.0.0.1:3920/acp",
|
|
}
|
|
|
|
for _, id := range order {
|
|
id := id
|
|
t.Run(id, func(t *testing.T) {
|
|
provider, ok := catalog[id]
|
|
if !ok {
|
|
t.Errorf("Provider %s missing from catalog", id)
|
|
return
|
|
}
|
|
if !provider.Enabled {
|
|
t.Errorf("Provider %s should be enabled in example config", id)
|
|
}
|
|
|
|
want := expectedEndpoints[id]
|
|
got := resolveSingleAgentForwardEndpoint(provider)
|
|
if got != want {
|
|
t.Errorf("resolveSingleAgentForwardEndpoint(%s) = %q, want %q (from example config)", id, got, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestResolveSingleAgentForwardEndpointManual(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := []struct {
|
|
name string
|
|
provider syncedProvider
|
|
want string
|
|
}{
|
|
{
|
|
name: "preserves http rpc endpoint",
|
|
provider: syncedProvider{
|
|
ProviderID: "custom",
|
|
Endpoint: "https://upstream-provider.example.com/acp/rpc",
|
|
},
|
|
want: "https://upstream-provider.example.com/acp/rpc",
|
|
},
|
|
{
|
|
name: "normalizes http acp endpoint to rpc endpoint",
|
|
provider: syncedProvider{
|
|
ProviderID: "opencode",
|
|
Endpoint: "http://127.0.0.1:39992/acp",
|
|
},
|
|
want: "http://127.0.0.1:39992/acp/rpc",
|
|
},
|
|
{
|
|
name: "normalizes websocket opencode endpoint to http rpc endpoint",
|
|
provider: syncedProvider{
|
|
ProviderID: "opencode",
|
|
Endpoint: "ws://127.0.0.1:39992/acp",
|
|
},
|
|
want: "http://127.0.0.1:39992/acp/rpc",
|
|
},
|
|
{
|
|
name: "does not duplicate nested acp path",
|
|
provider: syncedProvider{
|
|
ProviderID: "opencode",
|
|
Endpoint: "http://127.0.0.1:39992/acp",
|
|
},
|
|
want: "http://127.0.0.1:39992/acp/rpc",
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
if got := resolveSingleAgentForwardEndpoint(tc.provider); got != tc.want {
|
|
t.Fatalf("resolveSingleAgentForwardEndpoint() = %q, want %q", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormalizeAuthorizationHeader(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cases := map[string]string{
|
|
"": "",
|
|
"Bearer bridge": "Bearer bridge",
|
|
"bridge-token": "Bearer bridge-token",
|
|
" bridge-token ": "Bearer bridge-token",
|
|
}
|
|
for raw, want := range cases {
|
|
raw, want := raw, want
|
|
t.Run(raw, func(t *testing.T) {
|
|
t.Parallel()
|
|
if got := normalizeAuthorizationHeader(raw); got != want {
|
|
t.Fatalf("normalizeAuthorizationHeader(%q) = %q, want %q", raw, got, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCodexCompatTranslatesSessionLifecycleToThreadAndTurnRPC(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var methods []string
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
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)
|
|
}
|
|
method := stringValue(request["method"])
|
|
methods = append(methods, method)
|
|
result := map[string]any{}
|
|
switch method {
|
|
case "thread/start":
|
|
result["id"] = "codex-thread-1"
|
|
case "turn/start":
|
|
result["output"] = "pong"
|
|
default:
|
|
t.Fatalf("unexpected codex upstream method %q", method)
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"jsonrpc": "2.0",
|
|
"id": request["id"],
|
|
"result": result,
|
|
})
|
|
}))
|
|
defer upstream.Close()
|
|
|
|
compat := newProviderCompat(syncedProvider{
|
|
ProviderID: "codex",
|
|
Label: "Codex",
|
|
Endpoint: upstream.URL,
|
|
Enabled: true,
|
|
})
|
|
result, err := compat.StartSession(
|
|
context.Background(),
|
|
"session-1",
|
|
"thread-1",
|
|
map[string]any{
|
|
"taskPrompt": "Reply with exactly pong",
|
|
"workingDirectory": t.TempDir(),
|
|
},
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("StartSession failed: %v", err)
|
|
}
|
|
if got := result["output"]; got != "pong" {
|
|
t.Fatalf("expected pong output, got %#v", result)
|
|
}
|
|
if len(methods) != 2 || methods[0] != "thread/start" || methods[1] != "turn/start" {
|
|
t.Fatalf("expected thread/start then turn/start, got %#v", methods)
|
|
}
|
|
}
|
|
|
|
func TestExternalACPNotificationCollectorExtractsNestedSessionUpdateText(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
collector := &externalACPNotificationCollector{}
|
|
collector.observe(map[string]any{
|
|
"method": "session.update",
|
|
"params": map[string]any{
|
|
"turnId": "turn-1",
|
|
"update": map[string]any{
|
|
"sessionUpdate": "agent_message_chunk",
|
|
"content": map[string]any{
|
|
"text": "pong",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
result := collector.apply(map[string]any{})
|
|
if got := result["output"]; got != "pong" {
|
|
t.Fatalf("expected output pong, got %#v", result)
|
|
}
|
|
if got := result["turnId"]; got != "turn-1" {
|
|
t.Fatalf("expected turnId turn-1, got %#v", result)
|
|
}
|
|
}
|
|
|
|
func TestExternalACPNotificationCollectorPrefersStreamTextOverAckResult(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
collector := &externalACPNotificationCollector{}
|
|
collector.observe(map[string]any{
|
|
"method": "session.update",
|
|
"params": map[string]any{
|
|
"update": map[string]any{
|
|
"sessionUpdate": "agent_message_chunk",
|
|
"content": map[string]any{
|
|
"text": "pong",
|
|
},
|
|
},
|
|
},
|
|
})
|
|
|
|
result := collector.apply(map[string]any{
|
|
"output": "ok",
|
|
"summary": "ok",
|
|
"message": "ok",
|
|
})
|
|
if got := result["output"]; got != "pong" {
|
|
t.Fatalf("expected stream text to win over ack result, got %#v", result)
|
|
}
|
|
}
|