fix: respect hierarchy.max-depth config setting (GH#995) (#997)
* fix: respect hierarchy.max-depth config setting (GH#995) The hierarchy.max-depth config setting was being ignored because storage implementations had the depth limit hardcoded to 3. This fix: - Registers hierarchy.max-depth default (3) in config initialization - Adds hierarchy.max-depth to yaml-only keys for config.yaml storage - Updates SQLite and Memory storage to read max depth from config - Adds validation to reject hierarchy.max-depth values < 1 - Adds tests for configurable hierarchy depth Users can now set deeper hierarchies: bd config set hierarchy.max-depth 10 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: extract shared CheckHierarchyDepth function (GH#995) - Extract duplicated depth-checking logic to types.CheckHierarchyDepth() - Update sqlite and memory storage backends to use shared function - Add t.Cleanup() for proper test isolation in sqlite test - Add equivalent test coverage for memory storage backend - Add comprehensive unit tests for CheckHierarchyDepth function Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/beads/internal/config"
|
||||
"github.com/steveyegge/beads/internal/storage"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
@@ -21,14 +22,14 @@ type MemoryStorage struct {
|
||||
mu sync.RWMutex // Protects all maps
|
||||
|
||||
// Core data
|
||||
issues map[string]*types.Issue // ID -> Issue
|
||||
issues map[string]*types.Issue // ID -> Issue
|
||||
dependencies map[string][]*types.Dependency // IssueID -> Dependencies
|
||||
labels map[string][]string // IssueID -> Labels
|
||||
events map[string][]*types.Event // IssueID -> Events
|
||||
comments map[string][]*types.Comment // IssueID -> Comments
|
||||
config map[string]string // Config key-value pairs
|
||||
metadata map[string]string // Metadata key-value pairs
|
||||
counters map[string]int // Prefix -> Last ID
|
||||
labels map[string][]string // IssueID -> Labels
|
||||
events map[string][]*types.Event // IssueID -> Events
|
||||
comments map[string][]*types.Comment // IssueID -> Comments
|
||||
config map[string]string // Config key-value pairs
|
||||
metadata map[string]string // Metadata key-value pairs
|
||||
counters map[string]int // Prefix -> Last ID
|
||||
|
||||
// Indexes for O(1) lookups
|
||||
externalRefToID map[string]string // ExternalRef -> IssueID
|
||||
@@ -1598,10 +1599,9 @@ func (m *MemoryStorage) GetNextChildID(ctx context.Context, parentID string) (st
|
||||
return "", fmt.Errorf("parent issue %s does not exist", parentID)
|
||||
}
|
||||
|
||||
// Calculate depth (count dots)
|
||||
depth := strings.Count(parentID, ".")
|
||||
if depth >= 3 {
|
||||
return "", fmt.Errorf("maximum hierarchy depth (3) exceeded for parent %s", parentID)
|
||||
// Check hierarchy depth limit (GH#995)
|
||||
if err := types.CheckHierarchyDepth(parentID, config.GetInt("hierarchy.max-depth")); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Get or initialize counter for this parent
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/steveyegge/beads/internal/config"
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
)
|
||||
|
||||
@@ -1276,3 +1277,76 @@ func TestGetIssueByExternalRefLoadFromIssues(t *testing.T) {
|
||||
t.Errorf("Expected to find bd-2 by external ref jira#200")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetNextChildID_ConfigurableMaxDepth tests that hierarchy.max-depth config is respected (GH#995)
|
||||
func TestGetNextChildID_ConfigurableMaxDepth(t *testing.T) {
|
||||
// Initialize config for testing
|
||||
if err := config.Initialize(); err != nil {
|
||||
t.Fatalf("failed to initialize config: %v", err)
|
||||
}
|
||||
|
||||
// Ensure config is reset even if test fails or panics
|
||||
t.Cleanup(func() {
|
||||
config.Set("hierarchy.max-depth", 3)
|
||||
})
|
||||
|
||||
store := setupTestMemory(t)
|
||||
defer store.Close()
|
||||
ctx := context.Background()
|
||||
|
||||
// Create a chain of issues up to depth 3
|
||||
issues := []struct {
|
||||
id string
|
||||
title string
|
||||
}{
|
||||
{"bd-depth", "Root"},
|
||||
{"bd-depth.1", "Level 1"},
|
||||
{"bd-depth.1.1", "Level 2"},
|
||||
{"bd-depth.1.1.1", "Level 3"},
|
||||
}
|
||||
|
||||
for _, issue := range issues {
|
||||
iss := &types.Issue{
|
||||
ID: issue.id,
|
||||
Title: issue.title,
|
||||
Description: "Test issue",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeTask,
|
||||
}
|
||||
if err := store.CreateIssue(ctx, iss, "test"); err != nil {
|
||||
t.Fatalf("failed to create issue %s: %v", issue.id, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Test 1: With default max-depth (3), depth 4 should fail
|
||||
config.Set("hierarchy.max-depth", 3)
|
||||
_, err := store.GetNextChildID(ctx, "bd-depth.1.1.1")
|
||||
if err == nil {
|
||||
t.Errorf("expected error for depth 4 with max-depth=3, got nil")
|
||||
}
|
||||
if err != nil && err.Error() != "maximum hierarchy depth (3) exceeded for parent bd-depth.1.1.1" {
|
||||
t.Errorf("unexpected error message: %v", err)
|
||||
}
|
||||
|
||||
// Test 2: With max-depth=5, depth 4 should succeed
|
||||
config.Set("hierarchy.max-depth", 5)
|
||||
childID, err := store.GetNextChildID(ctx, "bd-depth.1.1.1")
|
||||
if err != nil {
|
||||
t.Errorf("depth 4 should be allowed with max-depth=5, got error: %v", err)
|
||||
}
|
||||
expectedID := "bd-depth.1.1.1.1"
|
||||
if childID != expectedID {
|
||||
t.Errorf("expected %s, got %s", expectedID, childID)
|
||||
}
|
||||
|
||||
// Test 3: With max-depth=2, depth 3 should fail
|
||||
config.Set("hierarchy.max-depth", 2)
|
||||
_, err = store.GetNextChildID(ctx, "bd-depth.1.1")
|
||||
if err == nil {
|
||||
t.Errorf("expected error for depth 3 with max-depth=2, got nil")
|
||||
}
|
||||
if err != nil && err.Error() != "maximum hierarchy depth (2) exceeded for parent bd-depth.1.1" {
|
||||
t.Errorf("unexpected error message: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user