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:
@@ -90,3 +90,25 @@ func ParseHierarchicalID(id string) (rootID, parentID string, depth int) {
|
||||
// MaxHierarchyDepth is the maximum nesting level for hierarchical IDs.
|
||||
// Prevents over-decomposition and keeps IDs manageable.
|
||||
const MaxHierarchyDepth = 3
|
||||
|
||||
// CheckHierarchyDepth validates that adding a child to parentID won't exceed maxDepth.
|
||||
// Returns an error if the depth would be exceeded.
|
||||
// If maxDepth < 1, it defaults to MaxHierarchyDepth.
|
||||
func CheckHierarchyDepth(parentID string, maxDepth int) error {
|
||||
if maxDepth < 1 {
|
||||
maxDepth = MaxHierarchyDepth
|
||||
}
|
||||
|
||||
// Count dots to determine current depth
|
||||
depth := 0
|
||||
for _, ch := range parentID {
|
||||
if ch == '.' {
|
||||
depth++
|
||||
}
|
||||
}
|
||||
|
||||
if depth >= maxDepth {
|
||||
return fmt.Errorf("maximum hierarchy depth (%d) exceeded for parent %s", maxDepth, parentID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -209,3 +209,46 @@ func BenchmarkGenerateChildID(b *testing.B) {
|
||||
GenerateChildID("bd-af78e9a2", 42)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckHierarchyDepth(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
parentID string
|
||||
maxDepth int
|
||||
wantErr bool
|
||||
errMsg string
|
||||
}{
|
||||
// Default maxDepth (uses MaxHierarchyDepth = 3)
|
||||
{"root parent with default depth", "bd-abc123", 0, false, ""},
|
||||
{"depth 1 parent with default depth", "bd-abc123.1", 0, false, ""},
|
||||
{"depth 2 parent with default depth", "bd-abc123.1.2", 0, false, ""},
|
||||
{"depth 3 parent with default depth - exceeds", "bd-abc123.1.2.3", 0, true, "maximum hierarchy depth (3) exceeded for parent bd-abc123.1.2.3"},
|
||||
|
||||
// Custom maxDepth
|
||||
{"root parent with max=1", "bd-abc123", 1, false, ""},
|
||||
{"depth 1 parent with max=1 - exceeds", "bd-abc123.1", 1, true, "maximum hierarchy depth (1) exceeded for parent bd-abc123.1"},
|
||||
{"depth 3 parent with max=5", "bd-abc123.1.2.3", 5, false, ""},
|
||||
{"depth 4 parent with max=5", "bd-abc123.1.2.3.4", 5, false, ""},
|
||||
{"depth 5 parent with max=5 - exceeds", "bd-abc123.1.2.3.4.5", 5, true, "maximum hierarchy depth (5) exceeded for parent bd-abc123.1.2.3.4.5"},
|
||||
|
||||
// Negative maxDepth falls back to default
|
||||
{"negative maxDepth uses default", "bd-abc123.1.2.3", -1, true, "maximum hierarchy depth (3) exceeded for parent bd-abc123.1.2.3"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := CheckHierarchyDepth(tt.parentID, tt.maxDepth)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Errorf("expected error, got nil")
|
||||
} else if err.Error() != tt.errMsg {
|
||||
t.Errorf("expected error %q, got %q", tt.errMsg, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user