1package config
2
3import (
4 "os"
5 "path/filepath"
6 "regexp"
7 "runtime"
8 "sort"
9 "strings"
10 "testing"
11
12 "github.com/bnema/dumber/internal/domain/entity"
13 "github.com/spf13/viper"
14 "github.com/stretchr/testify/require"
15)
16
17func TestModeLegendSchemaDefaults(t *testing.T) {
18 keys := make(map[string]entity.ConfigKeyInfo)
19 for _, key := range NewSchemaProvider().GetSchema() {
20 keys[key.Key] = key
21 }
22 for _, tc := range []struct{ key, kind, value, valueRange string }{
23 {"workspace.styling.mode_legend", "string", "delay", ""},
24 {"workspace.styling.mode_legend_delay_ms", "int", "500", ">=0"},
25 {"workspace.styling.mode_legend_linger", "bool", "true", ""},
26 {"workspace.styling.vim_mode_color", "string", "#F7768E", ""},
27 {"workspace.styling.mode_legend_animations", "bool", "true", ""},
28 {"workspace.styling.mode_legend_collapsed", "bool", "false", ""},
29 } {
30 entry, ok := keys[tc.key]
31 require.True(t, ok, "missing schema key %s", tc.key)
32 require.Equal(t, tc.kind, entry.Type)
33 require.Equal(t, tc.value, entry.Default)
34 require.Equal(t, tc.valueRange, entry.Range)
35 }
36 require.Equal(t, []string{"always", "delay", "off"}, keys["workspace.styling.mode_legend"].Values)
37}
38
39func TestConfigurationReferenceCoversSchemaKeys(t *testing.T) {
40 docKeys := configurationReferenceKeys(t)
41
42 var missing []string
43 for _, key := range NewSchemaProvider().GetSchema() {
44 if configurationReferenceCoversKey(docKeys, key.Key) {
45 continue
46 }
47 missing = append(missing, key.Key)
48 }
49
50 sort.Strings(missing)
51 require.Empty(t, missing, "docs/reference/configuration.md should cover every SchemaProvider key")
52}
53
54func configurationReferenceKeys(t *testing.T) map[string]struct{} {
55 t.Helper()
56
57 _, filename, _, ok := runtime.Caller(0)
58 require.True(t, ok, "locate current test file")
59 docPath := filepath.Join(filepath.Dir(filename), "..", "..", "..", "docs", "reference", "configuration.md")
60 data, err := os.ReadFile(docPath)
61 require.NoError(t, err)
62
63 // Keep this split point aligned with the markdown heading so legacy-key rows
64 // stay out of the canonical schema key coverage set.
65 canonicalReference, _, _ := strings.Cut(string(data), "Legacy key migration:")
66 rowRE := regexp.MustCompile(`(?m)^\| ` + "`" + `([^` + "`" + `]+)` + "`" + ` \|`)
67 keys := make(map[string]struct{})
68 for _, match := range rowRE.FindAllStringSubmatch(canonicalReference, -1) {
69 keys[match[1]] = struct{}{}
70 }
71 return keys
72}
73
74func configurationReferenceCoversKey(docKeys map[string]struct{}, schemaKey string) bool {
75 if _, ok := docKeys[schemaKey]; ok {
76 return true
77 }
78
79 schemaSegments := strings.Split(schemaKey, ".")
80 for docKey := range docKeys {
81 if configurationReferencePatternCoversKey(strings.Split(docKey, "."), schemaSegments) {
82 return true
83 }
84 }
85
86 return false
87}
88
89func configurationReferencePatternCoversKey(docSegments, schemaSegments []string) bool {
90 for idx, schemaSegment := range schemaSegments {
91 if schemaSegment == "*" {
92 return idx == len(schemaSegments)-1 && len(docSegments) == idx+1
93 }
94 if idx >= len(docSegments) {
95 return false
96 }
97 if isPlaceholderSegment(schemaSegment) && isPlaceholderSegment(docSegments[idx]) {
98 continue
99 }
100 if docSegments[idx] != schemaSegment {
101 return false
102 }
103 }
104
105 if len(docSegments) == len(schemaSegments) {
106 return true
107 }
108 return configurationReferenceAllowsObjectFields(strings.Join(schemaSegments, ".")) &&
109 len(docSegments) > len(schemaSegments)
110}
111
112func isPlaceholderSegment(segment string) bool {
113 return strings.HasPrefix(segment, "<") && strings.HasSuffix(segment, ">")
114}
115
116func configurationReferenceAllowsObjectFields(schemaKey string) bool {
117 switch schemaKey {
118 case "search_shortcuts.<key>", "workspace.floating_pane.profiles.<name>":
119 return true
120 default:
121 return false
122 }
123}
124
125func TestConfigurationReferenceCoversKeyRequiresPlaceholderSegmentMatches(t *testing.T) {
126 docKeys := map[string]struct{}{
127 "workspace.floating_pane.profiles.<profile>.keys_extra": {},
128 }
129
130 require.False(t, configurationReferenceCoversKey(
131 docKeys,
132 "workspace.floating_pane.profiles.<name>.keys",
133 ))
134}
135
136func TestConfigurationReferenceCoversKeyAllowsDocumentedPlaceholderSegments(t *testing.T) {
137 docKeys := map[string]struct{}{
138 "workspace.floating_pane.profiles.<profile>.keys": {},
139 }
140
141 require.True(t, configurationReferenceCoversKey(
142 docKeys,
143 "workspace.floating_pane.profiles.<name>.keys",
144 ))
145}
146
147func TestConfigurationReferenceCoversKeyAllowsDocumentedObjectFields(t *testing.T) {
148 docKeys := map[string]struct{}{
149 "search_shortcuts.<name>.url": {},
150 }
151
152 require.True(t, configurationReferenceCoversKey(docKeys, "search_shortcuts.<key>"))
153}
154
155func TestSchemaProviderCoversViperDefaults(t *testing.T) {
156 m := &Manager{viper: viper.New()}
157 m.setDefaults()
158
159 schemaKeys := make(map[string]struct{})
160 for _, key := range NewSchemaProvider().GetSchema() {
161 require.NotEmpty(t, key.Key, "schema key should not be empty")
162 require.NotContains(t, schemaKeys, key.Key, "duplicate schema key %s", key.Key)
163 schemaKeys[key.Key] = struct{}{}
164 }
165
166 defaultsCoveredByAggregateSchemaKeys := map[string]string{
167 "appearance.light_palette": "appearance.light_palette.*",
168 "appearance.dark_palette": "appearance.dark_palette.*",
169 "search_shortcuts": "search_shortcuts.<key>",
170 "workspace.prefix_mode.actions": "workspace.prefix_mode.actions.<action>",
171 "workspace.pane_mode.actions": "workspace.pane_mode.actions.<action>",
172 "workspace.tab_mode.actions": "workspace.tab_mode.actions.<action>",
173 "workspace.vim_mode.actions": "workspace.vim_mode.actions.<action>",
174 "workspace.resize_mode.actions": "workspace.resize_mode.actions.<action>",
175 "workspace.floating_pane.profiles": "workspace.floating_pane.profiles.<name>",
176 "session.session_mode.actions": "session.session_mode.actions.<action>",
177 }
178
179 var missing []string
180 for _, key := range m.viper.AllKeys() {
181 if _, ok := schemaKeys[key]; ok {
182 continue
183 }
184 if schemaKey, ok := defaultsCoveredByAggregateSchemaKeys[key]; ok {
185 if _, hasAggregate := schemaKeys[schemaKey]; hasAggregate {
186 continue
187 }
188 }
189 missing = append(missing, key)
190 }
191
192 sort.Strings(missing)
193 require.Empty(t, missing, "every Viper default should have SchemaProvider metadata or an explicit aggregate schema key")
194}