v0.33.2 57f21d6

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		{"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}

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