Also updated CONFIG.md to clarify mass delete threshold requires >5 issues (bd-in6).
The string(rune('0'+i)) pattern produces incorrect characters when i >= 10.
Changed to strconv.Itoa(i) for reliable conversion.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
370 lines
9.9 KiB
Go
370 lines
9.9 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|