schema_provider_test.go RawHistoryFolder

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.prefix_mode.actions":    "workspace.prefix_mode.actions.<action>",
170		"workspace.pane_mode.actions":      "workspace.pane_mode.actions.<action>",
171		"workspace.tab_mode.actions":       "workspace.tab_mode.actions.<action>",
172		"workspace.vim_mode.actions":       "workspace.vim_mode.actions.<action>",
173		"workspace.resize_mode.actions":    "workspace.resize_mode.actions.<action>",
174		"workspace.floating_pane.profiles": "workspace.floating_pane.profiles.<name>",
175		"session.session_mode.actions":     "session.session_mode.actions.<action>",
176	}
177
178	var missing []string
179	for _, key := range m.viper.AllKeys() {
180		if _, ok := schemaKeys[key]; ok {
181			continue
182		}
183		if schemaKey, ok := defaultsCoveredByAggregateSchemaKeys[key]; ok {
184			if _, hasAggregate := schemaKeys[schemaKey]; hasAggregate {
185				continue
186			}
187		}
188		missing = append(missing, key)
189	}
190
191	sort.Strings(missing)
192	require.Empty(t, missing, "every Viper default should have SchemaProvider metadata or an explicit aggregate schema key")
193}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close