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