Sync module imports across agentic, mcp, ml, and rag packages. Add .gitignore for mlx build artifacts. Co-Authored-By: Virgil <virgil@lethean.io>
237 lines
5.3 KiB
Go
237 lines
5.3 KiB
Go
package ide
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"forge.lthn.ai/core/go/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)
|
|
}
|
|
}
|