feat(issues): add structured ticket search

This commit is contained in:
pseudoyu 2026-04-08 00:35:27 +08:00
parent efe131591f
commit 34c39b765e
No known key found for this signature in database
11 changed files with 1033 additions and 27 deletions

View file

@ -8,6 +8,7 @@ import (
"net/http"
"net/http/httptest"
"os"
"strconv"
"strings"
"testing"
@ -299,6 +300,57 @@ func TestCommentCRUD(t *testing.T) {
testHandler.DeleteIssue(w, req)
}
func TestListIssuesAllIgnoresPagination(t *testing.T) {
createdIDs := make([]string, 0, 3)
for i := 0; i < 3; i++ {
w := httptest.NewRecorder()
req := newRequest("POST", "/api/issues?workspace_id="+testWorkspaceID, map[string]any{
"title": "Searchable issue " + strconv.Itoa(i+1),
})
testHandler.CreateIssue(w, req)
if w.Code != http.StatusCreated {
t.Fatalf("CreateIssue: expected 201, got %d: %s", w.Code, w.Body.String())
}
var issue IssueResponse
json.NewDecoder(w.Body).Decode(&issue)
createdIDs = append(createdIDs, issue.ID)
}
t.Cleanup(func() {
for _, issueID := range createdIDs {
w := httptest.NewRecorder()
req := newRequest("DELETE", "/api/issues/"+issueID, nil)
req = withURLParam(req, "id", issueID)
testHandler.DeleteIssue(w, req)
}
})
w := httptest.NewRecorder()
req := newRequest("GET", "/api/issues?workspace_id="+testWorkspaceID+"&all=true&limit=1", nil)
testHandler.ListIssues(w, req)
if w.Code != http.StatusOK {
t.Fatalf("ListIssues(all=true): expected 200, got %d: %s", w.Code, w.Body.String())
}
var listResp struct {
Issues []IssueResponse `json:"issues"`
Total int `json:"total"`
}
if err := json.NewDecoder(w.Body).Decode(&listResp); err != nil {
t.Fatalf("ListIssues(all=true): decode response: %v", err)
}
if len(listResp.Issues) < 3 {
t.Fatalf("ListIssues(all=true): expected at least 3 issues, got %d", len(listResp.Issues))
}
if listResp.Total != len(listResp.Issues) {
t.Fatalf("ListIssues(all=true): expected total %d, got %d", len(listResp.Issues), listResp.Total)
}
}
func TestAgentCRUD(t *testing.T) {
// List agents
w := httptest.NewRecorder()

View file

@ -103,6 +103,31 @@ func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) {
return
}
if r.URL.Query().Get("all") == "true" {
issues, err := h.Queries.ListAllIssues(ctx, db.ListAllIssuesParams{
WorkspaceID: wsUUID,
Status: statusFilterFromQuery(r),
Priority: priorityFilter,
AssigneeID: assigneeFilter,
})
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to list issues")
return
}
prefix := h.getIssuePrefix(ctx, wsUUID)
resp := make([]IssueResponse, len(issues))
for i, issue := range issues {
resp[i] = issueToResponse(issue, prefix)
}
writeJSON(w, http.StatusOK, map[string]any{
"issues": resp,
"total": len(resp),
})
return
}
limit := 100
offset := 0
if l := r.URL.Query().Get("limit"); l != "" {
@ -116,10 +141,7 @@ func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) {
}
}
var statusFilter pgtype.Text
if s := r.URL.Query().Get("status"); s != "" {
statusFilter = pgtype.Text{String: s, Valid: true}
}
statusFilter := statusFilterFromQuery(r)
issues, err := h.Queries.ListIssues(ctx, db.ListIssuesParams{
WorkspaceID: wsUUID,
@ -157,6 +179,14 @@ func (h *Handler) ListIssues(w http.ResponseWriter, r *http.Request) {
})
}
func statusFilterFromQuery(r *http.Request) pgtype.Text {
var statusFilter pgtype.Text
if s := r.URL.Query().Get("status"); s != "" {
statusFilter = pgtype.Text{String: s, Valid: true}
}
return statusFilter
}
func (h *Handler) GetIssue(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
issue, ok := h.loadIssueForUser(w, r, id)