From f1dd1e4446e299b31220b4cd6060a86924e304af Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Fri, 31 Oct 2025 18:01:36 -0700 Subject: [PATCH] test: improve internal/importer coverage from 33.5% to 70.5% - Add comprehensive tests for ImportIssues main function - Test basic import, updates, dry-run, dependencies, labels - Test helper functions: getOrCreateStore, GetPrefixList - All tests passing --- internal/importer/importer_test.go | 340 +++++++++++++++++++++++++++++ 1 file changed, 340 insertions(+) diff --git a/internal/importer/importer_test.go b/internal/importer/importer_test.go index 52bab261..14674373 100644 --- a/internal/importer/importer_test.go +++ b/internal/importer/importer_test.go @@ -1,9 +1,11 @@ package importer import ( + "context" "testing" "time" + "github.com/steveyegge/beads/internal/storage/sqlite" "github.com/steveyegge/beads/internal/types" ) @@ -540,3 +542,341 @@ func TestIsNumeric(t *testing.T) { func stringPtr(s string) *string { return &s } + +func TestImportIssues_Basic(t *testing.T) { + ctx := context.Background() + + // Create temp database + tmpDB := t.TempDir() + "/test.db" + store, err := sqlite.New(tmpDB) + if err != nil { + t.Fatalf("Failed to create store: %v", err) + } + defer store.Close() + + // Set config prefix + if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil { + t.Fatalf("Failed to set prefix: %v", err) + } + + // Import single issue + issues := []*types.Issue{ + { + ID: "test-abc123", + Title: "Test Issue", + Description: "Test description", + Status: types.StatusOpen, + Priority: 1, + IssueType: types.TypeTask, + }, + } + + result, err := ImportIssues(ctx, tmpDB, store, issues, Options{}) + if err != nil { + t.Fatalf("Import failed: %v", err) + } + + if result.Created != 1 { + t.Errorf("Expected 1 created, got %d", result.Created) + } + + // Verify issue was created + retrieved, err := store.GetIssue(ctx, "test-abc123") + if err != nil { + t.Fatalf("Failed to retrieve issue: %v", err) + } + if retrieved.Title != "Test Issue" { + t.Errorf("Expected title 'Test Issue', got '%s'", retrieved.Title) + } +} + +func TestImportIssues_Update(t *testing.T) { + ctx := context.Background() + + tmpDB := t.TempDir() + "/test.db" + store, err := sqlite.New(tmpDB) + if err != nil { + t.Fatalf("Failed to create store: %v", err) + } + defer store.Close() + + if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil { + t.Fatalf("Failed to set prefix: %v", err) + } + + // Create initial issue + issue1 := &types.Issue{ + ID: "test-abc123", + Title: "Original Title", + Description: "Original description", + Status: types.StatusOpen, + Priority: 1, + IssueType: types.TypeTask, + } + issue1.ContentHash = issue1.ComputeContentHash() + + err = store.CreateIssue(ctx, issue1, "test") + if err != nil { + t.Fatalf("Failed to create initial issue: %v", err) + } + + // Import updated version + issue2 := &types.Issue{ + ID: "test-abc123", + Title: "Updated Title", + Description: "Updated description", + Status: types.StatusInProgress, + Priority: 2, + IssueType: types.TypeTask, + } + + result, err := ImportIssues(ctx, tmpDB, store, []*types.Issue{issue2}, Options{}) + if err != nil { + t.Fatalf("Import failed: %v", err) + } + + // The importer detects this as both a collision (1) and then upserts it (creates=1) + // Total updates = collision count + actual upserts + if result.Updated == 0 && result.Created == 0 { + t.Error("Expected some updates or creates") + } + + // Verify update + retrieved, err := store.GetIssue(ctx, "test-abc123") + if err != nil { + t.Fatalf("Failed to retrieve issue: %v", err) + } + if retrieved.Title != "Updated Title" { + t.Errorf("Expected title 'Updated Title', got '%s'", retrieved.Title) + } +} + +func TestImportIssues_DryRun(t *testing.T) { + ctx := context.Background() + + tmpDB := t.TempDir() + "/test.db" + store, err := sqlite.New(tmpDB) + if err != nil { + t.Fatalf("Failed to create store: %v", err) + } + defer store.Close() + + if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil { + t.Fatalf("Failed to set prefix: %v", err) + } + + issues := []*types.Issue{ + { + ID: "test-abc123", + Title: "Test Issue", + Status: types.StatusOpen, + Priority: 1, + IssueType: types.TypeTask, + }, + } + + // Dry run returns early when no collisions, so it reports what would be created + result, err := ImportIssues(ctx, tmpDB, store, issues, Options{DryRun: true}) + if err != nil { + t.Fatalf("Import failed: %v", err) + } + + // Should report that 1 issue would be created + if result.Created != 1 { + t.Errorf("Expected 1 would be created in dry run, got %d", result.Created) + } +} + +func TestImportIssues_Dependencies(t *testing.T) { + ctx := context.Background() + + tmpDB := t.TempDir() + "/test.db" + store, err := sqlite.New(tmpDB) + if err != nil { + t.Fatalf("Failed to create store: %v", err) + } + defer store.Close() + + if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil { + t.Fatalf("Failed to set prefix: %v", err) + } + + issues := []*types.Issue{ + { + ID: "test-abc123", + Title: "Issue 1", + Status: types.StatusOpen, + Priority: 1, + IssueType: types.TypeTask, + Dependencies: []*types.Dependency{ + {IssueID: "test-abc123", DependsOnID: "test-def456", Type: types.DepBlocks}, + }, + }, + { + ID: "test-def456", + Title: "Issue 2", + Status: types.StatusOpen, + Priority: 1, + IssueType: types.TypeTask, + }, + } + + result, err := ImportIssues(ctx, tmpDB, store, issues, Options{}) + if err != nil { + t.Fatalf("Import failed: %v", err) + } + + if result.Created != 2 { + t.Errorf("Expected 2 created, got %d", result.Created) + } + + // Verify dependency was created + deps, err := store.GetDependencies(ctx, "test-abc123") + if err != nil { + t.Fatalf("Failed to get dependencies: %v", err) + } + if len(deps) != 1 { + t.Errorf("Expected 1 dependency, got %d", len(deps)) + } +} + +func TestImportIssues_Labels(t *testing.T) { + ctx := context.Background() + + tmpDB := t.TempDir() + "/test.db" + store, err := sqlite.New(tmpDB) + if err != nil { + t.Fatalf("Failed to create store: %v", err) + } + defer store.Close() + + if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil { + t.Fatalf("Failed to set prefix: %v", err) + } + + issues := []*types.Issue{ + { + ID: "test-abc123", + Title: "Test Issue", + Status: types.StatusOpen, + Priority: 1, + IssueType: types.TypeTask, + Labels: []string{"bug", "critical"}, + }, + } + + result, err := ImportIssues(ctx, tmpDB, store, issues, Options{}) + if err != nil { + t.Fatalf("Import failed: %v", err) + } + + if result.Created != 1 { + t.Errorf("Expected 1 created, got %d", result.Created) + } + + // Verify labels were created + retrieved, err := store.GetIssue(ctx, "test-abc123") + if err != nil { + t.Fatalf("Failed to retrieve issue: %v", err) + } + if len(retrieved.Labels) != 2 { + t.Errorf("Expected 2 labels, got %d", len(retrieved.Labels)) + } +} + +func TestGetOrCreateStore_ExistingStore(t *testing.T) { + ctx := context.Background() + + tmpDB := t.TempDir() + "/test.db" + store, err := sqlite.New(tmpDB) + if err != nil { + t.Fatalf("Failed to create store: %v", err) + } + defer store.Close() + + result, needClose, err := getOrCreateStore(ctx, tmpDB, store) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + if needClose { + t.Error("Expected needClose=false for existing store") + } + if result != store { + t.Error("Expected same store instance") + } +} + +func TestGetOrCreateStore_NewStore(t *testing.T) { + ctx := context.Background() + + tmpDB := t.TempDir() + "/test.db" + + // Create initial database + initStore, err := sqlite.New(tmpDB) + if err != nil { + t.Fatalf("Failed to create store: %v", err) + } + initStore.Close() + + // Test creating new connection + result, needClose, err := getOrCreateStore(ctx, tmpDB, nil) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + defer result.Close() + + if !needClose { + t.Error("Expected needClose=true for new store") + } + if result == nil { + t.Error("Expected non-nil store") + } +} + +func TestGetOrCreateStore_EmptyPath(t *testing.T) { + ctx := context.Background() + + _, _, err := getOrCreateStore(ctx, "", nil) + if err == nil { + t.Error("Expected error for empty database path") + } +} + +func TestGetPrefixList(t *testing.T) { + tests := []struct { + name string + prefixes map[string]int + want []string + }{ + { + name: "single prefix", + prefixes: map[string]int{"test": 5}, + want: []string{"test- (5 issues)"}, + }, + { + name: "multiple prefixes", + prefixes: map[string]int{"test": 3, "other": 2, "foo": 1}, + want: []string{"foo- (1 issues)", "other- (2 issues)", "test- (3 issues)"}, + }, + { + name: "empty", + prefixes: map[string]int{}, + want: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := GetPrefixList(tt.prefixes) + if len(got) != len(tt.want) { + t.Errorf("Length mismatch: got %d, want %d", len(got), len(tt.want)) + return + } + for i := range got { + if got[i] != tt.want[i] { + t.Errorf("Index %d: got %q, want %q", i, got[i], tt.want[i]) + } + } + }) + } +}