mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 03:38:32 +02:00
157 lines
5.1 KiB
Go
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])
|
|
}
|
|
}
|