mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add pr, repo, pr_title chat search filters (#25569)
Relates to CODAGT-432 Adds three new search filters to the chat list endpoint (`GET /api/experimental/chats/`): - `pr:<number>` - exact PR number match - `repo:<owner/repo>` - substring match against git remote origin or URL - `pr_title:<text>` - case-insensitive PR title substring match Includes SQL filter clauses (EXISTS against `chat_diff_statuses`), parser with validation, handler wiring, unit tests, swagger annotation update, and a new search syntax documentation page. > 🤖 Generated with [Coder Agents](https://coder.com/agents)
This commit is contained in:
@@ -14118,6 +14118,37 @@ func TestGetChatsFilter(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
linkPRFull := func(chatID uuid.UUID, url, state string, draft bool, prNumber int32, gitRemoteOrigin string, prTitle string) {
|
||||
t.Helper()
|
||||
now := time.Now()
|
||||
// First set the git remote origin via the reference upsert.
|
||||
if gitRemoteOrigin != "" {
|
||||
_, err := store.UpsertChatDiffStatusReference(ctx, database.UpsertChatDiffStatusReferenceParams{
|
||||
ChatID: chatID,
|
||||
Url: sql.NullString{String: url, Valid: url != ""},
|
||||
GitBranch: "main",
|
||||
GitRemoteOrigin: gitRemoteOrigin,
|
||||
StaleAt: now.Add(time.Hour),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
// Then set PR metadata via the status upsert.
|
||||
_, err := store.UpsertChatDiffStatus(ctx, database.UpsertChatDiffStatusParams{
|
||||
ChatID: chatID,
|
||||
Url: sql.NullString{String: url, Valid: url != ""},
|
||||
PullRequestState: sql.NullString{String: state, Valid: state != ""},
|
||||
PullRequestTitle: prTitle,
|
||||
PullRequestDraft: draft,
|
||||
PrNumber: sql.NullInt32{Int32: prNumber, Valid: prNumber > 0},
|
||||
Additions: 1,
|
||||
Deletions: 1,
|
||||
ChangedFiles: 1,
|
||||
RefreshedAt: now,
|
||||
StaleAt: now.Add(time.Hour),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
makeUnread := func(chatID uuid.UUID) {
|
||||
t.Helper()
|
||||
_, err := store.InsertChatMessages(ctx, database.InsertChatMessagesParams{
|
||||
@@ -14199,12 +14230,25 @@ func TestGetChatsFilter(t *testing.T) {
|
||||
linkPR(childWithDraftPR.ID, "https://github.com/coder/coder/pull/1005", "open", true)
|
||||
makeUnread(childWithDraftPR.ID)
|
||||
|
||||
// Chats with specific PR numbers and repos for new filter tests.
|
||||
// Use "acme/widget" and "acme/other-repo" origins to avoid overlapping
|
||||
// with the "coder/coder" URLs in the earlier PR fixtures.
|
||||
prNumberChat := createRoot("pr number 42 chat")
|
||||
linkPRFull(prNumberChat.ID, "https://github.com/acme/widget/pull/42", "open", false, 42, "https://github.com/acme/widget.git", "Fix authentication bug")
|
||||
|
||||
repoChat := createRoot("repo filter chat")
|
||||
linkPRFull(repoChat.ID, "https://github.com/acme/other-repo/pull/7", "merged", false, 7, "https://github.com/acme/other-repo.git", "Add feature X")
|
||||
|
||||
prTitleChat := createRoot("pr title filter chat")
|
||||
linkPRFull(prTitleChat.ID, "https://github.com/acme/widget/pull/99", "open", false, 99, "https://github.com/acme/widget.git", "Deploy new dashboard")
|
||||
|
||||
// All root chat IDs (for "returns everything" baseline).
|
||||
allRootIDs := []uuid.UUID{
|
||||
alphaProject.ID, betaProject.ID, gammaUnrelated.ID,
|
||||
percentComplete.ID, thousandOne.ID, underscoreConfig.ID, hyphenConfig.ID,
|
||||
draftPR.ID, openPR.ID, mergedPR.ID, closedPR.ID,
|
||||
unreadNoPR.ID, readChat.ID, childParent.ID,
|
||||
prNumberChat.ID, repoChat.ID, prTitleChat.ID,
|
||||
}
|
||||
|
||||
// --- test cases ---
|
||||
@@ -14228,15 +14272,32 @@ func TestGetChatsFilter(t *testing.T) {
|
||||
|
||||
// PR status filter.
|
||||
{"PRStatus/Draft", database.GetChatsParams{PullRequestStatuses: []string{"draft"}}, []uuid.UUID{draftPR.ID}},
|
||||
{"PRStatus/Open", database.GetChatsParams{PullRequestStatuses: []string{"open"}}, []uuid.UUID{openPR.ID}},
|
||||
{"PRStatus/Merged", database.GetChatsParams{PullRequestStatuses: []string{"merged"}}, []uuid.UUID{mergedPR.ID}},
|
||||
{"PRStatus/Open", database.GetChatsParams{PullRequestStatuses: []string{"open"}}, []uuid.UUID{openPR.ID, prNumberChat.ID, prTitleChat.ID}},
|
||||
{"PRStatus/Merged", database.GetChatsParams{PullRequestStatuses: []string{"merged"}}, []uuid.UUID{mergedPR.ID, repoChat.ID}},
|
||||
{"PRStatus/Closed", database.GetChatsParams{PullRequestStatuses: []string{"closed"}}, []uuid.UUID{closedPR.ID}},
|
||||
{"PRStatus/MultiStatus", database.GetChatsParams{PullRequestStatuses: []string{"draft", "closed"}}, []uuid.UUID{draftPR.ID, closedPR.ID}},
|
||||
|
||||
// Unread filter.
|
||||
{"Unread/MatchesUnread", database.GetChatsParams{HasUnread: sql.NullBool{Bool: true, Valid: true}}, []uuid.UUID{draftPR.ID, unreadNoPR.ID}},
|
||||
// HasUnread=false returns chats without unread messages.
|
||||
{"Unread/ExcludesRead", database.GetChatsParams{HasUnread: sql.NullBool{Bool: false, Valid: true}}, []uuid.UUID{alphaProject.ID, betaProject.ID, gammaUnrelated.ID, percentComplete.ID, thousandOne.ID, underscoreConfig.ID, hyphenConfig.ID, openPR.ID, mergedPR.ID, closedPR.ID, readChat.ID, childParent.ID}},
|
||||
{"Unread/ExcludesRead", database.GetChatsParams{HasUnread: sql.NullBool{Bool: false, Valid: true}}, []uuid.UUID{alphaProject.ID, betaProject.ID, gammaUnrelated.ID, percentComplete.ID, thousandOne.ID, underscoreConfig.ID, hyphenConfig.ID, openPR.ID, mergedPR.ID, closedPR.ID, readChat.ID, childParent.ID, prNumberChat.ID, repoChat.ID, prTitleChat.ID}},
|
||||
|
||||
// PR number filter.
|
||||
{"PRNumber/ExactMatch", database.GetChatsParams{PrNumber: 42}, []uuid.UUID{prNumberChat.ID}},
|
||||
{"PRNumber/NoMatch", database.GetChatsParams{PrNumber: 999}, nil},
|
||||
{"PRNumber/ZeroIsNoOp", database.GetChatsParams{PrNumber: 0}, allRootIDs},
|
||||
|
||||
// Repo filter.
|
||||
{"Repo/SubstringMatch", database.GetChatsParams{RepoQuery: "acme/widget"}, []uuid.UUID{prNumberChat.ID, prTitleChat.ID}},
|
||||
{"Repo/DifferentRepo", database.GetChatsParams{RepoQuery: "acme/other-repo"}, []uuid.UUID{repoChat.ID}},
|
||||
{"Repo/NoMatch", database.GetChatsParams{RepoQuery: "nonexistent/repo"}, nil},
|
||||
{"Repo/CaseInsensitive", database.GetChatsParams{RepoQuery: "ACME/WIDGET"}, []uuid.UUID{prNumberChat.ID, prTitleChat.ID}},
|
||||
{"Repo/MatchesViaURL", database.GetChatsParams{RepoQuery: "coder/coder"}, []uuid.UUID{draftPR.ID, openPR.ID, mergedPR.ID, closedPR.ID}},
|
||||
|
||||
// PR title filter.
|
||||
{"PRTitle/SubstringMatch", database.GetChatsParams{PrTitleQuery: "auth"}, []uuid.UUID{prNumberChat.ID}},
|
||||
{"PRTitle/CaseInsensitive", database.GetChatsParams{PrTitleQuery: "DEPLOY"}, []uuid.UUID{prTitleChat.ID}},
|
||||
{"PRTitle/NoMatch", database.GetChatsParams{PrTitleQuery: "nonexistent title"}, nil},
|
||||
|
||||
// Composed filters.
|
||||
{"Composed/TitleAndPRStatus", database.GetChatsParams{TitleQuery: "draft", PullRequestStatuses: []string{"draft"}}, []uuid.UUID{draftPR.ID}},
|
||||
@@ -14244,6 +14305,8 @@ func TestGetChatsFilter(t *testing.T) {
|
||||
{"Composed/PRStatusAndUnread", database.GetChatsParams{PullRequestStatuses: []string{"draft"}, HasUnread: sql.NullBool{Bool: true, Valid: true}}, []uuid.UUID{draftPR.ID}},
|
||||
{"Composed/AllFilters", database.GetChatsParams{TitleQuery: "draft", PullRequestStatuses: []string{"draft"}, HasUnread: sql.NullBool{Bool: true, Valid: true}}, []uuid.UUID{draftPR.ID}},
|
||||
{"Composed/TitleNarrowsUnread", database.GetChatsParams{TitleQuery: "no pr", HasUnread: sql.NullBool{Bool: true, Valid: true}}, []uuid.UUID{unreadNoPR.ID}},
|
||||
{"Composed/PRNumberAndStatus", database.GetChatsParams{PrNumber: 42, PullRequestStatuses: []string{"closed"}}, nil},
|
||||
{"Composed/RepoAndPRTitle", database.GetChatsParams{RepoQuery: "acme/widget", PrTitleQuery: "auth"}, []uuid.UUID{prNumberChat.ID}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
Reference in New Issue
Block a user