fix: populate all chat fields in pubsub events (#23664)

*Problem:* `publishChatPubsubEvent` was constructing a partial
`codersdk.Chat` that omitted `LastModelConfigID` and other fields. Go's
zero-value UUID caused the sidebar to show "Default model" for chats
received via SSE.

*Solution:*
- Extracted `convertChat`/`convertChats` from `exp_chats.go` into
`db2sdk.Chat`/`db2sdk.Chats`, alongside existing `ChatMessage`,
`ChatQueuedMessage`, and `ChatDiffStatus` converters.
`publishChatPubsubEvent` now calls `db2sdk.Chat(chat, nil)` instead of
maintaining its own copy of the conversion logic
- Added backend integration test
`TestWatchChats/CreatedEventIncludesAllChatFields`
- Added frontend regression tests for nil-UUID and valid model config ID
cases

> 🤖 Created by Coder Agents, reviewed by this human.
This commit is contained in:
Cian Johnston
2026-03-26 16:49:26 +00:00
committed by GitHub
parent 52b5d5fdc6
commit bfee7e6245
7 changed files with 343 additions and 99 deletions
+73
View File
@@ -775,6 +775,79 @@ func TestWatchChats(t *testing.T) {
}
})
t.Run("CreatedEventIncludesAllChatFields", func(t *testing.T) {
t.Parallel()
// This test verifies that the pubsub "created" event
// carries a fully-populated codersdk.Chat. Exhaustive
// field-level coverage of the converter is handled by
// TestChat_AllFieldsPopulated (db2sdk) and
// TestChat_JSONRoundTrip (codersdk). This integration
// test only checks that key fields survive the full
// API → pubsub → websocket pipeline.
ctx := testutil.Context(t, testutil.WaitLong)
client := newChatClient(t)
_ = coderdtest.CreateFirstUser(t, client.Client)
modelConfig := createChatModelConfig(t, client)
conn, err := client.Dial(ctx, "/api/experimental/chats/watch", nil)
require.NoError(t, err)
defer conn.Close(websocket.StatusNormalClosure, "done")
type watchEvent struct {
Type codersdk.ServerSentEventType `json:"type"`
Data json.RawMessage `json:"data,omitempty"`
}
// Skip the initial ping.
var event watchEvent
err = wsjson.Read(ctx, conn, &event)
require.NoError(t, err)
require.Equal(t, codersdk.ServerSentEventTypePing, event.Type)
require.True(t, len(event.Data) == 0 || string(event.Data) == "null")
createdChat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
Content: []codersdk.ChatInputPart{
{
Type: codersdk.ChatInputPartTypeText,
Text: "watch route fields completeness test",
},
},
})
require.NoError(t, err)
var got codersdk.Chat
testutil.Eventually(ctx, t, func(_ context.Context) bool {
var update watchEvent
if readErr := wsjson.Read(ctx, conn, &update); readErr != nil {
return false
}
if update.Type != codersdk.ServerSentEventTypeData {
return false
}
var payload coderdpubsub.ChatEvent
if unmarshalErr := json.Unmarshal(update.Data, &payload); unmarshalErr != nil {
return false
}
if payload.Kind == coderdpubsub.ChatEventKindCreated &&
payload.Chat.ID == createdChat.ID {
got = payload.Chat
return true
}
return false
}, testutil.IntervalFast, "expected a created event for chat %s", createdChat.ID)
require.Equal(t, createdChat.ID, got.ID)
require.Equal(t, createdChat.OwnerID, got.OwnerID)
require.Equal(t, modelConfig.ID, got.LastModelConfigID)
require.Equal(t, createdChat.Title, got.Title)
require.Equal(t, codersdk.ChatStatusPending, got.Status)
require.NotNil(t, got.RootChatID)
require.Equal(t, createdChat.ID, *got.RootChatID)
require.NotZero(t, got.CreatedAt)
require.NotZero(t, got.UpdatedAt)
})
t.Run("DiffStatusChangeIncludesDiffStatus", func(t *testing.T) {
t.Parallel()