Ctrl K
ComunidadGuías…

coderd/exp_chats_chatstate_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package coderd_test

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"testing"

	"github.com/google/uuid"
	"github.com/stretchr/testify/require"

	"github.com/coder/coder/v2/coderd"
	"github.com/coder/coder/v2/coderd/coderdtest"
	"github.com/coder/coder/v2/coderd/database"
	"github.com/coder/coder/v2/coderd/database/dbauthz"
	"github.com/coder/coder/v2/coderd/database/dbgen"
	"github.com/coder/coder/v2/coderd/x/chatd/chatprompt"
	"github.com/coder/coder/v2/coderd/x/chatd/chatstate"
	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/coder/v2/testutil"
)

// withChatWorkerDisabled turns off the chat daemon's background worker
// so every test in this file observes synchronous chatstate endpoint
// behavior deterministically. Without it the worker races the tests:
// it can finish a turn (running -> waiting), promote queued messages,
// or commit steps concurrently with the driveChatTo* fixtures.
func withChatWorkerDisabled(o *coderdtest.Options) {
	o.ChatWorkerDisabled = true
}

// driveChatToWaiting transitions the chat from `running` (its initial
// state per the RFC) to `waiting` by running chatstate.FinishTurn.
// Tests use this when they need to exercise endpoint behavior that
// only succeeds from idle execution states (W, E0).
func driveChatToWaiting(ctx context.Context, t *testing.T, api *coderd.API, chatID uuid.UUID) {
	t.Helper()
	chatdCtx := dbauthz.AsChatd(ctx) //nolint:gocritic // Test fixture mirrors chatd background transitions.
	machine := chatstate.NewChatMachine(api.Database, api.Pubsub, chatID)
	require.NoError(t, machine.Update(chatdCtx, func(tx *chatstate.Tx, store database.Store) error {
		_, err := tx.FinishTurn(chatstate.FinishTurnInput{})
		return err
	}))
}

// driveChatToRequiresAction commits an assistant message with a single
// dynamic tool_call part and then transitions the chat to
// `requires_action`. The tool_call_id returned lets the caller
// assemble a valid SubmitToolResultsRequest.
func driveChatToRequiresAction(
	ctx context.Context,
	t *testing.T,
	api *coderd.API,
	chat codersdk.Chat,
	toolName string,
) (toolCallID string) {
	t.Helper()
	chatdCtx := dbauthz.AsChatd(ctx) //nolint:gocritic // Test fixture mirrors chatd background transitions.

	toolCallID = "call-" + uuid.NewString()
	assistantContent, err := chatprompt.MarshalParts([]codersdk.ChatMessagePart{
		codersdk.ChatMessageText("dispatching dynamic tool"),
		{
			Type:       codersdk.ChatMessagePartTypeToolCall,
			ToolCallID: toolCallID,
			ToolName:   toolName,
			Args:       json.RawMessage(`{}`),
		},
	})
	require.NoError(t, err)

	machine := chatstate.NewChatMachine(api.Database, api.Pubsub, chat.ID)
	require.NoError(t, machine.Update(chatdCtx, func(tx *chatstate.Tx, store database.Store) error {
		_, err := tx.CommitStep(chatstate.CommitStepInput{
			Messages: []chatstate.Message{{
				Role:           database.ChatMessageRoleAssistant,
				Content:        assistantContent,
				Visibility:     database.ChatMessageVisibilityBoth,
				ModelConfigID:  uuid.NullUUID{UUID: chat.LastModelConfigID, Valid: true},
				ContentVersion: chatprompt.CurrentContentVersion,
			}},
		})
		if err != nil {
			return err
		}
		_, err = tx.EnterRequiresAction(chatstate.EnterRequiresActionInput{})
		return err
	}))
	return toolCallID
}

// TestPostChatsStartsRunning verifies the RFC-mandated `running`
// initial status surfaced by the create-chat endpoint.
func TestPostChatsStartsRunning(t *testing.T) {
	t.Parallel()

	ctx := testutil.Context(t, testutil.WaitLong)
	client, api := newChatClientWithAPI(t, withChatWorkerDisabled)
	firstUser := coderdtest.CreateFirstUser(t, client.Client)
	_ = createChatModel(t, client)

	chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
		OrganizationID: firstUser.OrganizationID,
		Content: []codersdk.ChatInputPart{{
			Type: codersdk.ChatInputPartTypeText,
			Text: "hello",
		}},
	})
	require.NoError(t, err)
	require.Equal(t, codersdk.ChatStatusRunning, chat.Status,
		"new chats must start in `running` per chatd RFC")

	// Re-reading also reports `running` because the chat row is
	// authoritative and no worker has advanced it.
	gotChat, err := client.GetChat(ctx, chat.ID)
	require.NoError(t, err)
	require.Equal(t, codersdk.ChatStatusRunning, gotChat.Status)
	require.NotNil(t, api.Pubsub)
}

// TestArchiveChatStateTransitions covers the two RFC-mandated archive
// behaviors at the endpoint contract level: archiving from an idle
// chat (W) succeeds, and archiving from an active chat (R0) returns
// a state conflict and leaves the chat unarchived.
func TestArchiveChatStateTransitions(t *testing.T) {
	t.Parallel()

	t.Run("IdleSucceeds", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.Context(t, testutil.WaitLong)
		client, api := newChatClientWithAPI(t, withChatWorkerDisabled)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content:        []codersdk.ChatInputPart{{Type: codersdk.ChatInputPartTypeText, Text: "archive me"}},
		})
		require.NoError(t, err)

		driveChatToWaiting(ctx, t, api, chat.ID)

		err = client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{Archived: new(true)})
		require.NoError(t, err)

		got, err := client.GetChat(ctx, chat.ID)
		require.NoError(t, err)
		require.True(t, got.Archived)
	})

	t.Run("ActiveChatReturnsConflict", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.Context(t, testutil.WaitLong)
		client := newChatClient(t, withChatWorkerDisabled)
		firstUser := coderdtest.CreateFirstUser(t, client.Client)
		_ = createChatModel(t, client)

		chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content:        []codersdk.ChatInputPart{{Type: codersdk.ChatInputPartTypeText, Text: "no archive"}},
		})
		require.NoError(t, err)

		err = client.UpdateChat(ctx, chat.ID, codersdk.UpdateChatRequest{Archived: new(true)})
		requireSDKError(t, err, http.StatusConflict)

		got, err := client.GetChat(ctx, chat.ID)
		require.NoError(t, err)
		require.False(t, got.Archived, "active chat must remain unarchived after a conflict")
	})
}

func TestPostChatMessagesBusyBehavior(t *testing.T) {
	t.Parallel()

	client := newChatClient(t, withChatWorkerDisabled)
	firstUser := coderdtest.CreateFirstUser(t, client.Client)
	_ = createChatModel(t, client)

	for _, tc := range []struct {
		name         string
		busyBehavior codersdk.ChatBusyBehavior
		wantRow      codersdk.ChatBusyBehavior
		wantDetail   string
	}{
		{name: "DefaultSteers", wantRow: codersdk.ChatBusyBehaviorSteer},
		{name: "Steer", busyBehavior: codersdk.ChatBusyBehaviorSteer, wantRow: codersdk.ChatBusyBehaviorSteer},
		{name: "Queue", busyBehavior: codersdk.ChatBusyBehaviorQueue, wantRow: codersdk.ChatBusyBehaviorQueue},
		{
			name:         "InterruptRejected",
			busyBehavior: "interrupt",
			wantDetail:   `Must be "steer" or "queue".`,
		},
		{name: "UnknownRejected", busyBehavior: "bogus", wantDetail: `Must be "steer" or "queue".`},
	} {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			ctx := testutil.Context(t, testutil.WaitLong)
			chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
				OrganizationID: firstUser.OrganizationID,
				Content:        []codersdk.ChatInputPart{{Type: codersdk.ChatInputPartTypeText, Text: "hello"}},
			})
			require.NoError(t, err)
			require.Equal(t, codersdk.ChatStatusRunning, chat.Status)

			resp, err := client.CreateChatMessage(ctx, chat.ID, codersdk.CreateChatMessageRequest{
				Content:      []codersdk.ChatInputPart{{Type: codersdk.ChatInputPartTypeText, Text: "follow up"}},
				BusyBehavior: tc.busyBehavior,
			})
			var wantQueued []codersdk.ChatBusyBehavior
			if tc.wantDetail != "" {
				sdkErr := requireSDKError(t, err, http.StatusBadRequest)
				require.Equal(t, "Invalid busy_behavior value.", sdkErr.Message)
				require.Equal(t, tc.wantDetail, sdkErr.Detail)
			} else {
				require.NoError(t, err)
				require.True(t, resp.Queued)
				require.NotNil(t, resp.QueuedMessage)
				require.Equal(t, tc.wantRow, resp.QueuedMessage.BusyBehavior)
				wantQueued = []codersdk.ChatBusyBehavior{tc.wantRow}
			}

			messages, err := client.GetChatMessages(ctx, chat.ID, nil)
			require.NoError(t, err)
			gotQueued := make([]codersdk.ChatBusyBehavior, 0, len(messages.QueuedMessages))
			for _, queued := range messages.QueuedMessages {
				gotQueued = append(gotQueued, queued.BusyBehavior)
			}
			require.ElementsMatch(t, wantQueued, gotQueued)

			got, err := client.GetChat(ctx, chat.ID)
			require.NoError(t, err)
			require.Equal(t, codersdk.ChatStatusRunning, got.Status)
		})
	}
}

// Missing-ID cases need a non-empty queue to pass the transition check.
func TestChatQueueEndpointErrors(t *testing.T) {
	t.Parallel()

	client := newChatClient(t, withChatWorkerDisabled)
	firstUser := coderdtest.CreateFirstUser(t, client.Client)
	_ = createChatModel(t, client)

	for _, tc := range []struct {
		name       string
		method     string
		pathSuffix string
		queueFirst bool
		wantStatus int
	}{
		{name: "DeleteMissingReturns404", method: http.MethodDelete, queueFirst: true, wantStatus: http.StatusNotFound},
		{name: "DeleteEmptyQueueReturns404", method: http.MethodDelete, wantStatus: http.StatusNotFound},
		{name: "PromoteMissingReturns404", method: http.MethodPost, pathSuffix: "/promote", queueFirst: true, wantStatus: http.StatusNotFound},
		{name: "PromoteEmptyQueueReturns404", method: http.MethodPost, pathSuffix: "/promote", wantStatus: http.StatusNotFound},
	} {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			ctx := testutil.Context(t, testutil.WaitLong)
			chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
				OrganizationID: firstUser.OrganizationID,
				Content:        []codersdk.ChatInputPart{{Type: codersdk.ChatInputPartTypeText, Text: "hello"}},
			})
			require.NoError(t, err)
			var queued *codersdk.ChatQueuedMessage
			if tc.queueFirst {
				sent, err := client.CreateChatMessage(ctx, chat.ID, codersdk.CreateChatMessageRequest{
					Content:      []codersdk.ChatInputPart{{Type: codersdk.ChatInputPartTypeText, Text: "queued"}},
					BusyBehavior: codersdk.ChatBusyBehaviorQueue,
				})
				require.NoError(t, err)
				require.NotNil(t, sent.QueuedMessage)
				queued = sent.QueuedMessage
			}

			res, err := client.Request(ctx, tc.method, fmt.Sprintf("/api/v2/chats/%s/queue/99999999%s", chat.ID, tc.pathSuffix), nil)
			require.NoError(t, err)
			defer res.Body.Close()
			require.Equal(t, tc.wantStatus, res.StatusCode)

			if queued != nil {
				got, err := client.GetChatMessages(ctx, chat.ID, nil)
				require.NoError(t, err)
				require.Len(t, got.QueuedMessages, 1)
				require.Equal(t, queued.ID, got.QueuedMessages[0].ID)
			}
		})
	}
}

// TestInterruptChatIdleReturnsChat verifies that interrupting an idle
// chat succeeds without changing it, so a client whose interrupt races
// with the end of a turn can still send its next message. The fixture
// composes chatstate transitions to reach the W state without
// depending on the background worker.
func TestInterruptChatIdleReturnsChat(t *testing.T) {
	t.Parallel()

	ctx := testutil.Context(t, testutil.WaitLong)
	client, api := newChatClientWithAPI(t, withChatWorkerDisabled)
	firstUser := coderdtest.CreateFirstUser(t, client.Client)
	_ = createChatModel(t, client)

	chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
		OrganizationID: firstUser.OrganizationID,
		Content:        []codersdk.ChatInputPart{{Type: codersdk.ChatInputPartTypeText, Text: "interrupt me"}},
	})
	require.NoError(t, err)

	driveChatToWaiting(ctx, t, api, chat.ID)

	interrupted, err := client.InterruptChat(ctx, chat.ID)
	require.NoError(t, err)
	require.Equal(t, chat.ID, interrupted.ID)
	require.Equal(t, codersdk.ChatStatusWaiting, interrupted.Status)
}

// TestSubmitToolResultsWrongStateReturnsConflict covers the wrong
// chat-status response when the chat is not in requires_action.
func TestSubmitToolResultsWrongStateReturnsConflict(t *testing.T) {
	t.Parallel()

	ctx := testutil.Context(t, testutil.WaitLong)
	client := newChatClient(t, withChatWorkerDisabled)
	firstUser := coderdtest.CreateFirstUser(t, client.Client)
	_ = createChatModel(t, client)

	chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
		OrganizationID: firstUser.OrganizationID,
		Content:        []codersdk.ChatInputPart{{Type: codersdk.ChatInputPartTypeText, Text: "hello"}},
	})
	require.NoError(t, err)
	require.Equal(t, codersdk.ChatStatusRunning, chat.Status)

	err = client.SubmitToolResults(ctx, chat.ID, codersdk.SubmitToolResultsRequest{
		Results: []codersdk.ToolResult{{
			ToolCallID: "unknown-call",
			Output:     json.RawMessage(`{}`),
		}},
	})
	requireSDKError(t, err, http.StatusConflict)
}

// TestSubmitToolResultsRequiresActionSucceeds drives a chat into
// requires_action with a single dynamic tool call and verifies a
// matching SubmitToolResults call returns 204 with the tool result
// persisted.
func TestSubmitToolResultsRequiresActionSucceeds(t *testing.T) {
	t.Parallel()

	ctx := testutil.Context(t, testutil.WaitLong)
	client, api := newChatClientWithAPI(t, withChatWorkerDisabled)
	firstUser := coderdtest.CreateFirstUser(t, client.Client)
	_ = createChatModel(t, client)

	dynamicTools := []codersdk.DynamicTool{{
		Name:        "echo",
		Description: "test echo tool",
		InputSchema: json.RawMessage(`{"type":"object"}`),
	}}
	chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
		OrganizationID:     firstUser.OrganizationID,
		Content:            []codersdk.ChatInputPart{{Type: codersdk.ChatInputPartTypeText, Text: "hello"}},
		UnsafeDynamicTools: dynamicTools,
	})
	require.NoError(t, err)

	toolCallID := driveChatToRequiresAction(ctx, t, api, chat, "echo")

	err = client.SubmitToolResults(ctx, chat.ID, codersdk.SubmitToolResultsRequest{
		Results: []codersdk.ToolResult{{
			ToolCallID: toolCallID,
			Output:     json.RawMessage(`{"ok":true}`),
		}},
	})
	require.NoError(t, err)

	// The tool result must be persisted as a visible tool message.
	got, err := client.GetChatMessages(ctx, chat.ID, nil)
	require.NoError(t, err)
	foundToolResult := false
	for _, msg := range got.Messages {
		if msg.Role != codersdk.ChatMessageRoleTool {
			continue
		}
		for _, part := range msg.Content {
			if part.Type == codersdk.ChatMessagePartTypeToolResult && part.ToolCallID == toolCallID {
				foundToolResult = true
				break
			}
		}
	}
	require.True(t, foundToolResult, "tool result message must be visible in chat history")
}

func TestPatchChatChildArchive(t *testing.T) {
	t.Parallel()

	client, db, api := newChatClientWithAPIAndDatabase(t, withChatWorkerDisabled)
	firstUser := coderdtest.CreateFirstUser(t, client.Client)
	modelConfig := createChatModel(t, client)

	createFamily := func(ctx context.Context, t *testing.T, childStatuses ...database.ChatStatus) []uuid.UUID {
		t.Helper()
		root, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content:        []codersdk.ChatInputPart{{Type: codersdk.ChatInputPartTypeText, Text: "root"}},
		})
		require.NoError(t, err)
		driveChatToWaiting(ctx, t, api, root.ID)

		family := []uuid.UUID{root.ID}
		for _, status := range childStatuses {
			child := dbgen.Chat(t, db, database.Chat{
				OrganizationID:    firstUser.OrganizationID,
				OwnerID:           firstUser.UserID,
				LastModelConfigID: modelConfig.ID,
				Status:            status,
				ParentChatID:      uuid.NullUUID{UUID: root.ID, Valid: true},
				RootChatID:        uuid.NullUUID{UUID: root.ID, Valid: true},
			})
			family = append(family, child.ID)
		}
		return family
	}
	requireArchived := func(ctx context.Context, t *testing.T, family []uuid.UUID, want bool, msg string) {
		t.Helper()
		for _, id := range family {
			got, err := loadChatRow(ctx, db, id)
			require.NoError(t, err)
			require.Equal(t, want, got.Archived, msg)
		}
	}

	t.Run("ArchiveChildRejected", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.Context(t, testutil.WaitLong)
		family := createFamily(ctx, t, database.ChatStatusWaiting, database.ChatStatusWaiting)

		err := client.UpdateChat(ctx, family[1], codersdk.UpdateChatRequest{Archived: new(true)})
		requireSDKError(t, err, http.StatusBadRequest)
		requireArchived(ctx, t, family, false, "no family member may flip archive state after a rejected child archive")
	})

	t.Run("UnarchiveChildRejected", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.Context(t, testutil.WaitLong)
		family := createFamily(ctx, t, database.ChatStatusWaiting, database.ChatStatusWaiting)
		err := client.UpdateChat(ctx, family[0], codersdk.UpdateChatRequest{Archived: new(true)})
		require.NoError(t, err)
		requireArchived(ctx, t, family, true, "precondition: family archived after root archive")

		err = client.UpdateChat(ctx, family[1], codersdk.UpdateChatRequest{Archived: new(false)})
		requireSDKError(t, err, http.StatusBadRequest)
		requireArchived(ctx, t, family, true, "no family member may flip archive state after a rejected child unarchive")
	})

	t.Run("RootArchiveRollsBackWhenChildCannotArchive", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.Context(t, testutil.WaitLong)
		family := createFamily(ctx, t, database.ChatStatusRunning)

		err := client.UpdateChat(ctx, family[0], codersdk.UpdateChatRequest{Archived: new(true)})
		requireSDKError(t, err, http.StatusConflict)
		requireArchived(ctx, t, family, false, "rolled-back family archive must not leave any member archived")
	})
}

func TestInvalidChatStateEndpoints(t *testing.T) {
	t.Parallel()

	client, db, api := newChatClientWithAPIAndDatabase(t, withChatWorkerDisabled)
	firstUser := coderdtest.CreateFirstUser(t, client.Client)
	_ = createChatModel(t, client)

	createChat := func(ctx context.Context, t *testing.T) codersdk.Chat {
		t.Helper()
		chat, err := client.CreateChat(ctx, codersdk.CreateChatRequest{
			OrganizationID: firstUser.OrganizationID,
			Content:        []codersdk.ChatInputPart{{Type: codersdk.ChatInputPartTypeText, Text: "hello"}},
		})
		require.NoError(t, err)
		return chat
	}

	for _, tc := range []struct {
		name string
		call func(ctx context.Context, chatID uuid.UUID) error
	}{
		{
			name: "PostChatMessagesReturnsSharedResponse",
			call: func(ctx context.Context, chatID uuid.UUID) error {
				_, err := client.CreateChatMessage(ctx, chatID, codersdk.CreateChatMessageRequest{
					Content: []codersdk.ChatInputPart{{Type: codersdk.ChatInputPartTypeText, Text: "send"}},
				})
				return err
			},
		},
		{
			name: "PostChatToolResultsReturnsSharedResponse",
			call: func(ctx context.Context, chatID uuid.UUID) error {
				return client.SubmitToolResults(ctx, chatID, codersdk.SubmitToolResultsRequest{
					Results: []codersdk.ToolResult{{ToolCallID: "call-irrelevant", Output: json.RawMessage(`{}`)}},
				})
			},
		},
	} {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()

			ctx := testutil.Context(t, testutil.WaitLong)
			chat := createChat(ctx, t)
			driveChatToInvalidWaitingWithQueue(ctx, t, api, chat.ID)

			sdkErr := requireSDKError(t, tc.call(ctx, chat.ID), http.StatusConflict)
			require.Equal(t, "Chat is in an invalid state.", sdkErr.Message)
		})
	}

	t.Run("ReconcileSucceeds", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.Context(t, testutil.WaitLong)
		chat := createChat(ctx, t)
		driveChatToInvalidWaitingWithQueue(ctx, t, api, chat.ID)

		reconciled, err := client.ReconcileInvalidChatState(ctx, chat.ID)
		require.NoError(t, err)
		require.Equal(t, chat.ID, reconciled.ID)
		require.Equal(t, codersdk.ChatStatusError, reconciled.Status)

		// The persisted row must reflect a valid error state with the
		// queued message preserved (E1) and a populated last_error.
		persisted, err := loadChatRow(ctx, db, chat.ID)
		require.NoError(t, err)
		require.Equal(t, database.ChatStatusError, persisted.Status)
		require.False(t, persisted.Archived)
		require.True(t, persisted.LastError.Valid)

		queueCount, err := db.CountChatQueuedMessages(dbauthz.AsChatd(ctx), chat.ID)
		require.NoError(t, err)
		require.Equal(t, int64(1), queueCount, "queued message is preserved by reconcile")
	})

	t.Run("ReconcileNotInvalidReturnsConflict", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.Context(t, testutil.WaitLong)
		// A freshly created chat starts in the valid running state (R0).
		chat := createChat(ctx, t)
		require.Equal(t, codersdk.ChatStatusRunning, chat.Status)

		_, err := client.ReconcileInvalidChatState(ctx, chat.ID)
		sdkErr := requireSDKError(t, err, http.StatusConflict)
		require.Equal(t, "Chat is not in an invalid state.", sdkErr.Message)
	})

	t.Run("ReconcileNotFound", func(t *testing.T) {
		t.Parallel()

		ctx := testutil.Context(t, testutil.WaitLong)
		_, err := client.ReconcileInvalidChatState(ctx, uuid.New())
		requireSDKError(t, err, http.StatusNotFound)
	})
}

// loadChatRow reads a chat row directly through dbauthz.AsChatd so
// endpoint tests verify side effects with the daemon's narrower
// permission set.
func loadChatRow(ctx context.Context, db database.Store, id uuid.UUID) (database.Chat, error) {
	chatdCtx := dbauthz.AsChatd(ctx) //nolint:gocritic // Test fixture reads rows with chatd permissions.
	return db.GetChatByID(chatdCtx, id)
}

// driveChatToInvalidWaitingWithQueue forces a chat into the
// chatstate-invalid combination (status=waiting, archived=false,
// queue non-empty) by writing directly through the database. This is
// an intentional invalid fixture: chatstate transitions reject
// driving toward this combination, so AsChatd is not used here.
func driveChatToInvalidWaitingWithQueue(
	ctx context.Context,
	t *testing.T,
	api *coderd.API,
	chatID uuid.UUID,
) {
	t.Helper()
	sysCtx := dbauthz.AsSystemRestricted(ctx) //nolint:gocritic // Test fixture writes invalid combination by design.

	// Seed the queue with one row attributed to the chat owner. The
	// content is a minimal valid JSON payload; only the row's
	// presence matters for ClassifyExecutionState. The owner_id is
	// filled from the chat row by the SQL.
	rawContent, err := chatprompt.MarshalParts([]codersdk.ChatMessagePart{
		codersdk.ChatMessageText("queued"),
	})
	require.NoError(t, err)
	_, err = api.Database.InsertChatQueuedMessage(sysCtx, database.InsertChatQueuedMessageParams{
		ChatID:        chatID,
		Content:       rawContent.RawMessage,
		ModelConfigID: uuid.NullUUID{},
	})
	require.NoError(t, err)

	// Flip the chat's status to waiting via a raw execution-state
	// update. This bypasses the transition matrix to produce the
	// (waiting, queued) invalid pairing.
	_, err = api.Database.UpdateChatExecutionState(sysCtx, database.UpdateChatExecutionStateParams{
		ID:       chatID,
		Status:   database.ChatStatusWaiting,
		Archived: false,
	})
	require.NoError(t, err)
}