package main import ( "context" "os" "path/filepath" "testing" "github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/types" ) type labelTestHelper struct { s *sqlite.SQLiteStorage ctx context.Context t *testing.T } func (h *labelTestHelper) createIssue(title string, issueType types.IssueType, priority int) *types.Issue { issue := &types.Issue{ Title: title, Priority: priority, IssueType: issueType, Status: types.StatusOpen, } if err := h.s.CreateIssue(h.ctx, issue, "test-user"); err != nil { h.t.Fatalf("Failed to create issue: %v", err) } return issue } func (h *labelTestHelper) addLabel(issueID, label string) { if err := h.s.AddLabel(h.ctx, issueID, label, "test-user"); err != nil { h.t.Fatalf("Failed to add label '%s': %v", label, err) } } func (h *labelTestHelper) addLabels(issueID string, labels []string) { for _, label := range labels { h.addLabel(issueID, label) } } func (h *labelTestHelper) removeLabel(issueID, label string) { if err := h.s.RemoveLabel(h.ctx, issueID, label, "test-user"); err != nil { h.t.Fatalf("Failed to remove label '%s': %v", label, err) } } func (h *labelTestHelper) getLabels(issueID string) []string { labels, err := h.s.GetLabels(h.ctx, issueID) if err != nil { h.t.Fatalf("Failed to get labels: %v", err) } return labels } func (h *labelTestHelper) assertLabelCount(issueID string, expected int) { labels := h.getLabels(issueID) if len(labels) != expected { h.t.Errorf("Expected %d labels, got %d", expected, len(labels)) } } func (h *labelTestHelper) assertHasLabel(issueID, expected string) { labels := h.getLabels(issueID) for _, l := range labels { if l == expected { return } } h.t.Errorf("Expected label '%s' not found", expected) } func (h *labelTestHelper) assertHasLabels(issueID string, expected []string) { labels := h.getLabels(issueID) labelMap := make(map[string]bool) for _, l := range labels { labelMap[l] = true } for _, exp := range expected { if !labelMap[exp] { h.t.Errorf("Expected label '%s' not found", exp) } } } func (h *labelTestHelper) assertNotHasLabel(issueID, label string) { labels := h.getLabels(issueID) for _, l := range labels { if l == label { h.t.Errorf("Did not expect label '%s' but found it", label) } } } func (h *labelTestHelper) assertLabelEvent(issueID string, eventType types.EventType, labelName string) { events, err := h.s.GetEvents(h.ctx, issueID, 100) if err != nil { h.t.Fatalf("Failed to get events: %v", err) } expectedComment := "" if eventType == types.EventLabelAdded { expectedComment = "Added label: " + labelName } else if eventType == types.EventLabelRemoved { expectedComment = "Removed label: " + labelName } for _, e := range events { if e.EventType == eventType && e.Comment != nil && *e.Comment == expectedComment { return } } h.t.Errorf("Expected to find event %s for label %s", eventType, labelName) } func TestLabelCommands(t *testing.T) { tmpDir, err := os.MkdirTemp("", "bd-test-label-*") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) } defer os.RemoveAll(tmpDir) testDB := filepath.Join(tmpDir, "test.db") s := newTestStore(t, testDB) defer s.Close() ctx := context.Background() h := &labelTestHelper{s: s, ctx: ctx, t: t} t.Run("add label to issue", func(t *testing.T) { issue := h.createIssue("Test Issue", types.TypeBug, 1) h.addLabel(issue.ID, "bug") h.assertLabelCount(issue.ID, 1) h.assertHasLabel(issue.ID, "bug") }) t.Run("add multiple labels", func(t *testing.T) { issue := h.createIssue("Multi Label Issue", types.TypeFeature, 1) labels := []string{"feature", "high-priority", "needs-review"} h.addLabels(issue.ID, labels) h.assertLabelCount(issue.ID, 3) h.assertHasLabels(issue.ID, labels) }) t.Run("add duplicate label is idempotent", func(t *testing.T) { issue := h.createIssue("Duplicate Label Test", types.TypeTask, 1) h.addLabel(issue.ID, "duplicate") h.addLabel(issue.ID, "duplicate") h.assertLabelCount(issue.ID, 1) }) t.Run("remove label from issue", func(t *testing.T) { issue := h.createIssue("Remove Label Test", types.TypeBug, 1) h.addLabel(issue.ID, "temporary") h.removeLabel(issue.ID, "temporary") h.assertLabelCount(issue.ID, 0) }) t.Run("remove one of multiple labels", func(t *testing.T) { issue := h.createIssue("Multi Remove Test", types.TypeTask, 1) labels := []string{"label1", "label2", "label3"} h.addLabels(issue.ID, labels) h.removeLabel(issue.ID, "label2") h.assertLabelCount(issue.ID, 2) h.assertNotHasLabel(issue.ID, "label2") }) t.Run("remove non-existent label is no-op", func(t *testing.T) { issue := h.createIssue("Remove Non-Existent Test", types.TypeTask, 1) h.addLabel(issue.ID, "exists") h.removeLabel(issue.ID, "does-not-exist") h.assertLabelCount(issue.ID, 1) }) t.Run("get labels for issue with no labels", func(t *testing.T) { issue := h.createIssue("No Labels Test", types.TypeTask, 1) h.assertLabelCount(issue.ID, 0) }) t.Run("label operations create events", func(t *testing.T) { issue := h.createIssue("Event Test", types.TypeTask, 1) h.addLabel(issue.ID, "test-label") h.removeLabel(issue.ID, "test-label") h.assertLabelEvent(issue.ID, types.EventLabelAdded, "test-label") h.assertLabelEvent(issue.ID, types.EventLabelRemoved, "test-label") }) t.Run("labels persist after issue update", func(t *testing.T) { issue := h.createIssue("Persistence Test", types.TypeTask, 1) h.addLabel(issue.ID, "persistent") updates := map[string]interface{}{ "description": "Updated description", "priority": 2, } if err := s.UpdateIssue(ctx, issue.ID, updates, "test-user"); err != nil { t.Fatalf("Failed to update issue: %v", err) } h.assertLabelCount(issue.ID, 1) h.assertHasLabel(issue.ID, "persistent") }) t.Run("labels work with different issue types", func(t *testing.T) { issueTypes := []types.IssueType{ types.TypeBug, types.TypeFeature, types.TypeTask, types.TypeEpic, types.TypeChore, } for _, issueType := range issueTypes { issue := h.createIssue("Type Test: "+string(issueType), issueType, 1) labelName := "type-" + string(issueType) h.addLabel(issue.ID, labelName) h.assertLabelCount(issue.ID, 1) h.assertHasLabel(issue.ID, labelName) } }) }