mirror of
https://github.com/block/buzz.git
synced 2026-11-08 02:11:36 +00:00
> Stacks on #8759 (`bmcanally/cli-sidebar-sections`). The diff shows only this PR's commits. Merge #8759 first. ## Summary This change adds three command groups to `buzz sidebar`: - `buzz sidebar stars list|add|remove --channel <uuid>...` - `buzz sidebar mutes list|add|remove --channel <uuid>...` - `buzz sidebar projects list|add|remove --project <address>...` They edit the same synced documents that Buzz desktop and mobile edit. The documents are kind:30078 events with the `d` tags `channel-stars`, `channel-mutes`, and `project-sidebar-membership`. Each document is encrypted to your own key. This PR stacks on #8759 (`buzz sidebar sections`). ## Design **One flag-map module.** `sidebar_flags.rs` serves all three documents. A small `Spec` value holds the differences: the `d` tag, the collection name, the flag name, the timestamp unit, and the entry cap. **One shared read and write path.** `sidebar_sync.rs` holds the head read, the verification, the decryption, the encryption, the signing, and the publish. `sidebar sections` now uses it too. No code is duplicated. **Safety rules, the same as for sections.** - One head read and one publish for each command. - The CLI never publishes over a head that it cannot verify or decode. - `created_at` is `max(now, head + 1)`. - A relay `duplicate:` reply gives a conflict (exit 5). - Channel UUIDs and project owners are stored in lowercase. - A command that changes nothing prints `"message": "unchanged"` and publishes nothing. - The ciphertext must not exceed 65535 bytes. **Wire format.** Desktop is the reference: - Stars and mutes: `{version: 1, channels: {<id>: {starred|muted, updatedAt}}}`. `updatedAt` is in seconds. The newest 500 entries stay, ordered by `(updatedAt, id)`. - Projects: `{version: 1, projects: {<address>: {selected, updatedAt}}}`. `updatedAt` is in milliseconds. There is no entry cap. - `remove` writes a `false` entry. It does not delete the entry. This is how desktop does it. - Entries that fail desktop's validation are dropped, as desktop drops them. **Differences from the task text.** Desktop does these things differently, so the CLI follows desktop: - Project `updatedAt` is in milliseconds (`Date.now()`), not seconds. - Project membership has no 500-entry cap. The size limit bounds it. - When two project entries tie, desktop picks "not selected". The CLI never creates a tie (see below). - Desktop accepts a legacy array of addresses for projects. The CLI reads it. - Mobile does not evict entries. Desktop does. The CLI follows desktop. **Two rules that are stricter than desktop.** - A changed entry gets `updatedAt = max(now, old updatedAt + 1)`. Desktop merges a tie in favor of its local value. A plain `now` could lose to a same-second entry on another device. - `remove` of an id that is not set does nothing. Desktop writes a new `false` entry. The CLI skips it so that it does not publish or use an entry slot. A command that would evict one of its own ids (more than 500 new ids) fails with a usage error. ## Testing **Ran:** - `cargo test -p buzz-cli`: 518 passed. - `cargo clippy -p buzz-cli --all-targets -- -D warnings`: clean. - `cargo fmt --all --check`: clean. - `just file-size-check`: passes. **Test design.** The tests call the production `run` function against an axum fake relay. The fake relay checks the query filter (kind, `limit: 1`, `consistency: strong`, `#d`). Tests cover: - the stars and mutes journey (one publish for many ids, canonical ids, `unchanged`, `false` entries, invalid input) - an entry newer than the clock, and entries that desktop would drop - unreadable heads, a forged signature, a foreign author, and a `duplicate:` conflict - the 500-entry cap and the refusal to evict own ids - projects: milliseconds, address checks, the legacy array, no eviction, and the size cap **Interop evidence.** I wrote CLI documents and ran desktop's own TypeScript functions on them with node (`parseStarPayload`, `parseMutePayload`, `parseProjectSidebarMembershipPayload`, and the three `merge` functions). Desktop kept every entry. A merge of the document into itself changed nothing, so desktop sees it as settled. A device with the older value lost to the CLI edit, both as local and as remote. Script: `interop-flags.mjs`, output `INTEROP OK`. **Mutation checks.** I broke each guard once and confirmed that a test failed. The guards were: the newer-stamp rule, `created_at` after the head, the 500 cap, the self-eviction refusal, the no-op skip, flag type validation, negative `updatedAt`, the strict collection check, the project unit, the project kind and owner checks, the project cap, the ciphertext cap, signature verification, the author check, UUID lowercasing, the legacy array, the version check, and keeping `false` entries. Three mutants first survived (signature, author, negative `updatedAt`). I added a relay mode that returns foreign and forged events, and a test for each. Then all were killed. ## Manual test steps Use a test identity on a staging relay. Set `BUZZ_RELAY_URL` and `BUZZ_PRIVATE_KEY` first. Pick two channel UUIDs from `buzz channels list`. 1. Star two channels: `buzz sidebar stars add --channel <uuid1> --channel <uuid2>` Success: JSON with `"accepted": true`. Both channels show a star in desktop within a few seconds. 2. Run the same command again. Success: `"message": "unchanged"` and `"event_id": null`. 3. `buzz sidebar stars list` Success: `{"channels": ["<uuid1>", "<uuid2>"]}`. 4. `buzz sidebar stars remove --channel <uuid1>` Success: the star for `<uuid1>` is gone in desktop. `stars list` shows only `<uuid2>`. 5. Star a channel in desktop. Run `stars list`. Success: the channel is in the list. 6. `buzz sidebar mutes add --channel <uuid1>` Success: the channel shows as muted in desktop. `mutes remove` unmutes it. 7. Copy a project address from a project (`30621:<owner-pubkey>:<d-tag>`). Run `buzz sidebar projects add --project <address>` Success: the project appears in the desktop sidebar. `projects remove` hides it. `projects list` shows the pinned projects. 8. Run `buzz sidebar stars add --channel not-a-uuid`. Success: exit code 1 and an "invalid channel UUID" error. Nothing is published. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: bmcanally <bmcanally@squareup.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>