package sqlite import ( "context" "strconv" "testing" "github.com/steveyegge/beads/internal/types" ) // TestAddIssueComment tests basic comment addition func TestAddIssueComment(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Create an issue issue := &types.Issue{ Title: "Test issue", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask, } if err := store.CreateIssue(ctx, issue, "test-user"); err != nil { t.Fatalf("CreateIssue failed: %v", err) } // Add a comment comment, err := store.AddIssueComment(ctx, issue.ID, "alice", "This is a test comment") if err != nil { t.Fatalf("AddIssueComment failed: %v", err) } // Verify comment fields if comment.IssueID != issue.ID { t.Errorf("Expected IssueID %s, got %s", issue.ID, comment.IssueID) } if comment.Author != "alice" { t.Errorf("Expected Author 'alice', got '%s'", comment.Author) } if comment.Text != "This is a test comment" { t.Errorf("Expected Text 'This is a test comment', got '%s'", comment.Text) } if comment.ID == 0 { t.Error("Expected non-zero comment ID") } if comment.CreatedAt.IsZero() { t.Error("Expected non-zero CreatedAt timestamp") } } // TestAddIssueCommentNonexistentIssue tests adding comment to non-existent issue func TestAddIssueCommentNonexistentIssue(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Try to add comment to non-existent issue _, err := store.AddIssueComment(ctx, "nonexistent-id", "alice", "comment") if err == nil { t.Fatal("Expected error when adding comment to non-existent issue, got nil") } } // TestGetIssueComments tests retrieving comments func TestGetIssueComments(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Create an issue issue := &types.Issue{ Title: "Test issue", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask, } if err := store.CreateIssue(ctx, issue, "test-user"); err != nil { t.Fatalf("CreateIssue failed: %v", err) } // Add multiple comments testComments := []struct { author string text string }{ {"alice", "First comment"}, {"bob", "Second comment"}, {"charlie", "Third comment"}, } for _, tc := range testComments { _, err := store.AddIssueComment(ctx, issue.ID, tc.author, tc.text) if err != nil { t.Fatalf("AddIssueComment failed: %v", err) } } // Retrieve comments comments, err := store.GetIssueComments(ctx, issue.ID) if err != nil { t.Fatalf("GetIssueComments failed: %v", err) } // Verify number of comments if len(comments) != len(testComments) { t.Fatalf("Expected %d comments, got %d", len(testComments), len(comments)) } // Verify comment content and ordering (should be chronological) for i, comment := range comments { if comment.Author != testComments[i].author { t.Errorf("Comment %d: expected author %s, got %s", i, testComments[i].author, comment.Author) } if comment.Text != testComments[i].text { t.Errorf("Comment %d: expected text %s, got %s", i, testComments[i].text, comment.Text) } if comment.IssueID != issue.ID { t.Errorf("Comment %d: expected IssueID %s, got %s", i, issue.ID, comment.IssueID) } } } // TestGetIssueCommentsOrdering tests that comments are returned in chronological order func TestGetIssueCommentsOrdering(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Create an issue issue := &types.Issue{ Title: "Test issue", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask, } if err := store.CreateIssue(ctx, issue, "test-user"); err != nil { t.Fatalf("CreateIssue failed: %v", err) } // Add comments with identifiable ordering for i := 1; i <= 5; i++ { text := "Comment " + strconv.Itoa(i) _, err := store.AddIssueComment(ctx, issue.ID, "alice", text) if err != nil { t.Fatalf("AddIssueComment failed: %v", err) } } // Retrieve comments comments, err := store.GetIssueComments(ctx, issue.ID) if err != nil { t.Fatalf("GetIssueComments failed: %v", err) } // Verify chronological ordering if len(comments) != 5 { t.Fatalf("Expected 5 comments, got %d", len(comments)) } for i := 0; i < len(comments); i++ { expectedText := "Comment " + strconv.Itoa(i+1) if comments[i].Text != expectedText { t.Errorf("Comment %d: expected text %s, got %s", i, expectedText, comments[i].Text) } // Verify timestamps are in ascending order if i > 0 && comments[i].CreatedAt.Before(comments[i-1].CreatedAt) { t.Errorf("Comments not in chronological order: comment %d (%v) is before comment %d (%v)", i, comments[i].CreatedAt, i-1, comments[i-1].CreatedAt) } } } // TestGetIssueCommentsEmpty tests retrieving comments for issue with no comments func TestGetIssueCommentsEmpty(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Create an issue issue := &types.Issue{ Title: "Test issue", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask, } if err := store.CreateIssue(ctx, issue, "test-user"); err != nil { t.Fatalf("CreateIssue failed: %v", err) } // Retrieve comments (should be empty) comments, err := store.GetIssueComments(ctx, issue.ID) if err != nil { t.Fatalf("GetIssueComments failed: %v", err) } if len(comments) != 0 { t.Errorf("Expected 0 comments, got %d", len(comments)) } } // TestGetIssueCommentsNonexistentIssue tests retrieving comments for non-existent issue func TestGetIssueCommentsNonexistentIssue(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Retrieve comments for non-existent issue comments, err := store.GetIssueComments(ctx, "nonexistent-id") if err != nil { t.Fatalf("GetIssueComments failed: %v", err) } // Should return empty slice, not error if len(comments) != 0 { t.Errorf("Expected 0 comments for non-existent issue, got %d", len(comments)) } } // TestAddIssueCommentEmptyText tests adding comment with empty text func TestAddIssueCommentEmptyText(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Create an issue issue := &types.Issue{ Title: "Test issue", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask, } if err := store.CreateIssue(ctx, issue, "test-user"); err != nil { t.Fatalf("CreateIssue failed: %v", err) } // Add comment with empty text (should succeed - validation is caller's responsibility) comment, err := store.AddIssueComment(ctx, issue.ID, "alice", "") if err != nil { t.Fatalf("AddIssueComment with empty text failed: %v", err) } if comment.Text != "" { t.Errorf("Expected empty text, got '%s'", comment.Text) } } // TestAddIssueCommentMarksDirty tests that adding a comment marks the issue dirty func TestAddIssueCommentMarksDirty(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Create an issue issue := &types.Issue{ Title: "Test issue", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask, } if err := store.CreateIssue(ctx, issue, "test-user"); err != nil { t.Fatalf("CreateIssue failed: %v", err) } // Clear dirty flag (simulating after export) if err := store.ClearDirtyIssuesByID(ctx, []string{issue.ID}); err != nil { t.Fatalf("ClearDirtyIssuesByID failed: %v", err) } // Add a comment _, err := store.AddIssueComment(ctx, issue.ID, "alice", "test comment") if err != nil { t.Fatalf("AddIssueComment failed: %v", err) } // Verify issue is marked dirty var exists bool err = store.db.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM dirty_issues WHERE issue_id = ?)`, issue.ID).Scan(&exists) if err != nil { t.Fatalf("Failed to check dirty flag: %v", err) } if !exists { t.Error("Expected issue to be marked dirty after adding comment") } } // TestGetIssueCommentsMultipleIssues tests that comments are properly isolated per issue func TestGetIssueCommentsMultipleIssues(t *testing.T) { store, cleanup := setupTestDB(t) defer cleanup() ctx := context.Background() // Create two issues issue1 := &types.Issue{ Title: "Issue 1", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask, } issue2 := &types.Issue{ Title: "Issue 2", Status: types.StatusOpen, Priority: 1, IssueType: types.TypeTask, } if err := store.CreateIssue(ctx, issue1, "test-user"); err != nil { t.Fatalf("CreateIssue failed: %v", err) } if err := store.CreateIssue(ctx, issue2, "test-user"); err != nil { t.Fatalf("CreateIssue failed: %v", err) } // Add comments to each issue _, err := store.AddIssueComment(ctx, issue1.ID, "alice", "Comment for issue 1") if err != nil { t.Fatalf("AddIssueComment failed: %v", err) } _, err = store.AddIssueComment(ctx, issue1.ID, "bob", "Another comment for issue 1") if err != nil { t.Fatalf("AddIssueComment failed: %v", err) } _, err = store.AddIssueComment(ctx, issue2.ID, "charlie", "Comment for issue 2") if err != nil { t.Fatalf("AddIssueComment failed: %v", err) } // Retrieve comments for issue 1 comments1, err := store.GetIssueComments(ctx, issue1.ID) if err != nil { t.Fatalf("GetIssueComments failed: %v", err) } // Retrieve comments for issue 2 comments2, err := store.GetIssueComments(ctx, issue2.ID) if err != nil { t.Fatalf("GetIssueComments failed: %v", err) } // Verify each issue has the correct number of comments if len(comments1) != 2 { t.Errorf("Expected 2 comments for issue 1, got %d", len(comments1)) } if len(comments2) != 1 { t.Errorf("Expected 1 comment for issue 2, got %d", len(comments2)) } // Verify comments belong to correct issues for _, c := range comments1 { if c.IssueID != issue1.ID { t.Errorf("Comment has wrong IssueID: expected %s, got %s", issue1.ID, c.IssueID) } } for _, c := range comments2 { if c.IssueID != issue2.ID { t.Errorf("Comment has wrong IssueID: expected %s, got %s", issue2.ID, c.IssueID) } } }