- Added functionality to build and manage MCP configuration for sandbox environments, allowing for dynamic tool execution. - Enhanced the Assistant's Stream method to skip MCP tool calls in sandbox mode, with internal handling by Claude CLI. - Introduced unit tests for MCP configuration building and skills directory resolution, ensuring robust integration. - Updated sandbox manager to create IPC sessions and manage tool exposure dynamically, improving interaction with external agents. - Enhanced documentation to reflect new features and integration points for MCP and skills within the sandbox.
642 lines
16 KiB
Go
642 lines
16 KiB
Go
package ipc
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestNewManager tests IPC manager creation
|
|
func TestNewManager(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("/tmp", "ipc-manager-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
m := NewManager(tmpDir)
|
|
if m == nil {
|
|
t.Fatal("NewManager returned nil")
|
|
}
|
|
|
|
if m.sockDir != tmpDir {
|
|
t.Errorf("Expected sockDir %s, got %s", tmpDir, m.sockDir)
|
|
}
|
|
}
|
|
|
|
// TestCreateSession tests creating an IPC session
|
|
func TestCreateSession(t *testing.T) {
|
|
// Use /tmp directly to avoid long paths (Unix socket path limit ~104 bytes)
|
|
tmpDir, err := os.MkdirTemp("/tmp", "ipc-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
m := NewManager(tmpDir)
|
|
ctx := context.Background()
|
|
|
|
sessionID := "test-session-1"
|
|
agentCtx := &AgentContext{
|
|
UserID: "user1",
|
|
ChatID: "chat1",
|
|
Locale: "en-US",
|
|
}
|
|
|
|
mcpTools := map[string]*MCPTool{
|
|
"test_tool": {
|
|
Name: "test_tool",
|
|
Description: "A test tool",
|
|
Process: "scripts.test.hello",
|
|
InputSchema: json.RawMessage(`{"type":"object","properties":{"name":{"type":"string"}}}`),
|
|
},
|
|
}
|
|
|
|
session, err := m.Create(ctx, sessionID, agentCtx, mcpTools)
|
|
if err != nil {
|
|
t.Fatalf("Create session failed: %v", err)
|
|
}
|
|
defer m.Close(sessionID)
|
|
|
|
// Verify session properties
|
|
if session.ID != sessionID {
|
|
t.Errorf("Expected session ID %s, got %s", sessionID, session.ID)
|
|
}
|
|
|
|
// Socket path uses hash now, just verify it's in the right directory and ends with .sock
|
|
if !strings.HasPrefix(session.SocketPath, tmpDir) {
|
|
t.Errorf("Socket path should be in %s, got %s", tmpDir, session.SocketPath)
|
|
}
|
|
if !strings.HasSuffix(session.SocketPath, ".sock") {
|
|
t.Errorf("Socket path should end with .sock, got %s", session.SocketPath)
|
|
}
|
|
|
|
if session.Context.UserID != "user1" {
|
|
t.Errorf("Expected UserID user1, got %s", session.Context.UserID)
|
|
}
|
|
|
|
if len(session.MCPTools) != 1 {
|
|
t.Errorf("Expected 1 MCP tool, got %d", len(session.MCPTools))
|
|
}
|
|
|
|
// Verify socket file exists
|
|
if _, err := os.Stat(session.SocketPath); os.IsNotExist(err) {
|
|
t.Error("Socket file should exist")
|
|
}
|
|
}
|
|
|
|
// TestGetSession tests retrieving a session
|
|
func TestGetSession(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("/tmp", "ipc-get-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
m := NewManager(tmpDir)
|
|
ctx := context.Background()
|
|
|
|
sessionID := "test-get-session"
|
|
agentCtx := &AgentContext{UserID: "user1", ChatID: "chat1"}
|
|
|
|
// Get non-existent session
|
|
_, ok := m.Get(sessionID)
|
|
if ok {
|
|
t.Error("Get should return false for non-existent session")
|
|
}
|
|
|
|
// Create session
|
|
_, err = m.Create(ctx, sessionID, agentCtx, nil)
|
|
if err != nil {
|
|
t.Fatalf("Create session failed: %v", err)
|
|
}
|
|
defer m.Close(sessionID)
|
|
|
|
// Get existing session
|
|
session, ok := m.Get(sessionID)
|
|
if !ok {
|
|
t.Error("Get should return true for existing session")
|
|
}
|
|
|
|
if session.ID != sessionID {
|
|
t.Errorf("Expected session ID %s, got %s", sessionID, session.ID)
|
|
}
|
|
}
|
|
|
|
// TestCloseSession tests closing a session
|
|
func TestCloseSession(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("/tmp", "ipc-close-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
m := NewManager(tmpDir)
|
|
ctx := context.Background()
|
|
|
|
sessionID := "test-close-session"
|
|
agentCtx := &AgentContext{UserID: "user1", ChatID: "chat1"}
|
|
|
|
session, err := m.Create(ctx, sessionID, agentCtx, nil)
|
|
if err != nil {
|
|
t.Fatalf("Create session failed: %v", err)
|
|
}
|
|
|
|
socketPath := session.SocketPath
|
|
|
|
// Close session
|
|
err = m.Close(sessionID)
|
|
if err != nil {
|
|
t.Fatalf("Close session failed: %v", err)
|
|
}
|
|
|
|
// Verify session is removed
|
|
_, ok := m.Get(sessionID)
|
|
if ok {
|
|
t.Error("Session should be removed after close")
|
|
}
|
|
|
|
// Verify socket file is removed (give it a moment)
|
|
time.Sleep(100 * time.Millisecond)
|
|
if _, err := os.Stat(socketPath); !os.IsNotExist(err) {
|
|
t.Error("Socket file should be removed after close")
|
|
}
|
|
}
|
|
|
|
// TestCloseNonExistentSession tests closing a non-existent session
|
|
func TestCloseNonExistentSession(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("/tmp", "ipc-close-nonexist-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
m := NewManager(tmpDir)
|
|
|
|
// Should not error
|
|
err = m.Close("nonexistent-session")
|
|
if err != nil {
|
|
t.Errorf("Close non-existent session should not error: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestCloseAllSessions tests closing all sessions
|
|
func TestCloseAllSessions(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("/tmp", "ipc-closeall-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
m := NewManager(tmpDir)
|
|
ctx := context.Background()
|
|
|
|
// Create multiple sessions
|
|
sessionIDs := []string{"session-1", "session-2", "session-3"}
|
|
for _, id := range sessionIDs {
|
|
_, err := m.Create(ctx, id, &AgentContext{UserID: "user", ChatID: id}, nil)
|
|
if err != nil {
|
|
t.Fatalf("Create session %s failed: %v", id, err)
|
|
}
|
|
}
|
|
|
|
// Verify sessions exist
|
|
for _, id := range sessionIDs {
|
|
if _, ok := m.Get(id); !ok {
|
|
t.Errorf("Session %s should exist", id)
|
|
}
|
|
}
|
|
|
|
// Close all
|
|
m.CloseAll()
|
|
|
|
// Verify all sessions are removed
|
|
time.Sleep(100 * time.Millisecond)
|
|
for _, id := range sessionIDs {
|
|
if _, ok := m.Get(id); ok {
|
|
t.Errorf("Session %s should be removed after CloseAll", id)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSessionReplace tests that creating a session with existing ID replaces it
|
|
func TestSessionReplace(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("/tmp", "ipc-replace-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
m := NewManager(tmpDir)
|
|
ctx := context.Background()
|
|
|
|
sessionID := "test-replace-session"
|
|
|
|
// Create first session
|
|
session1, err := m.Create(ctx, sessionID, &AgentContext{UserID: "user1", ChatID: "chat1"}, nil)
|
|
if err != nil {
|
|
t.Fatalf("Create first session failed: %v", err)
|
|
}
|
|
socketPath1 := session1.SocketPath
|
|
|
|
// Create second session with same ID
|
|
session2, err := m.Create(ctx, sessionID, &AgentContext{UserID: "user2", ChatID: "chat2"}, nil)
|
|
if err != nil {
|
|
t.Fatalf("Create second session failed: %v", err)
|
|
}
|
|
defer m.Close(sessionID)
|
|
|
|
// Verify second session replaced first
|
|
if session2.Context.UserID != "user2" {
|
|
t.Errorf("Expected UserID user2, got %s", session2.Context.UserID)
|
|
}
|
|
|
|
// Get session should return second
|
|
session, ok := m.Get(sessionID)
|
|
if !ok {
|
|
t.Error("Get should return session")
|
|
}
|
|
if session.Context.UserID != "user2" {
|
|
t.Errorf("Expected UserID user2 from Get, got %s", session.Context.UserID)
|
|
}
|
|
|
|
// Same socket path should be reused
|
|
if session2.SocketPath != socketPath1 {
|
|
t.Errorf("Expected same socket path, got %s vs %s", socketPath1, session2.SocketPath)
|
|
}
|
|
}
|
|
|
|
// TestConcurrentSessionAccess tests concurrent access to sessions
|
|
func TestConcurrentSessionAccess(t *testing.T) {
|
|
// Use /tmp for shorter socket path (macOS has 104 char limit for Unix sockets)
|
|
tmpDir, err := os.MkdirTemp("/tmp", "ipc-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
m := NewManager(tmpDir)
|
|
ctx := context.Background()
|
|
|
|
var wg sync.WaitGroup
|
|
var mu sync.Mutex
|
|
errors := make([]error, 0)
|
|
numGoroutines := 5 // Reduced for stability
|
|
|
|
// Concurrent creates
|
|
for i := 0; i < numGoroutines; i++ {
|
|
wg.Add(1)
|
|
go func(idx int) {
|
|
defer wg.Done()
|
|
sessionID := fmt.Sprintf("s%d", idx) // Short session ID
|
|
_, err := m.Create(ctx, sessionID, &AgentContext{UserID: "user", ChatID: sessionID}, nil)
|
|
if err != nil {
|
|
mu.Lock()
|
|
errors = append(errors, fmt.Errorf("session %s: %v", sessionID, err))
|
|
mu.Unlock()
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
|
|
// Check errors
|
|
for _, err := range errors {
|
|
t.Errorf("Concurrent create error: %v", err)
|
|
}
|
|
|
|
// Verify all sessions exist
|
|
for i := 0; i < numGoroutines; i++ {
|
|
sessionID := fmt.Sprintf("s%d", i)
|
|
if _, ok := m.Get(sessionID); !ok {
|
|
t.Errorf("Session %s should exist", sessionID)
|
|
}
|
|
}
|
|
|
|
// Cleanup
|
|
m.CloseAll()
|
|
}
|
|
|
|
// TestSessionConnection tests connecting to a session socket
|
|
func TestSessionConnection(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("/tmp", "ipc-connect-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
m := NewManager(tmpDir)
|
|
ctx := context.Background()
|
|
|
|
sessionID := "test-connect-session"
|
|
agentCtx := &AgentContext{UserID: "user1", ChatID: "chat1"}
|
|
mcpTools := map[string]*MCPTool{}
|
|
|
|
session, err := m.Create(ctx, sessionID, agentCtx, mcpTools)
|
|
if err != nil {
|
|
t.Fatalf("Create session failed: %v", err)
|
|
}
|
|
defer m.Close(sessionID)
|
|
|
|
// Give the listener time to start
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Try to connect to the socket
|
|
conn, err := net.Dial("unix", session.SocketPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to connect to socket: %v", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
// Send initialize request
|
|
initReq := JSONRPCRequest{
|
|
JSONRPC: "2.0",
|
|
ID: 1,
|
|
Method: "initialize",
|
|
Params: json.RawMessage(`{"protocolVersion":"2024-11-05"}`),
|
|
}
|
|
data, _ := json.Marshal(initReq)
|
|
|
|
// Write with newline (NDJSON)
|
|
_, err = conn.Write(append(data, '\n'))
|
|
if err != nil {
|
|
t.Fatalf("Failed to write to socket: %v", err)
|
|
}
|
|
|
|
// Set read deadline
|
|
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
|
|
|
// Read response
|
|
buf := make([]byte, 4096)
|
|
n, err := conn.Read(buf)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read from socket: %v", err)
|
|
}
|
|
|
|
// Parse response
|
|
var resp JSONRPCResponse
|
|
if err := json.Unmarshal(buf[:n], &resp); err != nil {
|
|
t.Fatalf("Failed to parse response: %v (raw: %s)", err, string(buf[:n]))
|
|
}
|
|
|
|
if resp.JSONRPC != "2.0" {
|
|
t.Errorf("Expected JSONRPC 2.0, got %s", resp.JSONRPC)
|
|
}
|
|
|
|
if resp.Error != nil {
|
|
t.Errorf("Unexpected error: %v", resp.Error)
|
|
}
|
|
|
|
if resp.Result == nil {
|
|
t.Error("Expected result, got nil")
|
|
}
|
|
}
|
|
|
|
// TestToolsList tests the tools/list method
|
|
func TestToolsList(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("/tmp", "ipc-tools-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
m := NewManager(tmpDir)
|
|
ctx := context.Background()
|
|
|
|
sessionID := "test-tools-session"
|
|
agentCtx := &AgentContext{UserID: "user1", ChatID: "chat1"}
|
|
mcpTools := map[string]*MCPTool{
|
|
"tool1": {
|
|
Name: "tool1",
|
|
Description: "First test tool",
|
|
Process: "scripts.test.tool1",
|
|
InputSchema: json.RawMessage(`{"type":"object"}`),
|
|
},
|
|
"tool2": {
|
|
Name: "tool2",
|
|
Description: "Second test tool",
|
|
Process: "scripts.test.tool2",
|
|
InputSchema: json.RawMessage(`{"type":"object","properties":{"arg":{"type":"string"}}}`),
|
|
},
|
|
}
|
|
|
|
session, err := m.Create(ctx, sessionID, agentCtx, mcpTools)
|
|
if err != nil {
|
|
t.Fatalf("Create session failed: %v", err)
|
|
}
|
|
defer m.Close(sessionID)
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
conn, err := net.Dial("unix", session.SocketPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to connect to socket: %v", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
// Send tools/list request
|
|
req := JSONRPCRequest{
|
|
JSONRPC: "2.0",
|
|
ID: 2,
|
|
Method: "tools/list",
|
|
}
|
|
data, _ := json.Marshal(req)
|
|
conn.Write(append(data, '\n'))
|
|
|
|
// Read response
|
|
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
|
buf := make([]byte, 4096)
|
|
n, err := conn.Read(buf)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read from socket: %v", err)
|
|
}
|
|
|
|
var resp JSONRPCResponse
|
|
if err := json.Unmarshal(buf[:n], &resp); err != nil {
|
|
t.Fatalf("Failed to parse response: %v", err)
|
|
}
|
|
|
|
if resp.Error != nil {
|
|
t.Fatalf("Unexpected error: %v", resp.Error)
|
|
}
|
|
|
|
// Parse result as ToolsListResult
|
|
resultBytes, _ := json.Marshal(resp.Result)
|
|
var toolsResult ToolsListResult
|
|
if err := json.Unmarshal(resultBytes, &toolsResult); err != nil {
|
|
t.Fatalf("Failed to parse tools result: %v", err)
|
|
}
|
|
|
|
if len(toolsResult.Tools) != 2 {
|
|
t.Errorf("Expected 2 tools, got %d", len(toolsResult.Tools))
|
|
}
|
|
|
|
// Verify tool names
|
|
toolNames := make(map[string]bool)
|
|
for _, tool := range toolsResult.Tools {
|
|
toolNames[tool.Name] = true
|
|
}
|
|
|
|
if !toolNames["tool1"] {
|
|
t.Error("Expected tool1 in tools list")
|
|
}
|
|
if !toolNames["tool2"] {
|
|
t.Error("Expected tool2 in tools list")
|
|
}
|
|
}
|
|
|
|
// TestMethodNotFound tests handling of unknown methods
|
|
func TestMethodNotFound(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("/tmp", "ipc-notfound-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
m := NewManager(tmpDir)
|
|
ctx := context.Background()
|
|
|
|
session, err := m.Create(ctx, "test-notfound", &AgentContext{UserID: "user", ChatID: "chat"}, nil)
|
|
if err != nil {
|
|
t.Fatalf("Create session failed: %v", err)
|
|
}
|
|
defer m.Close("test-notfound")
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
conn, err := net.Dial("unix", session.SocketPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to connect to socket: %v", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
// Send unknown method
|
|
req := JSONRPCRequest{
|
|
JSONRPC: "2.0",
|
|
ID: 3,
|
|
Method: "unknown/method",
|
|
}
|
|
data, _ := json.Marshal(req)
|
|
conn.Write(append(data, '\n'))
|
|
|
|
// Read response
|
|
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
|
buf := make([]byte, 4096)
|
|
n, err := conn.Read(buf)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read from socket: %v", err)
|
|
}
|
|
|
|
var resp JSONRPCResponse
|
|
if err := json.Unmarshal(buf[:n], &resp); err != nil {
|
|
t.Fatalf("Failed to parse response: %v", err)
|
|
}
|
|
|
|
if resp.Error == nil {
|
|
t.Error("Expected error for unknown method")
|
|
}
|
|
|
|
if resp.Error != nil && resp.Error.Code != ErrCodeMethodNotFound {
|
|
t.Errorf("Expected error code %d, got %d", ErrCodeMethodNotFound, resp.Error.Code)
|
|
}
|
|
}
|
|
|
|
// TestParseError tests handling of invalid JSON
|
|
func TestParseError(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("/tmp", "ipc-parse-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
m := NewManager(tmpDir)
|
|
ctx := context.Background()
|
|
|
|
session, err := m.Create(ctx, "test-parse", &AgentContext{UserID: "user", ChatID: "chat"}, nil)
|
|
if err != nil {
|
|
t.Fatalf("Create session failed: %v", err)
|
|
}
|
|
defer m.Close("test-parse")
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
conn, err := net.Dial("unix", session.SocketPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to connect to socket: %v", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
// Send invalid JSON
|
|
conn.Write([]byte("not valid json\n"))
|
|
|
|
// Read response
|
|
conn.SetReadDeadline(time.Now().Add(5 * time.Second))
|
|
buf := make([]byte, 4096)
|
|
n, err := conn.Read(buf)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read from socket: %v", err)
|
|
}
|
|
|
|
var resp JSONRPCResponse
|
|
if err := json.Unmarshal(buf[:n], &resp); err != nil {
|
|
t.Fatalf("Failed to parse response: %v", err)
|
|
}
|
|
|
|
if resp.Error == nil {
|
|
t.Error("Expected error for invalid JSON")
|
|
}
|
|
|
|
if resp.Error != nil && resp.Error.Code != ErrCodeParse {
|
|
t.Errorf("Expected error code %d, got %d", ErrCodeParse, resp.Error.Code)
|
|
}
|
|
}
|
|
|
|
// TestInitializedNotification tests that initialized notification doesn't return response
|
|
func TestInitializedNotification(t *testing.T) {
|
|
tmpDir, err := os.MkdirTemp("/tmp", "ipc-initialized-test-*")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
m := NewManager(tmpDir)
|
|
ctx := context.Background()
|
|
|
|
session, err := m.Create(ctx, "test-initialized", &AgentContext{UserID: "user", ChatID: "chat"}, nil)
|
|
if err != nil {
|
|
t.Fatalf("Create session failed: %v", err)
|
|
}
|
|
defer m.Close("test-initialized")
|
|
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
conn, err := net.Dial("unix", session.SocketPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to connect to socket: %v", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
// Send initialized notification (no ID = notification)
|
|
req := JSONRPCRequest{
|
|
JSONRPC: "2.0",
|
|
Method: "initialized",
|
|
}
|
|
data, _ := json.Marshal(req)
|
|
conn.Write(append(data, '\n'))
|
|
|
|
// Set short read deadline - we expect timeout since no response
|
|
conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
|
|
buf := make([]byte, 4096)
|
|
_, err = conn.Read(buf)
|
|
|
|
// Expect timeout (no response for notifications)
|
|
if err == nil {
|
|
t.Error("Expected no response for notification")
|
|
}
|
|
}
|