Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 0 additions & 7 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -1889,13 +1889,6 @@ func (q *querier) CalculateAIBridgeInterceptionsTelemetrySummary(ctx context.Con
return q.db.CalculateAIBridgeInterceptionsTelemetrySummary(ctx, arg)
}

func (q *querier) ChatSearchQueryIsEmpty(ctx context.Context, search string) (bool, error) {
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceChat); err != nil {
return false, err
}
return q.db.ChatSearchQueryIsEmpty(ctx, search)
}

func (q *querier) ClaimPrebuiltWorkspace(ctx context.Context, arg database.ClaimPrebuiltWorkspaceParams) (database.ClaimPrebuiltWorkspaceRow, error) {
empty := database.ClaimPrebuiltWorkspaceRow{}

Expand Down
4 changes: 0 additions & 4 deletions coderd/database/dbauthz/dbauthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -942,10 +942,6 @@ func (s *MethodTestSuite) TestChats() {
dbm.EXPECT().BackfillChatMessagesSearchTsv(gomock.Any(), int32(100)).Return(int64(0), nil).AnyTimes()
check.Args(int32(100)).Asserts(rbac.ResourceChat, policy.ActionUpdate)
}))
s.Run("ChatSearchQueryIsEmpty", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
dbm.EXPECT().ChatSearchQueryIsEmpty(gomock.Any(), "!!!").Return(true, nil).AnyTimes()
check.Args("!!!").Asserts(rbac.ResourceChat, policy.ActionRead)
}))
s.Run("GetChatRetentionDays", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
dbm.EXPECT().GetChatRetentionDays(gomock.Any()).Return(int32(30), nil).AnyTimes()
check.Args().Asserts()
Expand Down
8 changes: 0 additions & 8 deletions coderd/database/dbmetrics/querymetrics.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 0 additions & 15 deletions coderd/database/dbmock/dbmock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 0 additions & 3 deletions coderd/database/querier.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 0 additions & 13 deletions coderd/database/queries.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 0 additions & 5 deletions coderd/database/queries/chats.sql
Original file line number Diff line number Diff line change
Expand Up @@ -339,11 +339,6 @@ SET search_tsv = COALESCE(
''::tsvector)
FROM batch WHERE cm.id = batch.id;

-- name: ChatSearchQueryIsEmpty :one
-- Reports whether search text tokenizes to an empty tsquery (e.g. '!!!').
-- Used to reject input that would silently match nothing.
SELECT numnode(websearch_to_tsquery('simple', @search::text)) = 0 AS is_empty;

-- name: GetChatByID :one
SELECT *
FROM chats_expanded
Expand Down
24 changes: 1 addition & 23 deletions coderd/exp_chats.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ func (api *API) chatsByWorkspace(rw http.ResponseWriter, r *http.Request) {
// @Security CoderSessionToken
// @Tags Chats
// @Produce json
// @Param q query string false "Search query. Supports `title:<substring>` (case-insensitive, quote multi-word values), `archived:bool`, `has_unread:bool`, `pr_status:<draft\|open\|merged\|closed>` as repeated or comma-separated values, `source:<created_by_me\|shared_with_me>`, `diff_url:<url>` (quote values containing colons), `pr:<number>` (exact PR number match), `repo:<owner/repo>` (case-insensitive substring match against git remote origin or URL), `pr_title:<text>` (case-insensitive PR title substring), `search:<text>` (full-text search across chat titles, PR titles, PR numbers, and message bodies; quote multi-word values; cannot be combined with title, pr_title, or pr). Bare terms are not supported; use `title:<value>` or `search:<value>`."
// @Param q query string false "Search query. Supports `title:<substring>` (case-insensitive, quote multi-word values), `archived:bool`, `has_unread:bool`, `pr_status:<draft\|open\|merged\|closed>` as repeated or comma-separated values, `source:<created_by_me\|shared_with_me>`, `diff_url:<url>` (quote values containing colons), `pr:<number>` (exact PR number match), `repo:<owner/repo>` (case-insensitive substring match against git remote origin or URL), `pr_title:<text>` (case-insensitive PR title substring), `search:<text>` (full-text search across chat titles, PR titles, PR numbers, and message bodies; quote multi-word values; cannot be combined with title, pr_title, or pr; a value that tokenizes to no searchable words returns an empty list). Bare terms are not supported; use `title:<value>` or `search:<value>`."
// @Param label query string false "Filter by label as key:value. Repeat for multiple (AND logic)."
// @Success 200 {array} codersdk.Chat
// @Router /api/experimental/chats [get]
Expand All @@ -381,28 +381,6 @@ func (api *API) listChats(rw http.ResponseWriter, r *http.Request) {
return
}

// Reject text that tokenizes to nothing; it would silently match no rows.
if searchParams.Search != "" {
isEmpty, err := api.Database.ChatSearchQueryIsEmpty(ctx, searchParams.Search)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Failed to validate search query.",
Detail: err.Error(),
})
return
}
if isEmpty {
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid chat search query.",
Validations: []codersdk.ValidationError{{
Field: "search",
Detail: "Search query contains no searchable words.",
}},
})
return
}
}

var labelFilter pqtype.NullRawMessage
if labelParams := r.URL.Query()["label"]; len(labelParams) > 0 {
labelMap := make(map[string]string, len(labelParams))
Expand Down
24 changes: 17 additions & 7 deletions coderd/exp_chats_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2065,17 +2065,27 @@ func TestListChats_Search(t *testing.T) {
require.NotContains(t, ids, noMatch.ID)
})

t.Run("NoSearchableWordsReturns400", func(t *testing.T) {
t.Run("NoSearchableWordsReturnsEmpty", func(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-44] The zero-lexeme backend test pins !!!, the one input the frontend can never send, and skips or, the one it can. (Chopper)

The commit message for 026020b names "operator-only input like OR" as the motivating case, and the story types or, but that story mocks getChats, so the claim "the backend returns empty for or" is pinned nowhere. NoSearchableWordsReturnsEmpty uses search:"!!!", which the frontend letter/number guard blocks from ever being emitted. The Go code path is identical for both inputs; the untested distinction is websearch_to_tsquery('simple', 'or') producing an empty tsquery, which is Postgres behavior, and this suite runs against real Postgres. One added case (Query: \search:"or"``) covers the scenario the commit message and the story both claim.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The handler test now pins both search:"!!!" and search:"or" against real Postgres, covering the operator-only scenario the commit message and story reference.

🤖 Coder Agents

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. The handler test now pins both search:"!!!" and search:"or" against real Postgres, covering the operator-only scenario the commit message and story reference.

🤖 Coder Agents

t.Parallel()
ctx, client, _, _, _ := setup(t)
ctx, client, db, firstUser, modelConfig := setup(t)

_, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
// "or" is a real lexeme (an operator only between operands), so
// search:"or" matches the control chat; search:"!!!" has no lexemes and
// matches nothing.
control := createChat(t, db, firstUser, modelConfig.ID, "fix this or that")
backfillSearchTsv(ctx, t, db)

chats, err := client.ListChats(ctx, &codersdk.ListChatsOptions{
Query: `search:"or"`,
})
require.NoError(t, err)
require.Contains(t, chatIDs(chats), control.ID)

chats, err = client.ListChats(ctx, &codersdk.ListChatsOptions{
Query: `search:"!!!"`,
})
sdkErr := requireSDKError(t, err, http.StatusBadRequest)
require.Len(t, sdkErr.Validations, 1)
require.Equal(t, "search", sdkErr.Validations[0].Field)
require.Contains(t, sdkErr.Validations[0].Detail, "no searchable words")
require.NoError(t, err)
require.Empty(t, chats)
})

t.Run("ComposesWithRepoFilterAndArchivedDefault", func(t *testing.T) {
Expand Down
49 changes: 49 additions & 0 deletions coderd/searchquery/search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1265,6 +1265,55 @@ func TestSearchTasks(t *testing.T) {
}
}

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

// These query shapes must match the emitters in
// site/src/pages/AgentsPage/components/ChatsSidebar/dialogs/searchQuery.ts
// and site/src/api/queries/chats.ts.
testCases := []struct {
name string
query string
}{
{name: "SearchSingleWord", query: `search:"fix"`},
{name: "SearchMultipleWords", query: `search:"fix auth"`},
{name: "SearchColon", query: `search:"fix:lint"`},
{name: "SearchURL", query: `search:"http://example.com"`},
{name: "SearchUnicode", query: `search:"日本語"`},
{name: "SearchOperators", query: `search:"fix race OR deadlock -timeout"`},
{name: "SearchPunctuationOnly", query: `search:"!!!"`},
{name: "SearchOperatorWord", query: `search:"or"`},
{name: "HasUnread", query: "has_unread:true"},
{name: "Archived", query: "archived:true"},
{name: "PRStatuses", query: "pr_status:open,merged"},
{name: "DiffURL", query: `diff_url:"https://github.com/coder/coder/pull/1"`},
{name: "FilterAndSearch", query: `has_unread:true search:"fix auth"`},
{name: "SidebarDefault", query: "archived:false"},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 [CRF-41] The cross-language contract table misses a live sidebar emitter shape: archived:false has_unread:true. (Bisky)

getChatListQueryString emits has_unread:${...} when params.status !== "all" (chats.ts:995), reachable from AgentsPageLayout.tsx:220-228 via the sidebar filters. Neither TestSearchChatsFrontendEmitted nor the new chats.test.ts pin includes it. The whole point of this contract test is that every emitter shape appears on both sides; a shape that exists in production but not in the table is the exact hole the test was built to close. Add getChatListQueryString(toChatListParams({ chatStatus: "unread" })) expecting archived:false has_unread:true to chats.test.ts, and the matching row to the Go table.

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. Added the archived:false has_unread:true shape to both the Go contract table and the chats.test.ts shape test, so the unread-sidebar emitter is now pinned on both sides.

🤖 Coder Agents

{name: "SidebarUnread", query: "archived:false has_unread:true"},
{
name: "SidebarFiltered",
query: "archived:false pr_status:draft,closed source:created_by_me,shared_with_me",
},
}

for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
_, errs := searchquery.Chats(testCase.query)
require.Empty(t, errs)
})
}

rejectedQueries := []string{"pr_status:banana", "has_unread:maybe"}
for _, query := range rejectedQueries {
t.Run("Rejects"+query, func(t *testing.T) {
t.Parallel()
_, errs := searchquery.Chats(query)
require.NotEmpty(t, errs)
})
}
}

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

Expand Down
Loading
Loading