Skip to content

fix(configure): preserve existing config instead of overwriting it - #72

Merged
aaearon merged 1 commit into
mainfrom
fix/configure-preserves-config
Aug 17, 2026
Merged

aaearon merged 1 commit into
mainfrom
fix/configure-preserves-config

Conversation

@aaearon

@aaearon aaearon commented Aug 17, 2026

Copy link
Copy Markdown
Owner

The bug

runConfigure (cmd/configure.go) never read the existing config file. It constructed a fresh &config.Config{} with a hardcoded default_provider: azure and an empty Favorites map and Saved that over the top.

User impact

Every re-run of grant configure — including the auto-configure branch in grant login — silently destroyed:

  • every saved favorite (grant favorites add work, gone with no prompt and no message)
  • a non-default default_provider
  • a configured cache_ttl

The fix

runConfigure now resolves the config path first, calls config.Load, and merges onto a successful load, overwriting only the field it owns (profile). Everything else in the file is carried through untouched.

config.Load returns DefaultConfig(), nil for os.ErrNotExist, so a first run merges onto defaults rather than taking the fallback — asserted by a test rather than assumed. Load never returns a partial config (every failure path returns nil, err), so there is no half-loaded state to reason about.

The no-lockout property, and how it is preserved

The clobber was also what kept grant configure usable when the on-disk config could not be loaded (e.g. cache_ttl: garbage) — the only command that can repair such a file must not itself fail on it. That is preserved exactly: when config.Load returns an error, configure falls back to config.DefaultConfig() and writes it, as before. Making configure fail on an unloadable config would lock the user out; it does not.

Two changes on that path:

  • it now prints a warning to stderr naming the config path and the load error, so the (unavoidable, since the file cannot be read) loss of favorites is visible rather than silent;
  • it is still not the advertised remedy for a bad config value. The user-facing remedy remains editing the file, whose path every load error names. CLAUDE.md keeps that guidance, and the cache_ttl error messages continue not to point at configure.

Existing test renamed — deliberately, not silently

TestConfigure_OverwritesInvalidCacheTTLAndClobbersFavoritesTestConfigure_UnloadableConfigIsRebuiltFromDefaults.

Its fixture is an unloadable config, so every one of its assertions still holds: configure succeeds, cache_ttl is rewritten away, favorites and default_provider are lost. Its intent — pin no-lockout, and keep the collateral loss visible so nobody mistakes configure for a safe repair tool — is unchanged and now also asserts the stderr warning. Only the name changed, because "ClobbersFavorites" is no longer true in general: a loadable config is preserved. The doc comment now cross-references the preservation test.

No other test depended on the clobber (rg over the repo: only this one).

Tests added

  • TestConfigure_PreservesExistingConfigOnRerun — loadable config: favorites, default_provider: aws and cache_ttl: 30m all survive.
  • TestConfigure_FirstRunMergesOntoDefaults — no config file: succeeds onto defaults, empty favorites, no rebuild warning.
  • TestConfigure_UnloadableConfigIsRebuiltFromDefaults — renamed above, plus the stderr-warning assertions.

Written first; all three failed against the old implementation on the assertions that matter.

Verification

command rc
gofmt -s -l . 0, empty output
go build ./... 0
go test -race -count=1 ./... 0
go test -tags=integration -count=1 ./cmd 0
go test -shuffle=on -count=1 ./cmd 0
golangci-lint run 0

Docs

  • CHANGELOG.md: one ### Fixed line under [Unreleased].
  • CLAUDE.md: the Config section's "known sharp edge / deliberately not fixed" passage is rewritten — the clobber half is fixed; the no-lockout property and the "never advertise configure as the remedy" guidance are retained and re-justified for the fallback path.
  • docs/mutation-ledger.md: CFG-02's parenthetical about configure never calling Load is marked superseded.

runConfigure never read the existing config: it built a fresh Config with a
hardcoded default_provider and an empty Favorites map and saved that, so every
re-run of `grant configure` silently destroyed the user's favorites,
default_provider and cache_ttl.

It now loads the existing config and merges onto a successful load, overwriting
only the fields it owns. The rebuild-from-defaults path is kept for the case
config.Load fails, which is what keeps configure reachable when the on-disk
config is unloadable (no-lockout), and it now warns on stderr instead of
discarding the file silently.
@aaearon
aaearon merged commit ad68da2 into main Aug 17, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant