Files
multica/server/internal/handler/issue_grouped_test.go
2026-05-15 18:44:08 +08:00

157 lines
5.1 KiB
Go

package handler
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestListGroupedIssuesAssigneePaginatesPerGroup(t *testing.T) {
ctx := context.Background()
suffix := time.Now().UnixNano()
var assigneeID string
if err := testPool.QueryRow(ctx, `
INSERT INTO "user" (name, email)
VALUES ($1, $2)
RETURNING id
`, "Grouped Issues Test User", fmt.Sprintf("grouped-%d@multica.ai", suffix)).Scan(&assigneeID); err != nil {
t.Fatalf("create assignee user: %v", err)
}
t.Cleanup(func() {
_, _ = testPool.Exec(context.Background(), `DELETE FROM "user" WHERE id = $1`, assigneeID)
})
if _, err := testPool.Exec(ctx, `
INSERT INTO member (workspace_id, user_id, role)
VALUES ($1, $2, 'member')
`, testWorkspaceID, assigneeID); err != nil {
t.Fatalf("create assignee member: %v", err)
}
var agentID string
if err := testPool.QueryRow(ctx, `
INSERT INTO agent (
workspace_id, name, description, runtime_mode, runtime_config,
runtime_id, visibility, max_concurrent_tasks, owner_id
)
VALUES ($1, $2, '', 'cloud', '{}'::jsonb, $3, 'workspace', 1, $4)
RETURNING id
`, testWorkspaceID, "Grouped Issues Test Agent", testRuntimeID, testUserID).Scan(&agentID); err != nil {
t.Fatalf("create agent: %v", err)
}
t.Cleanup(func() {
_, _ = testPool.Exec(context.Background(), `DELETE FROM agent WHERE id = $1`, agentID)
})
createIssue := func(title, assigneeType, assigneeID string, position float64) string {
t.Helper()
var number int32
if err := testPool.QueryRow(ctx, `
UPDATE workspace
SET issue_counter = GREATEST(
issue_counter,
(SELECT COALESCE(MAX(number), 0) FROM issue WHERE workspace_id = $1)
) + 1
WHERE id = $1
RETURNING issue_counter
`, testWorkspaceID).Scan(&number); err != nil {
t.Fatalf("next issue number: %v", err)
}
var id string
if err := testPool.QueryRow(ctx, `
INSERT INTO issue (
workspace_id, title, description, status, priority,
assignee_type, assignee_id, creator_type, creator_id,
position, number
)
VALUES ($1, $2, NULL, 'todo', 'none', $3, $4, 'member', $5, $6, $7)
RETURNING id
`, testWorkspaceID, title, assigneeType, assigneeID, testUserID, position, number).Scan(&id); err != nil {
t.Fatalf("create issue %q: %v", title, err)
}
t.Cleanup(func() {
_, _ = testPool.Exec(context.Background(), `DELETE FROM issue WHERE id = $1`, id)
})
return id
}
createIssue("Grouped member one", "member", assigneeID, 1)
createIssue("Grouped member two", "member", assigneeID, 2)
createIssue("Grouped member three", "member", assigneeID, 3)
createIssue("Grouped agent one", "agent", agentID, 1)
path := fmt.Sprintf(
"/api/issues/grouped?workspace_id=%s&group_by=assignee&statuses=todo&limit=2&assignee_filters=member:%s,agent:%s",
testWorkspaceID,
assigneeID,
agentID,
)
w := httptest.NewRecorder()
testHandler.ListGroupedIssues(w, newRequest("GET", path, nil))
if w.Code != http.StatusOK {
t.Fatalf("ListGroupedIssues: expected 200, got %d: %s", w.Code, w.Body.String())
}
var resp GroupedIssuesResponse
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
t.Fatalf("decode grouped response: %v", err)
}
memberGroupID := "assignee:member:" + assigneeID
agentGroupID := "assignee:agent:" + agentID
groups := map[string]IssueAssigneeGroupResponse{}
for _, group := range resp.Groups {
groups[group.ID] = group
}
memberGroup, ok := groups[memberGroupID]
if !ok {
t.Fatalf("missing member group %s in %#v", memberGroupID, resp.Groups)
}
if memberGroup.Total != 3 || len(memberGroup.Issues) != 2 {
t.Fatalf("member group total/page mismatch: total=%d len=%d", memberGroup.Total, len(memberGroup.Issues))
}
if memberGroup.Issues[0].Title != "Grouped member one" || memberGroup.Issues[1].Title != "Grouped member two" {
t.Fatalf("member group order mismatch: %#v", memberGroup.Issues)
}
agentGroup, ok := groups[agentGroupID]
if !ok {
t.Fatalf("missing agent group %s in %#v", agentGroupID, resp.Groups)
}
if agentGroup.Total != 1 || len(agentGroup.Issues) != 1 {
t.Fatalf("agent group total/page mismatch: total=%d len=%d", agentGroup.Total, len(agentGroup.Issues))
}
nextPath := fmt.Sprintf(
"/api/issues/grouped?workspace_id=%s&group_by=assignee&statuses=todo&limit=2&offset=2&group_assignee_type=member&group_assignee_id=%s",
testWorkspaceID,
assigneeID,
)
next := httptest.NewRecorder()
testHandler.ListGroupedIssues(next, newRequest("GET", nextPath, nil))
if next.Code != http.StatusOK {
t.Fatalf("ListGroupedIssues next page: expected 200, got %d: %s", next.Code, next.Body.String())
}
var nextResp GroupedIssuesResponse
if err := json.NewDecoder(next.Body).Decode(&nextResp); err != nil {
t.Fatalf("decode next grouped response: %v", err)
}
if len(nextResp.Groups) != 1 {
t.Fatalf("expected one next-page group, got %#v", nextResp.Groups)
}
if nextResp.Groups[0].ID != memberGroupID || nextResp.Groups[0].Total != 3 || len(nextResp.Groups[0].Issues) != 1 {
t.Fatalf("unexpected next-page group: %#v", nextResp.Groups[0])
}
if nextResp.Groups[0].Issues[0].Title != "Grouped member three" {
t.Fatalf("unexpected next-page issue: %#v", nextResp.Groups[0].Issues[0])
}
}