Skip to content

fix(config): make vfox config write only the key it sets - #739

Open
linghengqian wants to merge 1 commit into
version-fox:mainfrom
linghengqian:fix/config-write-only-set-key
Open

linghengqian wants to merge 1 commit into
version-fox:mainfrom
linghengqian:fix/config-write-only-set-key

Conversation

@linghengqian

Copy link
Copy Markdown
Member

Part of #737 (item 4, "vfox config should write only the key it sets").

What changes

vfox config <key> <value> loaded the merged shared and user configuration and saved all of it into the user's config.yaml; --unset wrote the default value. Now the command edits the user's file as a YAML document and touches only the key that is set or unset, keeping the other keys and the comments. config.AliasFile is generalized into config.File for it; AliasFile is now a view of the aliases key of a File, so vfox alias behaves as before and its tests pass as they were; only the test of the top-level mapping moved to File, together with the code it tests.

Checked with real binaries, built from main and from this branch. The shared config.yaml has path.mode: baseline, diagnostics.onUse: false, cache.availableHookDuration: 24h and an alias; the user is new and runs vfox config proxy.url http://localhost:7890:

main this branch
path, diagnostics and aliases in the user's file all three, with the shared values none of them
the admin changes the shared path.mode to aggressive the user still gets baseline the user gets aggressive
vfox config --unset proxy.url url: "" stays in the file the key is gone
shared baseline; config path.mode legacy, then --unset path.mode legacy stays in the file and wins path is gone, the user gets baseline again

Code

  • File.Set checks the key against the yaml names of Config and the value against the type of the setting, and writes the value as that type. The other keys and the comments stay, a section the file does not declare stays undeclared, and a replaced value keeps the comment after it. Strings that YAML would read as something else ("true", "21") are quoted.
  • File.Unset removes the key and the sections it leaves empty. That part matters: a section without a key is still a section the file declares, and it would replace the shared one. A key the file does not have is not an error, and the file is not rewritten. A whole section can be unset (vfox config --unset path), which also cleans up the copies that earlier versions wrote.
  • File.Save refuses to write a file that does not load as a Config. Before, vfox config cache.availableHookDuration 500ms wrote availableHookDuration: "", and a config.yaml that does not load stops every command, including the one that could fix it.
  • LegacyVersionFile gets the UnmarshalYAML that Path and Diagnostics have, so a section declared with one key keeps the defaults of the others. Without it vfox config legacyVersionFile.strategy latest_installed on a file that lacks the section would have switched legacy version files off: enable is true by default and false once the section exists without it. AGENTS.md says that a section like this has to do the same.
  • decodeConfig is what NewConfigWithPath did to read a file, extracted so that Save can check with the same code.

Behavior that changes with it, all of them cases that used to go wrong without a word:

  • A key that does not exist (proxy.ulr) is an error. Before, nothing was set and the whole file was rewritten.
  • A boolean that is neither true nor false (yes) is an error. Before, it became false.
  • A section (proxy), a list or a table (path.preserve, diagnostics.ignore, aliases) is refused with a message. Lists and tables can still be edited in the file, and removed with --unset; aliases has vfox alias.
  • A negative cache.availableHookDuration other than -1, and one under a second, is refused. The messages for the HTTP timeouts name the key (plugin.http.downloadTimeout: must not be negative; it said timeout for both).

Tests

  • internal/config: File against files with comments, no content, only a comment, a null document, {}, an empty or null section and the flow style; every type of setting; everything that has to be refused (the file stays as it was); Unset step by step through a file, including the sections that go with their last key; the shared-config scenario of Follow-ups of #736: plugin updates, the path.mode default and vfox config #737 (the file vfox creates on its first run, one setting changed, the shared config changed afterwards, the user's own value set and unset); Save refusing a file that would not load; and a test that walks every setting of Config, so that a new setting of a type vfox config cannot set fails it.
  • cmd/commands: with a shared config present, setting one key copies nothing else (a user with a file and a new user); --unset hands the shared value back; the errors leave the file alone. The old TestConfigSetHTTPTimeout moved to the new API with all its assertions (TestSetHTTPTimeouts), and now also checks that unsetting removes the keys.
  • config runs inside the test process now (inProcess in cli_test.go). It had a process of its own because the old configSet changed the defaults that every command shares (EmptyPath and the like): TestConfigCommandHandlesTheNewSettings fails on main's config.go once config is in inProcess. The new code does not touch them.
  • The new CLI tests fail against main's config.go. I broke the implementation ten ways (no pruning of empty sections, the comment of a replaced value dropped, the Save check removed, the flow style kept for the first key, a null section not turned into a mapping, LegacyVersionFile without its defaults, the boolean error ignored, a negative timeout accepted, unknown keys accepted, a float setting without a parser) and a test failed each time.

Docs

docs/guides/configuration.md and the zh-hans copy: what vfox config <key> <value> and --unset do to the file, that lists and tables are edited in the file, and a callout that path and diagnostics still replace the shared section as a whole once the file declares them (so vfox config path.mode baseline drops a path.preserve of the shared config for that user, and vfox config --unset path gives it back). AGENTS.md says where this lives and replaces the sentence that vfox config saves the merged config.

Not in this PR

  • Sections are still replaced as a whole. Merging path and diagnostics key by key would need to know which keys a file declared; the callout above documents the rule as it is.
  • Found while testing, and not caused by this: when the user's file lacks proxy, gitignore or legacyVersionFile, the shared values of those sections do not apply. NewConfigWithPath puts the default in the missing section, and mergeProxy, mergeGitignore and mergeLegacyVersionFile take any non-nil user section as the user's. The other sections follow the shared config (path, diagnostics and aliases by design, plugin.http per key, registry, storage and cache through the empty or default value). So --unset hands the shared value back for those, and the default for these three. I left the merge alone; it deserves its own issue.
  • Config.SaveConfig is only used by tests now. It stays, with a comment that it is not how the user's file is changed; removing it touches five unrelated test files.
  • Setting lists (path.preserve, diagnostics.ignore) from the command line.

Draft for the release notes

vfox config changes only the key you give it. It used to save the whole merged configuration, the shared values included, into your config.yaml, so those values stopped following later changes of the shared configuration, and --unset wrote the default value instead of removing the key. Now vfox config <key> <value> writes just that key and vfox config --unset <key> removes it, and the section it leaves empty. A key that does not exist, or a boolean that is neither true nor false, is an error now; it used to be ignored or read as false. To drop a copy that an earlier version wrote, remove the whole section: vfox config --unset path.

Verification

go test on all packages except internal/shared/printer (it needs a TTY): pass, on the commit as it is and on the tree that merges it with #738 (they do not conflict). go vet and the compilation of the tests for GOOS=linux, windows and darwin: clean. The docs site builds (VitePress 1.0.0 as in package-lock.json, no dead links). The CLI tests skip on Windows, as the fixture does (#737, item 5); the internal/config tests run there.

CI on my fork for the head commit (d4ebc1e) is green, with the Go 1.24.0 of go.mod: VersionFox CI (ubuntu, macOS, Windows, the Windows regressions, Fish) and vfox E2E Test (Unix, macOS, Windows, MSIX packaging): CI, E2E.

`vfox config <key> <value>` loaded the merged shared and user
configuration, changed one field and saved all of it into the user's
config.yaml. The user's file then held a copy of every section, the
shared aliases included, and because a section that the user's file
declares replaces the shared one, a later change of the shared
configuration no longer applied. `--unset` did not remove the key
either: it wrote the default value.

Edit the user's file as a YAML document instead, and touch only the key
that is set or unset. File is the generalization of AliasFile, which is
now a view of the `aliases` key of a File, so `vfox alias` is unchanged.

- Set checks the key against the yaml names of Config and the value
  against the type of the setting, and writes the value as that type.
  The other keys and the comments stay, a section the file does not
  declare stays undeclared, and a replaced value keeps the comment after
  it. A key that does not exist, a section, a list and a table are
  refused, and so is a boolean that is neither true nor false (it used
  to become false without a word).
- Unset removes the key and the sections it leaves empty: a section
  without a key would still be one the file declares, and it would
  replace the shared one. A key that the file does not have is not an
  error, and the file is not rewritten.
- Save refuses to write a file that does not load as a Config, so a
  change can no longer leave a config.yaml that stops every command
  (a cache duration under a second used to be written as "").
- A section that a file declares with one key keeps the defaults of the
  others when they are not zero values. Path and Diagnostics already
  did; LegacyVersionFile did not, so `vfox config
  legacyVersionFile.strategy x` would have switched the parsing of
  legacy version files off.

Tests: with a shared config present, setting one key copies nothing else
(on the config level and through the CLI), `--unset` hands the shared
value back, and a test walks every setting of Config so that a new one of
a type `vfox config` cannot set fails it. The old test of configSet moves
to the new API with all its assertions. `vfox config` runs inside the test
process now: it no longer changes the defaults that every command shares,
which is why it had a process of its own.

Part of version-fox#737.
@linghengqian linghengqian added the bug Something isn't working label Oct 23, 2026
@codecov

codecov Bot commented Oct 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.52239% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.03%. Comparing base (10c9f96) to head (d4ebc1e).

Files with missing lines Patch % Lines
internal/config/file.go 93.61% 3 Missing and 3 partials ⚠️
internal/config/legacy_version_file.go 71.42% 1 Missing and 1 partial ⚠️
internal/config/alias_file.go 87.50% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #739      +/-   ##
==========================================
+ Coverage   51.39%   52.03%   +0.64%     
==========================================
  Files         111      114       +3     
  Lines        8240     8319      +79     
==========================================
+ Hits         4235     4329      +94     
+ Misses       3583     3566      -17     
- Partials      422      424       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant