go-ai/mcp/ide/bridge_test.go

238 lines
5.3 KiB
Go
Raw Normal View History

package ide
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"forge.lthn.ai/core/cli/pkg/ws"
"github.com/gorilla/websocket"
)
var testUpgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
// echoServer creates a test WebSocket server that echoes messages back.
func echoServer(t *testing.T) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := testUpgrader.Upgrade(w, r, nil)
if err != nil {
t.Logf("upgrade error: %v", err)
return
}
defer conn.Close()
for {
mt, data, err := conn.ReadMessage()
if err != nil {
break
}
if err := conn.WriteMessage(mt, data); err != nil {
break
}
}
}))
}
func wsURL(ts *httptest.Server) string {
return "ws" + strings.TrimPrefix(ts.URL, "http")
}
func TestBridge_Good_ConnectAndSend(t *testing.T) {
ts := echoServer(t)
defer ts.Close()
hub := ws.NewHub()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go hub.Run(ctx)
cfg := DefaultConfig()
cfg.LaravelWSURL = wsURL(ts)
cfg.ReconnectInterval = 100 * time.Millisecond
bridge := NewBridge(hub, cfg)
bridge.Start(ctx)
// Wait for connection
deadline := time.Now().Add(2 * time.Second)
for !bridge.Connected() && time.Now().Before(deadline) {
time.Sleep(50 * time.Millisecond)
}
if !bridge.Connected() {
t.Fatal("bridge did not connect within timeout")
}
err := bridge.Send(BridgeMessage{
Type: "test",
Data: "hello",
})
if err != nil {
t.Fatalf("Send() failed: %v", err)
}
}
func TestBridge_Good_Shutdown(t *testing.T) {
ts := echoServer(t)
defer ts.Close()
hub := ws.NewHub()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go hub.Run(ctx)
cfg := DefaultConfig()
cfg.LaravelWSURL = wsURL(ts)
cfg.ReconnectInterval = 100 * time.Millisecond
bridge := NewBridge(hub, cfg)
bridge.Start(ctx)
deadline := time.Now().Add(2 * time.Second)
for !bridge.Connected() && time.Now().Before(deadline) {
time.Sleep(50 * time.Millisecond)
}
bridge.Shutdown()
if bridge.Connected() {
t.Error("bridge should be disconnected after Shutdown")
}
}
func TestBridge_Bad_SendWithoutConnection(t *testing.T) {
hub := ws.NewHub()
cfg := DefaultConfig()
bridge := NewBridge(hub, cfg)
err := bridge.Send(BridgeMessage{Type: "test"})
if err == nil {
t.Error("expected error when sending without connection")
}
}
func TestBridge_Good_MessageDispatch(t *testing.T) {
// Server that sends a message to the bridge on connect.
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, err := testUpgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer conn.Close()
msg := BridgeMessage{
Type: "chat_response",
Channel: "chat:session-1",
Data: "hello from laravel",
}
data, _ := json.Marshal(msg)
conn.WriteMessage(websocket.TextMessage, data)
// Keep connection open
for {
_, _, err := conn.ReadMessage()
if err != nil {
break
}
}
}))
defer ts.Close()
hub := ws.NewHub()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go hub.Run(ctx)
cfg := DefaultConfig()
cfg.LaravelWSURL = wsURL(ts)
cfg.ReconnectInterval = 100 * time.Millisecond
bridge := NewBridge(hub, cfg)
bridge.Start(ctx)
deadline := time.Now().Add(2 * time.Second)
for !bridge.Connected() && time.Now().Before(deadline) {
time.Sleep(50 * time.Millisecond)
}
if !bridge.Connected() {
t.Fatal("bridge did not connect within timeout")
}
// Give time for the dispatched message to be processed.
time.Sleep(200 * time.Millisecond)
// Verify hub stats — the message was dispatched (even without subscribers).
// This confirms the dispatch path ran without error.
}
func TestBridge_Good_Reconnect(t *testing.T) {
callCount := 0
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
conn, err := testUpgrader.Upgrade(w, r, nil)
if err != nil {
return
}
// Close immediately on first connection to force reconnect
if callCount == 1 {
conn.Close()
return
}
defer conn.Close()
for {
_, _, err := conn.ReadMessage()
if err != nil {
break
}
}
}))
defer ts.Close()
hub := ws.NewHub()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go hub.Run(ctx)
cfg := DefaultConfig()
cfg.LaravelWSURL = wsURL(ts)
cfg.ReconnectInterval = 100 * time.Millisecond
cfg.MaxReconnectInterval = 200 * time.Millisecond
bridge := NewBridge(hub, cfg)
bridge.Start(ctx)
// Wait long enough for a reconnect cycle
deadline := time.Now().Add(3 * time.Second)
for !bridge.Connected() && time.Now().Before(deadline) {
time.Sleep(50 * time.Millisecond)
}
if !bridge.Connected() {
t.Fatal("bridge did not reconnect within timeout")
}
if callCount < 2 {
t.Errorf("expected at least 2 connection attempts, got %d", callCount)
}
}
func TestSubsystem_Good_Name(t *testing.T) {
sub := New(nil)
if sub.Name() != "ide" {
t.Errorf("expected name 'ide', got %q", sub.Name())
}
}
func TestSubsystem_Good_NilHub(t *testing.T) {
sub := New(nil)
if sub.Bridge() != nil {
t.Error("expected nil bridge when hub is nil")
}
// Shutdown should not panic
if err := sub.Shutdown(context.Background()); err != nil {
t.Errorf("Shutdown with nil bridge failed: %v", err)
}
}