Skip to content

fix(config): say what a rejected value should have been, and prune the group it left behind - #650

Open
filip131311 wants to merge 1 commit into
mainfrom
filip/config-value-errors
Open

fix(config): say what a rejected value should have been, and prune the group it left behind#650
filip131311 wants to merge 1 commit into
mainfrom
filip/config-value-errors

Conversation

@filip131311

Copy link
Copy Markdown
Collaborator

Fixes #643.

Before / after

$ argent config set ios.additionalDeviceSets "~/Library/Developer/Radon/Devices" --scope project
Error: Invalid value for config key "ios.additionalDeviceSets".        # ← the whole message
$ argent config set ios.additionalDeviceSets "~/Library/Developer/Radon/Devices" --scope project
Error: Invalid value for config key "ios.additionalDeviceSets": expected an array of strings.
Did you mean: argent config set ios.additionalDeviceSets '["~/Library/Developer/Radon/Devices"]' --scope project
Run `argent config list` to see each key's expected value.
$ argent config unset ios.additionalDeviceSets --scope project
$ cat .argent/config.json
{ "ios": {} }        →        {}

Symptom 1 — the message

The issue names three gaps: no expected type, no example, no pointer to config list. All three are
closed, plus one the issue didn't ask for and I think is the actual fix.

The suggestion carries the user's own value. A generic Example: … '["~/DeviceSets/ci"]' would
hand someone a path they have never heard of and expect them to spot the difference. Where the
correction is mechanical — a single item offered to a list-valued key — the error offers back what
they typed, wrapped, shell-quoted, and on the scope they asked for. Pasting it works; verified
live that it lands in project scope, not global.

Nothing is coerced. The issue floated accepting a bare string and storing it wrapped; I didn't,
because config set persists, gets committed, and is read by other machines, so a value silently
stored in a shape the user didn't type is expensive to debug and coerceCliValue already has one
silent fallback. Showing the corrected command costs the user one keystroke and keeps the store
honest. If a reviewer prefers coercion, this is the smaller change to build on.

The expected shape comes from the validator, not a per-key stringasStringArray is "an
array of strings", so swapping a key's parse swaps its description with it and the two cannot
drift. A bespoke validator can override via a new optional expected, and degrades to today's exact
message if it says nothing.

config list states it too. The issue notes list doesn't close the gap either — its
description describes the elements but never says the value is a list. It now shows
value: an array of strings, e.g. ["~/DeviceSets/ci"], and list --json carries both fields.

telemetry.enabled deliberately gets no example: setConfigValue refuses it before validating
(it's managed by argent telemetry), so a "here's how to set it" hint would point at a command you
may not run. A schema guard test keeps that honest as keys are added — and asserts every example
actually round-trips through its own validator, so an example can never ship that reproduces the
error it exists to fix.

Symptom 2 — the empty group

Deleting a key now removes any container the delete emptied, stopping at the first ancestor that
still holds something.

This is file hygiene, not a behaviour change. I traced every raw reader of the document —
readConfigObject, and telemetry's consent.ts and notice.ts, which each do their own read — and
an empty parent is already indistinguishable from an absent one: getAtPath returns undefined
either way, before parse, so the merge layer never sees a container at all. The only observable
difference is the bytes on disk, which is exactly the complaint.

Guards worth noting:

  • The root always survives — an emptied config is {}, never a deleted file. .argent/ is
    itself a project-root marker, so removing the file wouldn't restore the prior state anyway, and
    updateConfig is a shared publish path with a lock/rename protocol that has no defined meaning for
    file removal.
  • A no-op unset still doesn't touch the file. That fast path exists so an unset that removes
    nothing can't materialise a project config and dirty git status; pruning lives inside the mutate
    callback, which the fast path returns before reaching. So a group someone deliberately left empty
    by hand is never tidied away — pinned by a test asserting the file stays byte-identical.
  • Siblings at any level stop the unwind, including an empty array value, which is preserved.

Two existing tests asserted the old behaviour and now assert the new one
config-access.test.ts:47 and config.test.ts:109 (the latter is clearRememberedAgent's
sibling-preservation test, so it doubles as proof siblings survive).

Known same-class case, not fixed here

resetFirstRunNotice (packages/telemetry/src/notice.ts) does its own inline delete and still
leaves {"notices":{}}. It doesn't go through deleteAtPath, so it doesn't inherit this. It's a
one-line change to route it through the same function — happy to do it here or as a follow-up, but
it pulls the telemetry package into a config PR so I left it out by default.

Verification

Live against the built CLI: the reported case; pasting the suggestion back and confirming it lands in
project scope; unset leaving {}; a sibling key surviving; a non-array key correctly getting the
generic example and no bogus "Did you mean"; config list output; telemetry.enabled still
delegating unchanged.

configuration-core 103 passed, argent-cli 285 passed.

🤖 Generated with Claude Code

…e group it left

Setting the one list-valued key to a single path — the natural thing to type —
answered with `Invalid value for config key "ios.additionalDeviceSets".` and
nothing more: no shape, no example, no next step. The schema already carried an
example for every settable key; nothing read it.

A rejected value now says what the key accepts, and where the correction is
mechanical it offers the user's own value back, wrapped and on the scope they
asked for:

    Error: Invalid value for config key "ios.additionalDeviceSets": expected an
    array of strings.
    Did you mean: argent config set ios.additionalDeviceSets '["~/Devices"]' --scope project
    Run `argent config list` to see each key's expected value.

The shape comes from the validator itself rather than a per-key string, so the
wording cannot drift from what is enforced. Nothing is coerced on the user's
behalf: the corrected command is shown, not run, so a value is never stored in a
shape they did not ask for. `config list` states the expected value too, which
its description never did — a list-valued key read exactly like a string one.

Unsetting the last key under a group left `{ "ios": {} }` behind, so `unset` did
not restore what `set` had changed — noise in a project config that gets
committed. Deleting a key now takes with it any container the delete emptied,
stopping at the first ancestor that still holds something. The root object always
survives, and a no-op unset still does not touch the file at all, so a group
someone deliberately left empty is never tidied away.

Two tests asserted the old behaviour and now assert the new one.

Fixes #643
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.

config set on an array-typed key rejects a bare string with no shape hint, and unset leaves an empty parent object

1 participant