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
155 lines
4.4 KiB
Go
155 lines
4.4 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
agentruntime "github.com/tutti-os/tutti/packages/agent/daemon/runtime"
|
|
)
|
|
|
|
type scriptedConnection struct {
|
|
frames chan agentruntime.ProcessFrame
|
|
mu sync.Mutex
|
|
sent [][]byte
|
|
}
|
|
|
|
func newScriptedConnection() *scriptedConnection {
|
|
return &scriptedConnection{frames: make(chan agentruntime.ProcessFrame, 16)}
|
|
}
|
|
|
|
func (connection *scriptedConnection) Send(data []byte) error {
|
|
connection.mu.Lock()
|
|
connection.sent = append(connection.sent, append([]byte(nil), data...))
|
|
connection.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (connection *scriptedConnection) Recv() (agentruntime.ProcessFrame, error) {
|
|
frame, ok := <-connection.frames
|
|
if !ok {
|
|
return agentruntime.ProcessFrame{}, context.Canceled
|
|
}
|
|
return frame, nil
|
|
}
|
|
|
|
func (connection *scriptedConnection) Close() error { close(connection.frames); return nil }
|
|
|
|
func (connection *scriptedConnection) push(value any) {
|
|
data, _ := json.Marshal(value)
|
|
connection.frames <- agentruntime.ProcessFrame{Stdout: append(data, '\n')}
|
|
}
|
|
|
|
func TestStdioClientDeliversNotificationsAndDeclinesServerRequestsByDefault(t *testing.T) {
|
|
connection := newScriptedConnection()
|
|
notifications := make(chan Notification, 1)
|
|
client, err := NewStdioClient(StdioClientConfig{
|
|
Connection: connection,
|
|
NotificationHandler: func(notification Notification) {
|
|
notifications <- notification
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if client == nil {
|
|
t.Fatal("stdio client was not created")
|
|
}
|
|
connection.push(map[string]any{"jsonrpc": "2.0", "method": "notifications/tools/list_changed", "params": map[string]any{}})
|
|
connection.push(map[string]any{"jsonrpc": "2.0", "id": 42, "method": "elicitation/create", "params": map[string]any{}})
|
|
|
|
select {
|
|
case notification := <-notifications:
|
|
if notification.Method != "notifications/tools/list_changed" {
|
|
t.Fatalf("notification = %#v", notification)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("notification was not delivered")
|
|
}
|
|
deadline := time.Now().Add(time.Second)
|
|
for time.Now().Before(deadline) {
|
|
connection.mu.Lock()
|
|
sent := append([][]byte(nil), connection.sent...)
|
|
connection.mu.Unlock()
|
|
if len(sent) != 0 {
|
|
var response struct {
|
|
Error *RPCError `json:"error"`
|
|
}
|
|
if err := json.Unmarshal(sent[0], &response); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if response.Error == nil || response.Error.Code != -32601 {
|
|
t.Fatalf("response = %s", sent[0])
|
|
}
|
|
return
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
t.Fatal("server request response was not sent")
|
|
}
|
|
|
|
func TestStdioClientFailsClosedOnOversizedMessage(t *testing.T) {
|
|
connection := newScriptedConnection()
|
|
client, err := NewStdioClient(StdioClientConfig{Connection: connection, MaxMessageBytes: 16})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
connection.frames <- agentruntime.ProcessFrame{Stdout: []byte(strings.Repeat("x", 17))}
|
|
deadline := time.Now().Add(time.Second)
|
|
for !client.IsClosed() && time.Now().Before(deadline) {
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
if err := client.ClosedError(); err == nil || !strings.Contains(err.Error(), "exceeds limit") {
|
|
t.Fatalf("ClosedError() = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestStdioClientNotificationHandlerCanIssueNestedCall(t *testing.T) {
|
|
connection := newScriptedConnection()
|
|
completed := make(chan error, 1)
|
|
var client *StdioClient
|
|
client, err := NewStdioClient(StdioClientConfig{
|
|
Connection: connection,
|
|
NotificationHandler: func(Notification) {
|
|
_, callErr := client.Call(context.Background(), "tools/list", map[string]any{})
|
|
completed <- callErr
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
connection.push(map[string]any{"jsonrpc": "2.0", "method": "notifications/tools/list_changed"})
|
|
|
|
deadline := time.Now().Add(time.Second)
|
|
var request struct {
|
|
ID int64 `json:"id"`
|
|
}
|
|
for time.Now().Before(deadline) {
|
|
connection.mu.Lock()
|
|
sent := append([][]byte(nil), connection.sent...)
|
|
connection.mu.Unlock()
|
|
if len(sent) != 0 {
|
|
if err := json.Unmarshal(sent[0], &request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
break
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
if request.ID == 0 {
|
|
t.Fatal("notification handler did not issue nested call")
|
|
}
|
|
connection.push(map[string]any{"jsonrpc": "2.0", "id": request.ID, "result": map[string]any{"tools": []any{}}})
|
|
select {
|
|
case err := <-completed:
|
|
if err != nil {
|
|
t.Fatalf("nested Call: %v", err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("nested Call deadlocked the protocol reader")
|
|
}
|
|
}
|