test: Add comprehensive test coverage for storage layer

Added test files for core SQLite storage functionality:
- beads_test.go: Database path detection tests
- dirty_test.go: Dirty tracking for auto-flush
- events_test.go: Event logging tests
- labels_test.go: Label management tests
- sqlite_test.go: Added metadata tests (SetMetadata, GetMetadata)

Merged with upstream TestParallelIssueCreation (bd-89 regression test).

All tests passing. Ready to push.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-10-15 01:30:28 -07:00
parent 42d2f71925
commit 587e1d9d0f
6 changed files with 1613 additions and 226 deletions

File diff suppressed because one or more lines are too long

258
beads_test.go Normal file
View File

@@ -0,0 +1,258 @@
package beads
import (
"os"
"path/filepath"
"testing"
)
func TestFindDatabasePathEnvVar(t *testing.T) {
// Save original env var
originalEnv := os.Getenv("BEADS_DB")
defer func() {
if originalEnv != "" {
os.Setenv("BEADS_DB", originalEnv)
} else {
os.Unsetenv("BEADS_DB")
}
}()
// Set env var to a test path
testPath := "/test/path/test.db"
os.Setenv("BEADS_DB", testPath)
result := FindDatabasePath()
if result != testPath {
t.Errorf("Expected '%s', got '%s'", testPath, result)
}
}
func TestFindDatabasePathInTree(t *testing.T) {
// Save original env var and working directory
originalEnv := os.Getenv("BEADS_DB")
originalWd, _ := os.Getwd()
defer func() {
if originalEnv != "" {
os.Setenv("BEADS_DB", originalEnv)
} else {
os.Unsetenv("BEADS_DB")
}
os.Chdir(originalWd)
}()
// Clear env var
os.Unsetenv("BEADS_DB")
// Create temporary directory structure
tmpDir, err := os.MkdirTemp("", "beads-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
// Create .beads directory with a database file
beadsDir := filepath.Join(tmpDir, ".beads")
err = os.MkdirAll(beadsDir, 0o755)
if err != nil {
t.Fatalf("Failed to create .beads dir: %v", err)
}
dbPath := filepath.Join(beadsDir, "test.db")
f, err := os.Create(dbPath)
if err != nil {
t.Fatalf("Failed to create db file: %v", err)
}
f.Close()
// Create a subdirectory and change to it
subDir := filepath.Join(tmpDir, "sub", "nested")
err = os.MkdirAll(subDir, 0o755)
if err != nil {
t.Fatalf("Failed to create subdirectory: %v", err)
}
err = os.Chdir(subDir)
if err != nil {
t.Fatalf("Failed to change directory: %v", err)
}
// Should find the database in the parent directory tree
result := FindDatabasePath()
// Resolve symlinks for both paths (macOS uses /private/var symlinked to /var)
expectedPath, err := filepath.EvalSymlinks(dbPath)
if err != nil {
expectedPath = dbPath
}
resultPath, err := filepath.EvalSymlinks(result)
if err != nil {
resultPath = result
}
if resultPath != expectedPath {
t.Errorf("Expected '%s', got '%s'", expectedPath, resultPath)
}
}
func TestFindDatabasePathNotFound(t *testing.T) {
// Save original env var and working directory
originalEnv := os.Getenv("BEADS_DB")
originalWd, _ := os.Getwd()
defer func() {
if originalEnv != "" {
os.Setenv("BEADS_DB", originalEnv)
} else {
os.Unsetenv("BEADS_DB")
}
os.Chdir(originalWd)
}()
// Clear env var
os.Unsetenv("BEADS_DB")
// Create temporary directory without .beads
tmpDir, err := os.MkdirTemp("", "beads-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
err = os.Chdir(tmpDir)
if err != nil {
t.Fatalf("Failed to change directory: %v", err)
}
// Should return empty string (no database found)
result := FindDatabasePath()
// Result might be the home directory default if it exists, or empty string
// Just verify it doesn't error
_ = result
}
func TestFindJSONLPathWithExistingFile(t *testing.T) {
// Create temporary directory
tmpDir, err := os.MkdirTemp("", "beads-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
// Create a .jsonl file
jsonlPath := filepath.Join(tmpDir, "custom.jsonl")
f, err := os.Create(jsonlPath)
if err != nil {
t.Fatalf("Failed to create jsonl file: %v", err)
}
f.Close()
// Create a fake database path in the same directory
dbPath := filepath.Join(tmpDir, "test.db")
// Should find the existing .jsonl file
result := FindJSONLPath(dbPath)
if result != jsonlPath {
t.Errorf("Expected '%s', got '%s'", jsonlPath, result)
}
}
func TestFindJSONLPathDefault(t *testing.T) {
// Create temporary directory
tmpDir, err := os.MkdirTemp("", "beads-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
// Create a fake database path (no .jsonl files exist)
dbPath := filepath.Join(tmpDir, "test.db")
// Should return default issues.jsonl
result := FindJSONLPath(dbPath)
expected := filepath.Join(tmpDir, "issues.jsonl")
if result != expected {
t.Errorf("Expected '%s', got '%s'", expected, result)
}
}
func TestFindJSONLPathEmpty(t *testing.T) {
// Empty database path should return empty string
result := FindJSONLPath("")
if result != "" {
t.Errorf("Expected empty string for empty db path, got '%s'", result)
}
}
func TestFindJSONLPathMultipleFiles(t *testing.T) {
// Create temporary directory
tmpDir, err := os.MkdirTemp("", "beads-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
// Create multiple .jsonl files
jsonlFiles := []string{"issues.jsonl", "backup.jsonl", "archive.jsonl"}
for _, filename := range jsonlFiles {
f, err := os.Create(filepath.Join(tmpDir, filename))
if err != nil {
t.Fatalf("Failed to create jsonl file: %v", err)
}
f.Close()
}
// Create a fake database path
dbPath := filepath.Join(tmpDir, "test.db")
// Should return the first .jsonl file found (lexicographically sorted by Glob)
result := FindJSONLPath(dbPath)
// Verify it's one of the .jsonl files we created
found := false
for _, filename := range jsonlFiles {
if result == filepath.Join(tmpDir, filename) {
found = true
break
}
}
if !found {
t.Errorf("Expected one of the created .jsonl files, got '%s'", result)
}
}
func TestFindDatabasePathHomeDefault(t *testing.T) {
// This test verifies that if no database is found, it falls back to home directory
// We can't reliably test this without modifying the home directory, so we'll skip
// creating the file and just verify the function doesn't crash
originalEnv := os.Getenv("BEADS_DB")
originalWd, _ := os.Getwd()
defer func() {
if originalEnv != "" {
os.Setenv("BEADS_DB", originalEnv)
} else {
os.Unsetenv("BEADS_DB")
}
os.Chdir(originalWd)
}()
os.Unsetenv("BEADS_DB")
// Create an empty temp directory and cd to it
tmpDir, err := os.MkdirTemp("", "beads-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
err = os.Chdir(tmpDir)
if err != nil {
t.Fatalf("Failed to change directory: %v", err)
}
// Call FindDatabasePath - it might return home dir default or empty string
result := FindDatabasePath()
// If result is not empty, verify it contains .beads
if result != "" && !filepath.IsAbs(result) {
t.Errorf("Expected absolute path or empty string, got '%s'", result)
}
}

View File

@@ -0,0 +1,313 @@
package sqlite
import (
"context"
"testing"
"time"
"github.com/steveyegge/beads/internal/types"
)
func TestMarkIssueDirty(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create an issue first
issue := &types.Issue{
Title: "Test issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
err := store.CreateIssue(ctx, issue, "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Issue should already be marked dirty by CreateIssue
dirtyIssues, err := store.GetDirtyIssues(ctx)
if err != nil {
t.Fatalf("GetDirtyIssues failed: %v", err)
}
if len(dirtyIssues) != 1 {
t.Fatalf("Expected 1 dirty issue, got %d", len(dirtyIssues))
}
if dirtyIssues[0] != issue.ID {
t.Errorf("Expected dirty issue %s, got %s", issue.ID, dirtyIssues[0])
}
// Clear dirty issues
err = store.ClearDirtyIssues(ctx)
if err != nil {
t.Fatalf("ClearDirtyIssues failed: %v", err)
}
// Verify cleared
dirtyIssues, err = store.GetDirtyIssues(ctx)
if err != nil {
t.Fatalf("GetDirtyIssues failed: %v", err)
}
if len(dirtyIssues) != 0 {
t.Errorf("Expected 0 dirty issues after clear, got %d", len(dirtyIssues))
}
// Mark it dirty again manually
err = store.MarkIssueDirty(ctx, issue.ID)
if err != nil {
t.Fatalf("MarkIssueDirty failed: %v", err)
}
// Verify it's dirty again
dirtyIssues, err = store.GetDirtyIssues(ctx)
if err != nil {
t.Fatalf("GetDirtyIssues failed: %v", err)
}
if len(dirtyIssues) != 1 {
t.Errorf("Expected 1 dirty issue after marking, got %d", len(dirtyIssues))
}
}
func TestMarkIssuesDirty(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create multiple issues
var issueIDs []string
for i := 0; i < 3; i++ {
issue := &types.Issue{
Title: "Test issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
err := store.CreateIssue(ctx, issue, "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
issueIDs = append(issueIDs, issue.ID)
}
// Clear all dirty issues
err := store.ClearDirtyIssues(ctx)
if err != nil {
t.Fatalf("ClearDirtyIssues failed: %v", err)
}
// Mark multiple issues dirty at once
err = store.MarkIssuesDirty(ctx, issueIDs)
if err != nil {
t.Fatalf("MarkIssuesDirty failed: %v", err)
}
// Verify all are dirty
dirtyIssues, err := store.GetDirtyIssues(ctx)
if err != nil {
t.Fatalf("GetDirtyIssues failed: %v", err)
}
if len(dirtyIssues) != 3 {
t.Errorf("Expected 3 dirty issues, got %d", len(dirtyIssues))
}
}
func TestMarkIssuesDirtyEmpty(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Mark empty slice - should not error
err := store.MarkIssuesDirty(ctx, []string{})
if err != nil {
t.Errorf("MarkIssuesDirty with empty slice should not error: %v", err)
}
}
func TestGetDirtyIssueCount(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Count should be 0 initially
count, err := store.GetDirtyIssueCount(ctx)
if err != nil {
t.Fatalf("GetDirtyIssueCount failed: %v", err)
}
if count != 0 {
t.Errorf("Expected 0 dirty issues, got %d", count)
}
// Create an issue
issue := &types.Issue{
Title: "Test issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
err = store.CreateIssue(ctx, issue, "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Count should be 1 now
count, err = store.GetDirtyIssueCount(ctx)
if err != nil {
t.Fatalf("GetDirtyIssueCount failed: %v", err)
}
if count != 1 {
t.Errorf("Expected 1 dirty issue, got %d", count)
}
}
func TestClearDirtyIssuesByID(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create multiple issues
var issueIDs []string
for i := 0; i < 5; i++ {
issue := &types.Issue{
Title: "Test issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
err := store.CreateIssue(ctx, issue, "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
issueIDs = append(issueIDs, issue.ID)
}
// Verify all are dirty
count, err := store.GetDirtyIssueCount(ctx)
if err != nil {
t.Fatalf("GetDirtyIssueCount failed: %v", err)
}
if count != 5 {
t.Errorf("Expected 5 dirty issues, got %d", count)
}
// Clear only the first 3
err = store.ClearDirtyIssuesByID(ctx, issueIDs[:3])
if err != nil {
t.Fatalf("ClearDirtyIssuesByID failed: %v", err)
}
// Should have 2 remaining
count, err = store.GetDirtyIssueCount(ctx)
if err != nil {
t.Fatalf("GetDirtyIssueCount failed: %v", err)
}
if count != 2 {
t.Errorf("Expected 2 dirty issues remaining, got %d", count)
}
// Verify the correct ones remain
dirtyIssues, err := store.GetDirtyIssues(ctx)
if err != nil {
t.Fatalf("GetDirtyIssues failed: %v", err)
}
for _, id := range dirtyIssues {
if id == issueIDs[0] || id == issueIDs[1] || id == issueIDs[2] {
t.Errorf("Issue %s should have been cleared", id)
}
}
}
func TestClearDirtyIssuesByIDEmpty(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Clear empty slice - should not error
err := store.ClearDirtyIssuesByID(ctx, []string{})
if err != nil {
t.Errorf("ClearDirtyIssuesByID with empty slice should not error: %v", err)
}
}
func TestDirtyIssuesOrdering(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create issues with slight time delays to ensure ordering
var issueIDs []string
for i := 0; i < 3; i++ {
issue := &types.Issue{
Title: "Test issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
err := store.CreateIssue(ctx, issue, "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
issueIDs = append(issueIDs, issue.ID)
time.Sleep(10 * time.Millisecond) // Ensure different timestamps
}
// Get dirty issues - should be in order by marked_at (oldest first)
dirtyIssues, err := store.GetDirtyIssues(ctx)
if err != nil {
t.Fatalf("GetDirtyIssues failed: %v", err)
}
if len(dirtyIssues) != 3 {
t.Fatalf("Expected 3 dirty issues, got %d", len(dirtyIssues))
}
// Verify order matches creation order
for i, id := range issueIDs {
if dirtyIssues[i] != id {
t.Errorf("Expected issue %d to be %s, got %s", i, id, dirtyIssues[i])
}
}
}
func TestDirtyIssuesUpdateTimestamp(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,
}
err := store.CreateIssue(ctx, issue, "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Wait a bit
time.Sleep(50 * time.Millisecond)
// Mark dirty again - should update timestamp
err = store.MarkIssueDirty(ctx, issue.ID)
if err != nil {
t.Fatalf("MarkIssueDirty failed: %v", err)
}
// Should still have only 1 dirty issue (ON CONFLICT DO UPDATE)
count, err := store.GetDirtyIssueCount(ctx)
if err != nil {
t.Fatalf("GetDirtyIssueCount failed: %v", err)
}
if count != 1 {
t.Errorf("Expected 1 dirty issue after re-marking, got %d", count)
}
}

View File

@@ -0,0 +1,339 @@
package sqlite
import (
"context"
"testing"
"github.com/steveyegge/beads/internal/types"
)
func TestAddComment(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,
}
err := store.CreateIssue(ctx, issue, "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Add a comment
err = store.AddComment(ctx, issue.ID, "alice", "This is a test comment")
if err != nil {
t.Fatalf("AddComment failed: %v", err)
}
// Get events to verify comment was added
events, err := store.GetEvents(ctx, issue.ID, 0)
if err != nil {
t.Fatalf("GetEvents failed: %v", err)
}
// Should have 2 events: created and commented
if len(events) < 2 {
t.Fatalf("Expected at least 2 events, got %d", len(events))
}
// Find the comment event (most recent should be first due to DESC order)
var commentEvent *types.Event
for _, event := range events {
if event.EventType == types.EventCommented {
commentEvent = event
break
}
}
if commentEvent == nil {
t.Fatal("Comment event not found")
}
if commentEvent.Actor != "alice" {
t.Errorf("Expected actor 'alice', got '%s'", commentEvent.Actor)
}
if commentEvent.Comment == nil || *commentEvent.Comment != "This is a test comment" {
t.Errorf("Expected comment 'This is a test comment', got %v", commentEvent.Comment)
}
}
func TestAddMultipleComments(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,
}
err := store.CreateIssue(ctx, issue, "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Add multiple comments
comments := []struct {
actor string
comment string
}{
{"alice", "First comment"},
{"bob", "Second comment"},
{"charlie", "Third comment"},
}
for _, c := range comments {
err = store.AddComment(ctx, issue.ID, c.actor, c.comment)
if err != nil {
t.Fatalf("AddComment failed: %v", err)
}
}
// Get events
events, err := store.GetEvents(ctx, issue.ID, 0)
if err != nil {
t.Fatalf("GetEvents failed: %v", err)
}
// Count comment events
commentCount := 0
var commentEvents []*types.Event
for _, event := range events {
if event.EventType == types.EventCommented {
commentCount++
commentEvents = append(commentEvents, event)
}
}
if commentCount != 3 {
t.Fatalf("Expected 3 comment events, got %d", commentCount)
}
// Verify we can find all three comments
foundComments := make(map[string]bool)
for _, event := range commentEvents {
if event.Comment != nil {
foundComments[*event.Comment] = true
}
}
expectedComments := []string{"First comment", "Second comment", "Third comment"}
for _, expected := range expectedComments {
if !foundComments[expected] {
t.Errorf("Expected to find comment '%s'", expected)
}
}
}
func TestGetEventsWithLimit(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,
}
err := store.CreateIssue(ctx, issue, "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Add 5 comments
for i := 0; i < 5; i++ {
err = store.AddComment(ctx, issue.ID, "alice", "Comment")
if err != nil {
t.Fatalf("AddComment failed: %v", err)
}
}
// Get events with limit
events, err := store.GetEvents(ctx, issue.ID, 3)
if err != nil {
t.Fatalf("GetEvents failed: %v", err)
}
if len(events) != 3 {
t.Errorf("Expected 3 events with limit, got %d", len(events))
}
}
func TestGetEventsEmpty(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Get events for non-existent issue
events, err := store.GetEvents(ctx, "bd-999", 0)
if err != nil {
t.Fatalf("GetEvents failed: %v", err)
}
if len(events) != 0 {
t.Errorf("Expected 0 events for non-existent issue, got %d", len(events))
}
}
func TestAddCommentMarksDirty(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,
}
err := store.CreateIssue(ctx, issue, "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Clear dirty issues
err = store.ClearDirtyIssues(ctx)
if err != nil {
t.Fatalf("ClearDirtyIssues failed: %v", err)
}
// Add comment - should mark issue dirty
err = store.AddComment(ctx, issue.ID, "alice", "Test comment")
if err != nil {
t.Fatalf("AddComment failed: %v", err)
}
// Verify issue is dirty
dirtyIssues, err := store.GetDirtyIssues(ctx)
if err != nil {
t.Fatalf("GetDirtyIssues failed: %v", err)
}
if len(dirtyIssues) != 1 || dirtyIssues[0] != issue.ID {
t.Error("Expected issue to be marked dirty after adding comment")
}
}
func TestAddCommentUpdatesTimestamp(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,
}
err := store.CreateIssue(ctx, issue, "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
originalUpdatedAt := issue.UpdatedAt
// Add comment
err = store.AddComment(ctx, issue.ID, "alice", "Test comment")
if err != nil {
t.Fatalf("AddComment failed: %v", err)
}
// Get issue again and verify updated_at changed
updatedIssue, err := store.GetIssue(ctx, issue.ID)
if err != nil {
t.Fatalf("GetIssue failed: %v", err)
}
if !updatedIssue.UpdatedAt.After(originalUpdatedAt) {
t.Error("Expected updated_at to be updated after adding comment")
}
}
func TestEventTypesInHistory(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,
}
err := store.CreateIssue(ctx, issue, "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Perform various operations that create events
err = store.UpdateIssue(ctx, issue.ID, map[string]interface{}{
"priority": 2,
}, "test-user")
if err != nil {
t.Fatalf("UpdateIssue failed: %v", err)
}
err = store.AddComment(ctx, issue.ID, "alice", "A comment")
if err != nil {
t.Fatalf("AddComment failed: %v", err)
}
err = store.AddLabel(ctx, issue.ID, "bug", "test-user")
if err != nil {
t.Fatalf("AddLabel failed: %v", err)
}
err = store.CloseIssue(ctx, issue.ID, "Done", "test-user")
if err != nil {
t.Fatalf("CloseIssue failed: %v", err)
}
// Get events
events, err := store.GetEvents(ctx, issue.ID, 0)
if err != nil {
t.Fatalf("GetEvents failed: %v", err)
}
// Should have multiple event types
eventTypes := make(map[types.EventType]bool)
for _, event := range events {
eventTypes[event.EventType] = true
}
// Verify we have different event types
if !eventTypes[types.EventCreated] {
t.Error("Expected EventCreated in history")
}
if !eventTypes[types.EventUpdated] {
t.Error("Expected EventUpdated in history")
}
if !eventTypes[types.EventCommented] {
t.Error("Expected EventCommented in history")
}
if !eventTypes[types.EventLabelAdded] {
t.Error("Expected EventLabelAdded in history")
}
if !eventTypes[types.EventClosed] {
t.Error("Expected EventClosed in history")
}
}

View File

@@ -0,0 +1,394 @@
package sqlite
import (
"context"
"testing"
"github.com/steveyegge/beads/internal/types"
)
func TestAddLabel(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,
}
err := store.CreateIssue(ctx, issue, "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Add a label
err = store.AddLabel(ctx, issue.ID, "bug", "test-user")
if err != nil {
t.Fatalf("AddLabel failed: %v", err)
}
// Get labels
labels, err := store.GetLabels(ctx, issue.ID)
if err != nil {
t.Fatalf("GetLabels failed: %v", err)
}
if len(labels) != 1 {
t.Fatalf("Expected 1 label, got %d", len(labels))
}
if labels[0] != "bug" {
t.Errorf("Expected label 'bug', got '%s'", labels[0])
}
}
func TestAddMultipleLabels(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,
}
err := store.CreateIssue(ctx, issue, "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Add multiple labels
labelsToAdd := []string{"bug", "critical", "ui"}
for _, label := range labelsToAdd {
err = store.AddLabel(ctx, issue.ID, label, "test-user")
if err != nil {
t.Fatalf("AddLabel failed for %s: %v", label, err)
}
}
// Get labels
labels, err := store.GetLabels(ctx, issue.ID)
if err != nil {
t.Fatalf("GetLabels failed: %v", err)
}
if len(labels) != 3 {
t.Fatalf("Expected 3 labels, got %d", len(labels))
}
// Verify labels are sorted alphabetically
expectedOrder := []string{"bug", "critical", "ui"}
for i, expected := range expectedOrder {
if labels[i] != expected {
t.Errorf("Expected label %d to be '%s', got '%s'", i, expected, labels[i])
}
}
}
func TestAddDuplicateLabel(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,
}
err := store.CreateIssue(ctx, issue, "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Add a label
err = store.AddLabel(ctx, issue.ID, "bug", "test-user")
if err != nil {
t.Fatalf("AddLabel failed: %v", err)
}
// Add the same label again - should not error (INSERT OR IGNORE)
err = store.AddLabel(ctx, issue.ID, "bug", "test-user")
if err != nil {
t.Fatalf("AddLabel duplicate should not error: %v", err)
}
// Should still have only 1 label
labels, err := store.GetLabels(ctx, issue.ID)
if err != nil {
t.Fatalf("GetLabels failed: %v", err)
}
if len(labels) != 1 {
t.Errorf("Expected 1 label after duplicate add, got %d", len(labels))
}
}
func TestRemoveLabel(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,
}
err := store.CreateIssue(ctx, issue, "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Add labels
err = store.AddLabel(ctx, issue.ID, "bug", "test-user")
if err != nil {
t.Fatalf("AddLabel failed: %v", err)
}
err = store.AddLabel(ctx, issue.ID, "critical", "test-user")
if err != nil {
t.Fatalf("AddLabel failed: %v", err)
}
// Remove one label
err = store.RemoveLabel(ctx, issue.ID, "bug", "test-user")
if err != nil {
t.Fatalf("RemoveLabel failed: %v", err)
}
// Get labels
labels, err := store.GetLabels(ctx, issue.ID)
if err != nil {
t.Fatalf("GetLabels failed: %v", err)
}
if len(labels) != 1 {
t.Fatalf("Expected 1 label after removal, got %d", len(labels))
}
if labels[0] != "critical" {
t.Errorf("Expected remaining label 'critical', got '%s'", labels[0])
}
}
func TestRemoveNonexistentLabel(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,
}
err := store.CreateIssue(ctx, issue, "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Remove a label that doesn't exist - should not error
err = store.RemoveLabel(ctx, issue.ID, "nonexistent", "test-user")
if err != nil {
t.Fatalf("RemoveLabel for nonexistent label should not error: %v", err)
}
}
func TestGetLabelsEmpty(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create an issue without labels
issue := &types.Issue{
Title: "Test issue",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
err := store.CreateIssue(ctx, issue, "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Get labels - should return nil or empty slice (both valid in Go)
labels, err := store.GetLabels(ctx, issue.ID)
if err != nil {
t.Fatalf("GetLabels failed: %v", err)
}
if len(labels) != 0 {
t.Errorf("Expected 0 labels, got %d", len(labels))
}
}
func TestGetIssuesByLabel(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Create issues with different labels
issue1 := &types.Issue{
Title: "Bug 1",
Status: types.StatusOpen,
Priority: 0,
IssueType: types.TypeBug,
}
err := store.CreateIssue(ctx, issue1, "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
err = store.AddLabel(ctx, issue1.ID, "critical", "test-user")
if err != nil {
t.Fatalf("AddLabel failed: %v", err)
}
issue2 := &types.Issue{
Title: "Bug 2",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeBug,
}
err = store.CreateIssue(ctx, issue2, "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
err = store.AddLabel(ctx, issue2.ID, "critical", "test-user")
if err != nil {
t.Fatalf("AddLabel failed: %v", err)
}
issue3 := &types.Issue{
Title: "Feature",
Status: types.StatusOpen,
Priority: 2,
IssueType: types.TypeFeature,
}
err = store.CreateIssue(ctx, issue3, "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
err = store.AddLabel(ctx, issue3.ID, "enhancement", "test-user")
if err != nil {
t.Fatalf("AddLabel failed: %v", err)
}
// Get issues by label "critical"
issues, err := store.GetIssuesByLabel(ctx, "critical")
if err != nil {
t.Fatalf("GetIssuesByLabel failed: %v", err)
}
if len(issues) != 2 {
t.Fatalf("Expected 2 issues with 'critical' label, got %d", len(issues))
}
// Verify both critical issues are returned
foundIssue1 := false
foundIssue2 := false
for _, issue := range issues {
if issue.ID == issue1.ID {
foundIssue1 = true
}
if issue.ID == issue2.ID {
foundIssue2 = true
}
}
if !foundIssue1 || !foundIssue2 {
t.Error("Expected both critical issues to be returned")
}
}
func TestGetIssuesByLabelEmpty(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Get issues by nonexistent label
issues, err := store.GetIssuesByLabel(ctx, "nonexistent")
if err != nil {
t.Fatalf("GetIssuesByLabel failed: %v", err)
}
if len(issues) != 0 {
t.Errorf("Expected 0 issues for nonexistent label, got %d", len(issues))
}
}
func TestLabelMarksDirty(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,
}
err := store.CreateIssue(ctx, issue, "test-user")
if err != nil {
t.Fatalf("CreateIssue failed: %v", err)
}
// Clear dirty issues
err = store.ClearDirtyIssues(ctx)
if err != nil {
t.Fatalf("ClearDirtyIssues failed: %v", err)
}
// Add label - should mark issue dirty
err = store.AddLabel(ctx, issue.ID, "bug", "test-user")
if err != nil {
t.Fatalf("AddLabel failed: %v", err)
}
// Verify issue is dirty
dirtyIssues, err := store.GetDirtyIssues(ctx)
if err != nil {
t.Fatalf("GetDirtyIssues failed: %v", err)
}
if len(dirtyIssues) != 1 || dirtyIssues[0] != issue.ID {
t.Error("Expected issue to be marked dirty after adding label")
}
// Clear dirty again
err = store.ClearDirtyIssues(ctx)
if err != nil {
t.Fatalf("ClearDirtyIssues failed: %v", err)
}
// Remove label - should mark issue dirty
err = store.RemoveLabel(ctx, issue.ID, "bug", "test-user")
if err != nil {
t.Fatalf("RemoveLabel failed: %v", err)
}
// Verify issue is dirty again
dirtyIssues, err = store.GetDirtyIssues(ctx)
if err != nil {
t.Fatalf("GetDirtyIssues failed: %v", err)
}
if len(dirtyIssues) != 1 || dirtyIssues[0] != issue.ID {
t.Error("Expected issue to be marked dirty after removing label")
}
}

View File

@@ -499,3 +499,104 @@ func TestParallelIssueCreation(t *testing.T) {
}
}
}
func TestSetAndGetMetadata(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Set metadata
err := store.SetMetadata(ctx, "import_hash", "abc123def456")
if err != nil {
t.Fatalf("SetMetadata failed: %v", err)
}
// Get metadata
value, err := store.GetMetadata(ctx, "import_hash")
if err != nil {
t.Fatalf("GetMetadata failed: %v", err)
}
if value != "abc123def456" {
t.Errorf("Expected 'abc123def456', got '%s'", value)
}
}
func TestGetMetadataNotFound(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Get non-existent metadata
value, err := store.GetMetadata(ctx, "nonexistent")
if err != nil {
t.Fatalf("GetMetadata failed: %v", err)
}
if value != "" {
t.Errorf("Expected empty string for non-existent key, got '%s'", value)
}
}
func TestSetMetadataUpdate(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Set initial value
err := store.SetMetadata(ctx, "test_key", "initial_value")
if err != nil {
t.Fatalf("SetMetadata failed: %v", err)
}
// Update value
err = store.SetMetadata(ctx, "test_key", "updated_value")
if err != nil {
t.Fatalf("SetMetadata update failed: %v", err)
}
// Verify updated value
value, err := store.GetMetadata(ctx, "test_key")
if err != nil {
t.Fatalf("GetMetadata failed: %v", err)
}
if value != "updated_value" {
t.Errorf("Expected 'updated_value', got '%s'", value)
}
}
func TestMetadataMultipleKeys(t *testing.T) {
store, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
// Set multiple metadata keys
keys := map[string]string{
"key1": "value1",
"key2": "value2",
"key3": "value3",
}
for key, value := range keys {
err := store.SetMetadata(ctx, key, value)
if err != nil {
t.Fatalf("SetMetadata failed for %s: %v", key, err)
}
}
// Verify all keys
for key, expectedValue := range keys {
value, err := store.GetMetadata(ctx, key)
if err != nil {
t.Fatalf("GetMetadata failed for %s: %v", key, err)
}
if value != expectedValue {
t.Errorf("For key %s, expected '%s', got '%s'", key, expectedValue, value)
}
}
}