fix(hands): merge Hand settings on save instead of replacing the whole config#6213
Conversation
…e config The Hand settings editor PUTs only the keys the user changed this session, but update_hand_settings forwarded that partial map straight to HandRegistry::update_config, which does a wholesale entry.config = config replace. For a Hand with more than one setting, saving one field overwrote the instance config with just that key and reverted every other previously-saved setting to its default. Read the active instance's current config in the route and merge the incoming keys over it, so untouched settings survive; the registry's replace contract is left unchanged (it has one other caller and a unit test pinning the replace semantics). The dashboard editor also seeds its payload from the saved current_values as defense in depth. Closes #6204
There was a problem hiding this comment.
Per CLAUDE.md §"What you MUST do for any route / wiring change": every route change needs a #[tokio::test] against TestServer before merge.
The PR body explains the blocker honestly — no fixture Hand exists in the test harness, so the settings happy-path is unreachable. That's a real constraint, not an excuse to skip, so it needs to be resolved in this PR rather than deferred to a follow-up.
Minimal unblocking path (no new fixture infrastructure required): add a test that POSTs to PUT /api/hands/{id}/settings against start_test_server() with a non-existent hand_id and asserts the 404 + "Activate the hand first" error shape. That covers the route registration, auth middleware pass-through, and None branch without needing a live Hand. For the merge path, a second test can install a minimal stub Hand (or mock HandRegistry) and assert the extend semantics — but the 404 test alone satisfies the "route wired and gated" bar that reviewers check.
Generated by Claude Code
houko
left a comment
There was a problem hiding this comment.
CLAUDE.md requires a #[tokio::test] against TestServer for any route behaviour change (update_hand_settings now merges instead of replacing). The PR body acknowledges the gap and proposes a follow-up, but CLAUDE.md's rule is: defer only if the fix would touch a different crate or domain — the test would live in crates/librefang-api/tests/, the same domain as this route. "New fixture infrastructure" for a Hand instance is worth adding here rather than deferring; the suggested test shape from the PR body is exactly right: install a fixture Hand with ≥ 2 settings, PUT {a}, PUT {b}, GET, assert both keys are present. Please add that before merge.
Generated by Claude Code
Adds a TestServer integration test that installs a two-setting hand, activates it with both keys set, then PUTs a single key and asserts the response config preserves the untouched key. Guards against a revert of the merge fix this PR introduced.
# Conflicts: # .secrets.baseline
Summary
Addresses #6204 (Hands settings bug). The issue is a screen-recording with no text, so I traced the Hands settings surfaces for the strongest concrete, code-grounded bug.
Root cause
The Hand settings editor builds its PUT payload only from
draft— the per-session diff containing just the keys whose input firedonChange(HandsPage.tsx).update_hand_settingsforwards that partial map straight toHandRegistry::update_config, which does a wholesale replace (entry.config = config,registry.rs:1355, pinned by theupdate_config_replaces_and_refreshesunit test). So for a Hand with more than one setting, saving one field overwrote the instance config with just that key and reverted every other previously-saved setting to its default. Invisible for single-setting Hands; data-loss for multi-setting Hands.Fix
crates/librefang-api/src/routes/skills/hands.rs—update_hand_settingsnow reads the active instance's current config and merges the incoming keys over it, making the endpoint a true partial update. The registry'supdate_configreplace contract is left unchanged (it has one other caller and a unit test pinning it).crates/librefang-api/dashboard/src/pages/HandsPage.tsx— the editor seeds its payload from the savedcurrent_valuesbefore overlaying the draft, as defense in depth.Confidence / scope note
Medium confidence this is the bug the (text-less) video depicts — it's the strongest concrete settings-save defect in this area. If the video instead shows the agent-config tab (system prompt / tools), that path persists and refetches correctly; the only residual there is the state-resync
useEffectpotentially wiping an unsaved draft on a window-focus refetch — flagging for whoever has seen the video.No backend integration test was added: the hands test harness installs no fixture Hand, so the settings happy-path is unreachable without new fixture infrastructure (out of scope for this fix). Recommend a follow-up that adds a fixture Hand with ≥2 settings and asserts PUT{a}→PUT{b}→GET keeps both.
Verification