package bridge_test import ( "context" "net/http/httptest" "strings" "testing" "time" acp "github.com/coder/acp-go-sdk" "github.com/coder/websocket" "github.com/coder/websocket/wsjson" "github.com/bots-garden/ori/internal/bridge" ) // dialTestServer starts an HTTP test server exposing the bridge's WebSocket // handler and connects a client to it. func dialTestServer(t *testing.T, b *bridge.Bridge) (*websocket.Conn, context.Context) { t.Helper() server := httptest.NewServer(b.WebSocketHandler()) t.Cleanup(server.Close) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) t.Cleanup(cancel) url := "ws" + strings.TrimPrefix(server.URL, "http") conn, _, err := websocket.Dial(ctx, url, nil) if err != nil { t.Fatalf("websocket dial failed: %v", err) } t.Cleanup(func() { _ = conn.CloseNow() }) return conn, ctx } // readUntil reads messages until the wanted type arrives. func readUntil(t *testing.T, ctx context.Context, conn *websocket.Conn, wantType string) bridge.Outgoing { t.Helper() for { var msg bridge.Outgoing if err := wsjson.Read(ctx, conn, &msg); err != nil { t.Fatalf("websocket read failed while waiting for %q: %v", wantType, err) } if msg.Type == wantType { return msg } } } func TestWebSocketHelloThenPromptTurn(t *testing.T) { b := bridge.New(nil) b.SetSession(&fakeSession{}) conn, ctx := dialTestServer(t, b) hello := readUntil(t, ctx, conn, bridge.OutgoingHello) if hello.SessionId != "sess-fake" { t.Errorf("hello sessionId = %q, want sess-fake", hello.SessionId) } if err := wsjson.Write(ctx, conn, bridge.Incoming{Type: bridge.IncomingPrompt, Text: "hi"}); err != nil { t.Fatalf("websocket write failed: %v", err) } readUntil(t, ctx, conn, bridge.OutgoingTurnStarted) ended := readUntil(t, ctx, conn, bridge.OutgoingTurnEnded) if ended.StopReason != string(acp.StopReasonEndTurn) { t.Errorf("turn_ended stopReason = %q, want end_turn", ended.StopReason) } } func TestWebSocketReceivesSessionUpdates(t *testing.T) { b := bridge.New(nil) b.SetSession(&fakeSession{}) conn, ctx := dialTestServer(t, b) readUntil(t, ctx, conn, bridge.OutgoingHello) b.HandleSessionUpdate(context.Background(), acp.SessionNotification{ SessionId: "sess-fake", Update: acp.UpdateAgentMessageText("streamed"), }) update := readUntil(t, ctx, conn, bridge.OutgoingSessionUpdate) if update.Update == nil || !strings.Contains(string(update.Update), "streamed") { t.Errorf("session_update payload = %s, want it to carry the streamed text", update.Update) } } func TestWebSocketPermissionRoundTrip(t *testing.T) { b := bridge.New(nil) b.SetSession(&fakeSession{}) conn, ctx := dialTestServer(t, b) readUntil(t, ctx, conn, bridge.OutgoingHello) done := make(chan acp.RequestPermissionResponse, 1) go func() { resp, _ := b.HandlePermissionRequest(context.Background(), acp.RequestPermissionRequest{ SessionId: "sess-fake", Options: []acp.PermissionOption{{OptionId: "allow", Name: "Allow", Kind: acp.PermissionOptionKindAllowOnce}}, }) done <- resp }() request := readUntil(t, ctx, conn, bridge.OutgoingPermissionRequest) if err := wsjson.Write(ctx, conn, bridge.Incoming{ Type: bridge.IncomingPermissionResponse, RequestId: request.RequestId, OptionId: "allow", }); err != nil { t.Fatalf("websocket write failed: %v", err) } resp := <-done if resp.Outcome.Selected == nil || resp.Outcome.Selected.OptionId != "allow" { t.Errorf("outcome = %+v, want selected \"allow\"", resp.Outcome) } readUntil(t, ctx, conn, bridge.OutgoingPermissionResolved) } func TestWebSocketLateJoinerGetsReplay(t *testing.T) { b := bridge.New(nil) b.SetSession(&fakeSession{}) b.HandleSessionUpdate(context.Background(), acp.SessionNotification{ SessionId: "sess-fake", Update: acp.UpdateAgentMessageText("before you arrived"), }) conn, ctx := dialTestServer(t, b) readUntil(t, ctx, conn, bridge.OutgoingHello) update := readUntil(t, ctx, conn, bridge.OutgoingSessionUpdate) if !strings.Contains(string(update.Update), "before you arrived") { t.Errorf("replayed update = %s, want the pre-connection chunk", update.Update) } }