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