Skip to content

feat(macos): capture "unset" state instead of silently dropping prefs#67

Merged
fullstackjam merged 2 commits into
mainfrom
feat/macos-capture-unset-prefs
May 16, 2026
Merged

feat(macos): capture "unset" state instead of silently dropping prefs#67
fullstackjam merged 2 commits into
mainfrom
feat/macos-capture-unset-prefs

Conversation

@fullstackjam
Copy link
Copy Markdown
Collaborator

Summary

Fixes the structural side of #66's investigation. Previously CaptureMacOSPrefs silently dropped every catalog entry whose key wasn't explicitly set on the user's machine — so a snapshot that should have shown all 9 Menu Bar / Control Center toggles would only carry the 1 the user had actually customised, and the snapshot UI reported "1/1" instead of "1/9". Same dropout for every category.

Approach

Extend MacOSPref with Unset bool (omitempty for legacy snapshot compatibility), capture every catalog entry, and have every downstream consumer treat Unset entries as informational only.

File Change
internal/snapshot/snapshot.go Add Unset bool field with omitempty.
internal/snapshot/capture.go Record Unset=true + catalog default value when defaults read fails, instead of continue.
internal/cli/snapshot_import.go Filter Unset at the snapshot → RemoteMacOSPref boundary. Single chokepoint covers install-from-snapshot (Plan → Configure) and publish (state → API). RemoteMacOSPref intentionally has no Unset field.
internal/diff/compare.go diffMacOS ignores Unset entries on both sides — no false-positive Missing/Changed/Extra from "default" values.
internal/ui/snapshot_editor.go Unset prefs default to unselected and carry an (unset, default = X) badge in description.
internal/cli/snapshot.go Preview reports N (M set, K unset).
internal/archtest/baseline/no-direct-exec.txt Line-number shift only — same exec.Command sites in compare.go, two slots later.

Backwards compatibility

Unset bool \json:"unset,omitempty"`means snapshots written by the old CLI deserialize cleanly withUnset=false(covered byTestMacOSPref_JSON_LegacyDecodeNoUnsetField`). Newly written snapshots include the field only for unset entries, so the on-disk size barely changes.

RemoteMacOSPref schema is unchanged — Unset entries are filtered before they reach the remote API, so the contract with openboot.dev is preserved.

Test plan

  • go vet ./...
  • make test-unit (incl. archtest with refreshed baseline)
  • New unit tests:
    • TestMacOSPref_JSON_UnsetRoundTrip — marshal/unmarshal preserves Unset; omitempty when false.
    • TestMacOSPref_JSON_LegacyDecodeNoUnsetField — old snapshots without the field still load.
    • TestBuildImportConfig_MacOSPrefs_DropsUnset — boundary filter drops Unset entries before they hit state.
    • TestCompareSnapshots_MacOS_IgnoresUnset — Unset on either side contributes nothing to diff.
    • TestNewSnapshotEditor_UnsetPrefsDefaultUnselected — editor defaults Unset to unselected and surfaces the badge in the description.
  • L2 (CI) — real defaults round-trip will exercise capture on macOS runners.

Previously `CaptureMacOSPrefs` iterated the DefaultPreferences catalog,
ran `defaults read DOMAIN KEY` for each entry, and silently skipped any
key the user hadn't explicitly set on their machine. Result: a snapshot
that should have shown all 9 Menu Bar / Control Center toggles would
only carry the 1 the user had actually customised, and the snapshot UI
reported "1/1" instead of "1/9". The same dropout pattern hid most of
the catalog from every category.

Solution: extend the snapshot schema with an `Unset bool` flag (added
with `omitempty` so legacy snapshots decode unchanged), record every
catalog entry on capture — populating Value with the catalog default
and Unset=true when `defaults read` fails — and have every downstream
consumer treat Unset entries as informational only.

- internal/snapshot/snapshot.go — Unset field on MacOSPref.
- internal/snapshot/capture.go — record Unset=true instead of `continue`.
- internal/cli/snapshot_import.go — filter Unset at the
  snapshot→RemoteMacOSPref boundary. Single chokepoint covers both
  install-from-snapshot (Plan → Configure) and publish (state → API);
  RemoteMacOSPref intentionally has no Unset field because remote
  config models "what should be enforced", not "user had no opinion".
- internal/diff/compare.go — diffMacOS ignores Unset entries on both
  system and reference sides (no false-positive Missing/Changed/Extra).
- internal/ui/snapshot_editor.go — Unset prefs default to unselected
  and carry an "(unset, default = X)" badge in their description, so
  the user has to opt in before publishing one.
- internal/cli/snapshot.go — snapshot preview reports set/unset counts.
- Tests cover JSON round-trip + legacy decode, the import filter, the
  diff ignore, and the editor selection default.
- archtest baseline updated (pure line-number shift in compare.go from
  the new filter checks — same exec.Command call sites).
@github-actions github-actions Bot added tests Tests only snapshot Snapshot capture/restore ui Terminal UI labels May 16, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 16, 2026

Codecov Report

❌ Patch coverage is 94.73684% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/cli/snapshot.go 81.81% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@fullstackjam fullstackjam merged commit ff9a76a into main May 16, 2026
13 checks passed
@fullstackjam fullstackjam deleted the feat/macos-capture-unset-prefs branch May 16, 2026 14:50
fullstackjam added a commit that referenced this pull request May 16, 2026
…68)

#67 filtered Unset entries at the snapshot→state boundary and defaulted
them to unselected in the TUI editor. The reasoning ("Unset means user
had no opinion, don't propagate") was wrong: macOS does not write
default-equal values to the plist, so common cases like "Show Sound in
menu bar" — a key the user has never explicitly touched but which
macOS displays by default — capture as Unset. Filtering them dropped
8 of 9 Menu Bar items from publish and the web UI showed only "1",
unchecked.

Treat Unset as purely informational metadata going forward:

- internal/cli/snapshot_import.go — drop the boundary filter; publish
  every captured pref (Unset entries carry the catalog default, which
  is the value we'd want to enforce regardless).
- internal/ui/snapshot_editor.go — default Unset entries to selected.
  The `(unset, default = X)` badge in the description stays, so users
  can see which prefs originated from the catalog default vs the user's
  explicit plist value, but the default action is to include them.
- internal/diff/compare.go — unchanged; diffMacOS already treats Unset
  as "no opinion" on either side, which remains the right semantic for
  diff (vs publish, which is about transmitting the configured state).

Tests inverted to match (and renamed so the new contract is obvious).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

snapshot Snapshot capture/restore tests Tests only ui Terminal UI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant