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
6 changes: 3 additions & 3 deletions internal/dao/user_canvas.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ type UserCanvasListItem struct {
// ListByTenantIDs lists agent canvases accessible to the given owner IDs with optional
// keyword filter, tag filter, pagination, and ordering.
// Mirrors Python UserCanvasService.get_by_tenant_ids (list route only).
func (dao *UserCanvasDAO) ListByTenantIDs(ctx context.Context, db *gorm.DB, ownerIDs []string, userID string, page, pageSize int, orderby string, desc bool, keywords, canvasCategory, canvasType string, tags []string) ([]*UserCanvasListItem, int64, error) {
func (dao *UserCanvasDAO) ListByTenantIDs(ctx context.Context, db *gorm.DB, ownerIDs []string, userID string, page, pageSize int, orderby string, desc bool, keywords string, canvasCategories []string, canvasType string, tags []string) ([]*UserCanvasListItem, int64, error) {
if len(ownerIDs) == 0 {
return nil, 0, nil
}
Expand All @@ -373,8 +373,8 @@ func (dao *UserCanvasDAO) ListByTenantIDs(ctx context.Context, db *gorm.DB, owne
db.WithContext(ctx).Where("user_canvas.permission = ?", "team").
Or("user_canvas.user_id = ?", userID))

if canvasCategory != "" {
base = base.Where("user_canvas.canvas_category = ?", canvasCategory)
if len(canvasCategories) > 0 {
base = base.Where("user_canvas.canvas_category IN ?", canvasCategories)
}

if canvasType != "" {
Expand Down
46 changes: 44 additions & 2 deletions internal/dao/user_canvas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ func TestUserCanvasDAOKeywordSearchIncludesTags(t *testing.T) {
}
}

results, _, err := d.ListByTenantIDs(ctx, db, []string{"u1"}, "u1", 1, 10, "create_time", false, "finance", "", "", nil)
results, _, err := d.ListByTenantIDs(ctx, db, []string{"u1"}, "u1", 1, 10, "create_time", false, "finance", nil, "", nil)
if err != nil {
t.Fatalf("ListByTenantIDs: %v", err)
}
Expand All @@ -302,6 +302,48 @@ func TestUserCanvasDAOKeywordSearchIncludesTags(t *testing.T) {
}
}

// TestUserCanvasDAOListByTenantIDsCategoryUnion verifies that a multi-category
// canvas_category filter matches the union of the selected categories.
func TestUserCanvasDAOListByTenantIDsCategoryUnion(t *testing.T) {
db := setupUserCanvasTestDB(t)
if err := db.AutoMigrate(&entity.User{}); err != nil {
t.Fatalf("failed to migrate user: %v", err)
}
pushDB(t, db)
ctx := t.Context()
d := NewUserCanvasDAO()

if err := db.Create(&entity.User{ID: "u1", Nickname: "Owner", Email: "o@example.com"}).Error; err != nil {
t.Fatalf("create user: %v", err)
}
canvases := []entity.UserCanvas{
{ID: "c-wf-1", UserID: "u1", Permission: "me", CanvasCategory: "agent_canvas", Title: stringPtr("Workflow")},
{ID: "c-wf-2", UserID: "u1", Permission: "me", CanvasCategory: "agent_canvas", Title: stringPtr("Workflow Two")},
{ID: "c-df-1", UserID: "u1", Permission: "me", CanvasCategory: "dataflow_canvas", Title: stringPtr("Pipeline")},
}
for i := range canvases {
if err := db.Create(&canvases[i]).Error; err != nil {
t.Fatalf("create canvas: %v", err)
}
}

rows, total, err := d.ListByTenantIDs(ctx, db, []string{"u1"}, "u1", 1, 10, "create_time", false, "", []string{"dataflow_canvas", "agent_canvas"}, "", nil)
if err != nil {
t.Fatalf("ListByTenantIDs: %v", err)
}
if total != 3 || len(rows) != 3 {
t.Fatalf("category union returned total=%d rows=%d, want 3/3", total, len(rows))
}

rows, total, err = d.ListByTenantIDs(ctx, db, []string{"u1"}, "u1", 1, 10, "create_time", false, "", []string{"agent_canvas"}, "", nil)
if err != nil {
t.Fatalf("ListByTenantIDs (single category): %v", err)
}
if total != 2 || len(rows) != 2 {
t.Fatalf("single category returned total=%d rows=%d, want 2/2", total, len(rows))
}
}

// TestUserCanvasDAOOrderByTags verifies that agents can be sorted by
// their tags column (issue #14774: "Agents can be sorted by tag").
func TestUserCanvasDAOOrderByTags(t *testing.T) {
Expand All @@ -327,7 +369,7 @@ func TestUserCanvasDAOOrderByTags(t *testing.T) {
}
}

results, _, err := d.ListByTenantIDs(ctx, db, []string{"u1"}, "u1", 1, 10, "tags", false, "", "", "", nil)
results, _, err := d.ListByTenantIDs(ctx, db, []string{"u1"}, "u1", 1, 10, "tags", false, "", nil, "", nil)
if err != nil {
t.Fatalf("ListByTenantIDs: %v", err)
}
Expand Down
4 changes: 1 addition & 3 deletions internal/service/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -668,10 +668,8 @@ func (s *AgentService) ListAgents(ctx context.Context, userID string, keywords s
// Fetch agents. In merge/mixed modes we disable SQL pagination (page=0) and
// paginate in Go after interleaving with groups, matching Python.
listPage, listSize := page, pageSize
agentCategoryFilter := canvasCategory
if mergeMode || (wantsGroups && len(agentCategories) > 0) {
listPage, listSize = 0, 0
agentCategoryFilter = strings.Join(agentCategories, ",")
}
canvases, total, err := s.canvasDAO.ListByTenantIDs(
ctx,
Expand All @@ -683,7 +681,7 @@ func (s *AgentService) ListAgents(ctx context.Context, userID string, keywords s
orderBy,
desc,
keywords,
agentCategoryFilter,
agentCategories,
canvasType,
tags,
)
Expand Down
38 changes: 38 additions & 0 deletions internal/service/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1931,6 +1931,44 @@ func TestListAgentsIncludesReleaseTime(t *testing.T) {
}
}

// TestListAgents_MultiCategoryFilter verifies that a comma-separated
// canvas_category query (the agents page multi-select filter) returns the
// union of the selected categories instead of an exact string match.
func TestListAgents_MultiCategoryFilter(t *testing.T) {
setupAgentSessionServiceTest(t)

if err := dao.DB.Create(&entity.User{ID: "user-1", Nickname: "owner", Email: "owner@test.com"}).Error; err != nil {
t.Fatalf("failed to seed user: %v", err)
}
canvases := []entity.UserCanvas{
{ID: "canvas-wf-1", UserID: "user-1", Title: sptr("Workflow One"), CanvasCategory: "agent_canvas"},
{ID: "canvas-wf-2", UserID: "user-1", Title: sptr("Workflow Two"), CanvasCategory: "agent_canvas"},
{ID: "canvas-df-1", UserID: "user-1", Title: sptr("Pipeline One"), CanvasCategory: "dataflow_canvas"},
}
for i := range canvases {
if err := dao.DB.Create(&canvases[i]).Error; err != nil {
t.Fatalf("failed to create canvas %s: %v", canvases[i].ID, err)
}
}

ctx := t.Context()
resp, code, err := NewAgentService().ListAgents(ctx, "user-1", "", 1, 30, "create_time", true, nil, "dataflow_canvas,agent_canvas", "", nil)
if err != nil || code != common.CodeSuccess {
t.Fatalf("ListAgents failed: code=%v err=%v", code, err)
}
if resp.Total != 3 || len(resp.Canvas) != 3 {
t.Fatalf("multi-category filter returned total=%d rows=%d, want 3/3", resp.Total, len(resp.Canvas))
}

single, code, err := NewAgentService().ListAgents(ctx, "user-1", "", 1, 30, "create_time", true, nil, "agent_canvas", "", nil)
if err != nil || code != common.CodeSuccess {
t.Fatalf("ListAgents (single category) failed: code=%v err=%v", code, err)
}
if single.Total != 2 || len(single.Canvas) != 2 {
t.Fatalf("single-category filter returned total=%d rows=%d, want 2/2", single.Total, len(single.Canvas))
}
}

// TestListAgents_MergesCompilationTemplateGroups verifies that a compilation
// template group owned by the caller appears in the merged /agents list
// (no canvas_category filter), carrying the "compilation_template_group" type
Expand Down
Loading