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:
Erick Matsen
2026-01-10 13:36:52 -08:00
committed by GitHub
parent 69dae103db
commit 3f2b693bea
9 changed files with 347 additions and 43 deletions

View File

@@ -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
}

View File

@@ -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)
}
}
})
}
}