Improve test coverage for bd-136
Added tests for internal/rpc and internal/storage/sqlite: RPC tests (+5.8% coverage: 58.0% → 63.8%): - TestCloseIssue: Cover handleClose (was 0%) - TestReposStats: Cover handleReposStats (was 0%) - TestReposClearCache: Cover handleReposClearCache (was 0%) - TestEpicStatus: Cover handleEpicStatus (was 0%) Storage tests (+2.6% coverage: 62.2% → 64.8%): - Created epics_test.go with TestGetEpicsEligibleForClosure - TestUpdateIssueValidation: validateIssueType, validateEstimatedMinutes - TestGetAllConfig, TestDeleteConfig, TestIsClosed Overall coverage: 48.7% → 50.7% (+2.0%) Progress on bd-136: Achieve 75% test coverage across codebase Amp-Thread-ID: https://ampcode.com/threads/T-16b56923-6fbc-45db-b68b-315567849ec6 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
214
internal/storage/sqlite/epics_test.go
Normal file
214
internal/storage/sqlite/epics_test.go
Normal file
@@ -0,0 +1,214 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
func TestGetEpicsEligibleForClosure(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create an epic
|
||||
epic := &types.Issue{
|
||||
Title: "Test Epic",
|
||||
Description: "Epic for testing",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeEpic,
|
||||
}
|
||||
err := store.CreateIssue(ctx, epic, "test-user")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateIssue (epic) failed: %v", err)
|
||||
}
|
||||
|
||||
// Create two child tasks
|
||||
task1 := &types.Issue{
|
||||
Title: "Task 1",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: types.TypeTask,
|
||||
}
|
||||
err = store.CreateIssue(ctx, task1, "test-user")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateIssue (task1) failed: %v", err)
|
||||
}
|
||||
|
||||
task2 := &types.Issue{
|
||||
Title: "Task 2",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: types.TypeTask,
|
||||
}
|
||||
err = store.CreateIssue(ctx, task2, "test-user")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateIssue (task2) failed: %v", err)
|
||||
}
|
||||
|
||||
// Add parent-child dependencies
|
||||
dep1 := &types.Dependency{
|
||||
IssueID: task1.ID,
|
||||
DependsOnID: epic.ID,
|
||||
Type: types.DepParentChild,
|
||||
}
|
||||
err = store.AddDependency(ctx, dep1, "test-user")
|
||||
if err != nil {
|
||||
t.Fatalf("AddDependency (task1) failed: %v", err)
|
||||
}
|
||||
|
||||
dep2 := &types.Dependency{
|
||||
IssueID: task2.ID,
|
||||
DependsOnID: epic.ID,
|
||||
Type: types.DepParentChild,
|
||||
}
|
||||
err = store.AddDependency(ctx, dep2, "test-user")
|
||||
if err != nil {
|
||||
t.Fatalf("AddDependency (task2) failed: %v", err)
|
||||
}
|
||||
|
||||
// Test 1: Epic with open children should NOT be eligible for closure
|
||||
epics, err := store.GetEpicsEligibleForClosure(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetEpicsEligibleForClosure failed: %v", err)
|
||||
}
|
||||
|
||||
if len(epics) == 0 {
|
||||
t.Fatal("Expected at least one epic")
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, e := range epics {
|
||||
if e.Epic.ID == epic.ID {
|
||||
found = true
|
||||
if e.TotalChildren != 2 {
|
||||
t.Errorf("Expected 2 total children, got %d", e.TotalChildren)
|
||||
}
|
||||
if e.ClosedChildren != 0 {
|
||||
t.Errorf("Expected 0 closed children, got %d", e.ClosedChildren)
|
||||
}
|
||||
if e.EligibleForClose {
|
||||
t.Error("Epic should NOT be eligible for closure with open children")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Epic not found in results")
|
||||
}
|
||||
|
||||
// Test 2: Close one task
|
||||
err = store.CloseIssue(ctx, task1.ID, "Done", "test-user")
|
||||
if err != nil {
|
||||
t.Fatalf("CloseIssue (task1) failed: %v", err)
|
||||
}
|
||||
|
||||
epics, err = store.GetEpicsEligibleForClosure(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetEpicsEligibleForClosure (after closing task1) failed: %v", err)
|
||||
}
|
||||
|
||||
found = false
|
||||
for _, e := range epics {
|
||||
if e.Epic.ID == epic.ID {
|
||||
found = true
|
||||
if e.ClosedChildren != 1 {
|
||||
t.Errorf("Expected 1 closed child, got %d", e.ClosedChildren)
|
||||
}
|
||||
if e.EligibleForClose {
|
||||
t.Error("Epic should NOT be eligible with only 1/2 tasks closed")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Epic not found after closing one task")
|
||||
}
|
||||
|
||||
// Test 3: Close second task - epic should be eligible
|
||||
err = store.CloseIssue(ctx, task2.ID, "Done", "test-user")
|
||||
if err != nil {
|
||||
t.Fatalf("CloseIssue (task2) failed: %v", err)
|
||||
}
|
||||
|
||||
epics, err = store.GetEpicsEligibleForClosure(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetEpicsEligibleForClosure (after closing task2) failed: %v", err)
|
||||
}
|
||||
|
||||
found = false
|
||||
for _, e := range epics {
|
||||
if e.Epic.ID == epic.ID {
|
||||
found = true
|
||||
if e.ClosedChildren != 2 {
|
||||
t.Errorf("Expected 2 closed children, got %d", e.ClosedChildren)
|
||||
}
|
||||
if !e.EligibleForClose {
|
||||
t.Error("Epic SHOULD be eligible for closure with all children closed")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Epic not found after closing all tasks")
|
||||
}
|
||||
|
||||
// Test 4: Close the epic - should no longer appear in results
|
||||
err = store.CloseIssue(ctx, epic.ID, "All tasks complete", "test-user")
|
||||
if err != nil {
|
||||
t.Fatalf("CloseIssue (epic) failed: %v", err)
|
||||
}
|
||||
|
||||
epics, err = store.GetEpicsEligibleForClosure(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetEpicsEligibleForClosure (after closing epic) failed: %v", err)
|
||||
}
|
||||
|
||||
// Closed epics should not appear in results
|
||||
for _, e := range epics {
|
||||
if e.Epic.ID == epic.ID {
|
||||
t.Error("Closed epic should not appear in eligible list")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetEpicsEligibleForClosureWithNoChildren(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Create an epic with no children
|
||||
epic := &types.Issue{
|
||||
Title: "Childless Epic",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeEpic,
|
||||
}
|
||||
err := store.CreateIssue(ctx, epic, "test-user")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateIssue failed: %v", err)
|
||||
}
|
||||
|
||||
epics, err := store.GetEpicsEligibleForClosure(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetEpicsEligibleForClosure failed: %v", err)
|
||||
}
|
||||
|
||||
// Should find the epic but it should NOT be eligible (no children = not eligible)
|
||||
found := false
|
||||
for _, e := range epics {
|
||||
if e.Epic.ID == epic.ID {
|
||||
found = true
|
||||
if e.TotalChildren != 0 {
|
||||
t.Errorf("Expected 0 total children, got %d", e.TotalChildren)
|
||||
}
|
||||
if e.EligibleForClose {
|
||||
t.Error("Epic with no children should NOT be eligible for closure")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("Epic not found in results")
|
||||
}
|
||||
}
|
||||
@@ -671,6 +671,60 @@ func TestUpdateIssue(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateIssueValidation(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
issue := &types.Issue{
|
||||
Title: "Test Issue",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: types.TypeTask,
|
||||
}
|
||||
|
||||
err := store.CreateIssue(ctx, issue, "test-user")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateIssue failed: %v", err)
|
||||
}
|
||||
|
||||
// Test invalid issue type
|
||||
updates := map[string]interface{}{
|
||||
"issue_type": "invalid-type",
|
||||
}
|
||||
err = store.UpdateIssue(ctx, issue.ID, updates, "test-user")
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid issue_type, got nil")
|
||||
}
|
||||
|
||||
// Test negative estimated_minutes
|
||||
updates = map[string]interface{}{
|
||||
"estimated_minutes": -10,
|
||||
}
|
||||
err = store.UpdateIssue(ctx, issue.ID, updates, "test-user")
|
||||
if err == nil {
|
||||
t.Error("Expected error for negative estimated_minutes, got nil")
|
||||
}
|
||||
|
||||
// Test valid issue type
|
||||
updates = map[string]interface{}{
|
||||
"issue_type": string(types.TypeBug),
|
||||
}
|
||||
err = store.UpdateIssue(ctx, issue.ID, updates, "test-user")
|
||||
if err != nil {
|
||||
t.Errorf("Valid issue_type should not error: %v", err)
|
||||
}
|
||||
|
||||
// Test valid estimated_minutes
|
||||
updates = map[string]interface{}{
|
||||
"estimated_minutes": 60,
|
||||
}
|
||||
err = store.UpdateIssue(ctx, issue.ID, updates, "test-user")
|
||||
if err != nil {
|
||||
t.Errorf("Valid estimated_minutes should not error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloseIssue(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
@@ -1409,3 +1463,97 @@ func TestInMemorySharedCache(t *testing.T) {
|
||||
t.Errorf("Title mismatch: got %v, want %v", retrieved2.Title, issue2.Title)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAllConfig(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Set multiple config values
|
||||
err := store.SetConfig(ctx, "key1", "value1")
|
||||
if err != nil {
|
||||
t.Fatalf("SetConfig key1 failed: %v", err)
|
||||
}
|
||||
|
||||
err = store.SetConfig(ctx, "key2", "value2")
|
||||
if err != nil {
|
||||
t.Fatalf("SetConfig key2 failed: %v", err)
|
||||
}
|
||||
|
||||
// Get all config
|
||||
allConfig, err := store.GetAllConfig(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("GetAllConfig failed: %v", err)
|
||||
}
|
||||
|
||||
if len(allConfig) < 2 {
|
||||
t.Errorf("Expected at least 2 config entries, got %d", len(allConfig))
|
||||
}
|
||||
|
||||
if allConfig["key1"] != "value1" {
|
||||
t.Errorf("Expected key1=value1, got %s", allConfig["key1"])
|
||||
}
|
||||
|
||||
if allConfig["key2"] != "value2" {
|
||||
t.Errorf("Expected key2=value2, got %s", allConfig["key2"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteConfig(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Set a config value
|
||||
err := store.SetConfig(ctx, "test-key", "test-value")
|
||||
if err != nil {
|
||||
t.Fatalf("SetConfig failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify it exists
|
||||
value, err := store.GetConfig(ctx, "test-key")
|
||||
if err != nil {
|
||||
t.Fatalf("GetConfig failed: %v", err)
|
||||
}
|
||||
if value != "test-value" {
|
||||
t.Errorf("Expected test-value, got %s", value)
|
||||
}
|
||||
|
||||
// Delete it
|
||||
err = store.DeleteConfig(ctx, "test-key")
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteConfig failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify it's gone
|
||||
value, err = store.GetConfig(ctx, "test-key")
|
||||
if err != nil {
|
||||
t.Fatalf("GetConfig failed: %v", err)
|
||||
}
|
||||
if value != "" {
|
||||
t.Errorf("Expected empty value after deletion, got: %s", value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsClosed(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Store should not be closed initially
|
||||
if store.IsClosed() {
|
||||
t.Error("Store should not be closed initially")
|
||||
}
|
||||
|
||||
// Close the store
|
||||
err := store.Close()
|
||||
if err != nil {
|
||||
t.Fatalf("Close failed: %v", err)
|
||||
}
|
||||
|
||||
// Store should be closed now
|
||||
if !store.IsClosed() {
|
||||
t.Error("Store should be closed after calling Close()")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user