From 10d21f17e8d8b61449b8a354aff784d0d9e136f3 Mon Sep 17 00:00:00 2001 From: Steve Calvert Date: Sat, 11 Apr 2026 13:36:20 -0700 Subject: [PATCH] feat: warn on unknown config keys to catch typos Adds validation in config loading that checks for unknown JSON keys and logs warnings via the config:load debug namespace. Visible with -v or GLEAN_DEBUG=config:*. --- internal/config/config.go | 27 ++++++++++++++++++++++++++ internal/config/config_test.go | 35 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/internal/config/config.go b/internal/config/config.go index 9e2a262..94fb8d5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "path/filepath" + "sort" "strings" "github.com/gleanwork/glean-cli/internal/debug" @@ -284,6 +285,28 @@ func loadFromKeyring() *Config { return cfg } +var knownConfigKeys = map[string]bool{ + "host": true, + "token": true, + "oauth_client_id": true, + "oauth_client_secret": true, +} + +func validateConfigKeys(data []byte) []string { + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return nil + } + var warnings []string + for key := range raw { + if !knownConfigKeys[key] { + warnings = append(warnings, fmt.Sprintf("unknown key %q in config.json (typo?)", key)) + } + } + sort.Strings(warnings) + return warnings +} + func loadFromFile() (*Config, error) { if ConfigPath == "" { return nil, fmt.Errorf("config path not set") @@ -299,6 +322,10 @@ func loadFromFile() (*Config, error) { } cfgLog.Log("loaded config file: %s (%d bytes)", ConfigPath, len(data)) + for _, w := range validateConfigKeys(data) { + cfgLog.Log("%s", w) + } + var cfg Config if err := json.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("error parsing config file: %w", err) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 0d9a202..877b396 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -374,6 +374,41 @@ func TestSaveHostToFile_DoesNotTouchKeyring(t *testing.T) { assert.Empty(t, cfg.GleanToken) } +func TestValidateConfigKeys(t *testing.T) { + t.Run("unknown key produces warning", func(t *testing.T) { + data := []byte(`{"host":"x.glean.com","toke":"bad"}`) + warnings := validateConfigKeys(data) + require.Len(t, warnings, 1) + assert.Contains(t, warnings[0], `unknown key "toke"`) + }) + + t.Run("all known keys produce no warnings", func(t *testing.T) { + data := []byte(`{"host":"x","token":"t","oauth_client_id":"id","oauth_client_secret":"s"}`) + warnings := validateConfigKeys(data) + assert.Empty(t, warnings) + }) + + t.Run("unknown keys do not prevent loading", func(t *testing.T) { + isolateAuthState(t) + + cfgData := []byte(`{"host":"test-be.glean.com","token":"tok","extra_field":"val"}`) + err := os.MkdirAll(filepath.Dir(ConfigPath), 0700) + require.NoError(t, err) + err = os.WriteFile(ConfigPath, cfgData, 0600) + require.NoError(t, err) + + cfg, err := loadFromFile() + require.NoError(t, err) + assert.Equal(t, "test-be.glean.com", cfg.GleanHost) + assert.Equal(t, "tok", cfg.GleanToken) + }) + + t.Run("invalid JSON returns no warnings", func(t *testing.T) { + warnings := validateConfigKeys([]byte(`not json`)) + assert.Nil(t, warnings) + }) +} + func TestLoadFromFile(t *testing.T) { _, cleanup := setupTestConfig(t) defer cleanup()