Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

230 lines
6.9 KiB
Go

package agentgateway
import (
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
connectormcpserver "github.com/tutti-os/tutti/packages/connector/runtime/mcpserver"
)
type backendStub struct {
server *httptest.Server
token string
mu sync.Mutex
revokeAlls int
}
type scopedBackendStub struct {
*backendStub
mu sync.Mutex
scopes []connectormcpserver.BindingScope
}
func (backend *scopedBackendStub) Bind(scope connectormcpserver.BindingScope) (connectormcpserver.Binding, error) {
backend.mu.Lock()
backend.scopes = append(backend.scopes, scope)
backend.mu.Unlock()
return backend.Binding(scope.WorkspaceID, scope.AgentSessionID)
}
func newBackendStub(t *testing.T, token, response string) *backendStub {
t.Helper()
backend := &backendStub{token: token}
backend.server = httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
if request.URL.Path != connectorMCPPath {
http.NotFound(writer, request)
return
}
if request.Header.Get("Authorization") != "Bearer "+token {
http.Error(writer, "invalid backend authorization", http.StatusUnauthorized)
return
}
_, _ = writer.Write([]byte(response))
}))
t.Cleanup(backend.server.Close)
return backend
}
func (backend *backendStub) Binding(string, string) (connectormcpserver.Binding, error) {
return connectormcpserver.Binding{Name: "connector", Type: "http",
URL: backend.server.URL + connectorMCPPath,
Headers: map[string]string{"Authorization": "Bearer " + backend.token}}, nil
}
func (*backendStub) Revoke(string, string) {}
func (backend *backendStub) RevokeAll() {
backend.mu.Lock()
backend.revokeAlls++
backend.mu.Unlock()
}
func TestGatewayKeepsAgentBindingAcrossBackendReplacement(t *testing.T) {
gateway, err := Start(Config{})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = gateway.Close(context.Background()) })
first := newBackendStub(t, "backend-one", "one")
if err := gateway.SetBackend("runtime-1", first); err != nil {
t.Fatal(err)
}
binding, err := gateway.Binding("workspace-1", "session-1")
if err != nil {
t.Fatal(err)
}
if got := callGateway(t, binding); got != "one" {
t.Fatalf("first response = %q", got)
}
second := newBackendStub(t, "backend-two", "two")
if err := gateway.SetBackend("runtime-2", second); err != nil {
t.Fatal(err)
}
if got := callGateway(t, binding); got != "two" {
t.Fatalf("replacement response = %q", got)
}
first.mu.Lock()
revoked := first.revokeAlls
first.mu.Unlock()
if revoked != 1 {
t.Fatalf("previous backend revoke-all count = %d", revoked)
}
}
func TestGatewayHotRebindKeepsAgentBearerOnlyWithinInvocationScope(t *testing.T) {
gateway, err := Start(Config{})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = gateway.Close(context.Background()) })
backend := &scopedBackendStub{backendStub: newBackendStub(t, "backend", "ready")}
if err := gateway.SetBackend("runtime-1", backend); err != nil {
t.Fatal(err)
}
binding, err := gateway.Bind(connectormcpserver.BindingScope{
WorkspaceID: "workspace-1", AgentSessionID: "session-1",
InvocationID: "invocation-1", InvocationGeneration: 1,
})
if err != nil {
t.Fatal(err)
}
if got := callGateway(t, binding); got != "ready" {
t.Fatalf("initial response = %q", got)
}
if err := gateway.Rebind(connectormcpserver.BindingScope{
WorkspaceID: "workspace-1", AgentSessionID: "session-1",
InvocationID: "invocation-1", InvocationGeneration: 1,
}); err != nil {
t.Fatal(err)
}
if got := callGateway(t, binding); got != "ready" {
t.Fatalf("rebound response = %q", got)
}
backend.mu.Lock()
scopes := append([]connectormcpserver.BindingScope(nil), backend.scopes...)
backend.mu.Unlock()
if len(scopes) != 2 || scopes[0].InvocationID != "invocation-1" || scopes[0].InvocationGeneration != 1 ||
scopes[1] != scopes[0] {
t.Fatalf("backend scopes = %#v", scopes)
}
if err := gateway.Rebind(connectormcpserver.BindingScope{
WorkspaceID: "workspace-1", AgentSessionID: "session-1",
InvocationID: "invocation-2", InvocationGeneration: 2,
}); err == nil {
t.Fatal("Rebind moved a stable Agent bearer across Invocation scopes")
}
}
func TestGatewayRotateBindingRevokesPreviousInvocationBearer(t *testing.T) {
gateway, err := Start(Config{})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = gateway.Close(context.Background()) })
backend := &scopedBackendStub{backendStub: newBackendStub(t, "backend", "ready")}
if err := gateway.SetBackend("runtime-1", backend); err != nil {
t.Fatal(err)
}
first, err := gateway.Bind(connectormcpserver.BindingScope{
WorkspaceID: "workspace-1", AgentSessionID: "session-1",
InvocationID: "invocation-1", InvocationGeneration: 1,
})
if err != nil {
t.Fatal(err)
}
second, err := gateway.RotateBinding(connectormcpserver.BindingScope{
WorkspaceID: "workspace-1", AgentSessionID: "session-1",
InvocationID: "invocation-2", InvocationGeneration: 2,
})
if err != nil {
t.Fatal(err)
}
request, _ := http.NewRequest(http.MethodPost, first.URL, strings.NewReader(`{"jsonrpc":"2.0"}`))
request.Header.Set("Authorization", first.Headers["Authorization"])
response, err := http.DefaultClient.Do(request)
if err != nil {
t.Fatal(err)
}
response.Body.Close()
if response.StatusCode != http.StatusUnauthorized {
t.Fatalf("previous Invocation bearer status = %d", response.StatusCode)
}
if got := callGateway(t, second); got != "ready" {
t.Fatalf("rotated response = %q", got)
}
}
func TestGatewayRequiresScopedBackendForInvocationBinding(t *testing.T) {
gateway, err := Start(Config{})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = gateway.Close(context.Background()) })
if err := gateway.SetBackend("runtime-1", newBackendStub(t, "backend", "ready")); err != nil {
t.Fatal(err)
}
binding, err := gateway.Bind(connectormcpserver.BindingScope{
WorkspaceID: "workspace-1", AgentSessionID: "session-1",
InvocationID: "invocation-1", InvocationGeneration: 1,
})
if err != nil {
t.Fatal(err)
}
request, _ := http.NewRequest(http.MethodPost, binding.URL, strings.NewReader(`{"jsonrpc":"2.0"}`))
request.Header.Set("Authorization", binding.Headers["Authorization"])
response, err := http.DefaultClient.Do(request)
if err != nil {
t.Fatal(err)
}
response.Body.Close()
if response.StatusCode != http.StatusServiceUnavailable {
t.Fatalf("unscoped backend status = %d", response.StatusCode)
}
}
func callGateway(t *testing.T, binding connectormcpserver.Binding) string {
t.Helper()
request, err := http.NewRequest(http.MethodPost, binding.URL, strings.NewReader(`{"jsonrpc":"2.0"}`))
if err != nil {
t.Fatal(err)
}
request.Header.Set("Authorization", binding.Headers["Authorization"])
response, err := http.DefaultClient.Do(request)
if err != nil {
t.Fatal(err)
}
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
t.Fatal(err)
}
if response.StatusCode != http.StatusOK {
t.Fatalf("gateway status = %d body=%q", response.StatusCode, body)
}
return string(body)
}