mirror of
https://github.com/block/buzz.git
synced 2026-11-06 09:21:45 +00:00
9a6cb3ffdfe8bf077e184674d8850fdf48bfbdcd
1851 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fba8a7a94f |
feat(relay): log WebSocket EVENT writes on the access target (#9154)
Logs every WebSocket `EVENT` on the access target (`buzz_relay::access`). Before this, accepted writes logged only `Event ingested` on the handler target, and refused or ephemeral writes logged nothing. ### What's logged Each `EVENT` frame writes one `event=write` line when its handler finishes. The line has `conn_id`, `community`, `pubkey` (the authenticated login, if any), `kind`, `event_id`, and `outcome`/`reason`, taken from the OK the relay sent: - `accepted` - `rejected` (INFO), with `reason` set to the OK's NIP-01 prefix (`invalid`, `restricted`, `auth-required`, `duplicate`, `rate-limited`, …). Any other message is logged as `other`. - `error` (WARN), when the OK starts with `error` - `aborted`, when the handler ended before deciding an OK `outcome` is always the relay's decision. If the OK couldn't be queued because the outbound channel was full or closed, the line adds `ok_queued=false`; the field is left off otherwise. `EVENT`s refused before the handler runs are logged too, where their OK is sent: admission rate limits, with admission's reason, and a full handler pool, as `rate-limited`. The OK text after the prefix, the event's content and its tag values are never logged. Lines join to `auth`/`close` lines (and so to `peer_addr`) through `conn_id`, the same way `req` lines do. Rejected writes are capped per connection by the same refusal-budget mechanism as refused reads, on their own counter: the first 16 rejections are logged and the rest are only counted, in a new `writes_suppressed` field on the `close` line. Accepted writes are never capped. ### How `access::WriteLog` sends the OK and records it in one call (`write.ok(...)`). That call replaces every `conn.send(RelayMessage::ok(...))` in `handle_event` and the observer-frame handler, so the line always shows what the client was told. ### Tests - `access::tests`: the reason comes from the OK prefix only; each outcome logs at the right level and without content; rejected lines are capped by the budget and accepted ones are not (draining the channel so every OK is queued); an OK refused by a full or a closed outbound channel keeps its outcome and adds `ok_queued=false`. - `handlers::event::tests::fanout_access`: an unauthenticated `EVENT` going through `handle_event` writes one `rejected`/`auth-required` line, with no content or tag values. --------- Signed-off-by: Will Pfleger <wpfleger@block.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
e94b547f60 |
fix(desktop): keep an own send out of the frozen timeline tail (#9095)
## Summary The timeline freezes its live tail when Virtua reports that the reader has left the bottom, and a send arms a one-shot pin for its own row. A not-at-bottom report between the send and the sent row reaching the deferred snapshot froze the tail again, so the sent row waited behind a "1 new message" pill. That is the `messaging.spec.ts:483` flake in Desktop Smoke E2E. The first commit holds off that freeze while the send is pending. The other three are follow-ups from pre-PR review that bound the hold to the send itself: it ends when the row lands, it is disarmed when the send fails, and it is armed only for the channel the send belongs to. ### fix(desktop): keep an own send out of the frozen timeline tail #### Problem `messaging.spec.ts:483` "long autolink wraps without widening the timeline" flaked in Desktop Smoke E2E at :497: the timeline never showed the sent message. The composer was empty, and a "1 new message" pill sat at the bottom of a timeline that was already at the floor. - seiler/BUZZ-517/desktop-upgrade-guidance: - https://github.com/block/buzz/actions/runs/41751914119/job/126319694104 (retry 1 passed) The failure video shows the composer filling while the channel still showed its skeleton, the rows rendering under the tall composer, and the send landing about 120 ms later. #### Cause The timeline freezes its live tail when Virtua reports that the reader has left the bottom, and later rows wait behind the pill. A send lifts the freeze and arms a one-shot pin for the next append (`prepareForOwnMessage`). Nothing kept a not-at-bottom report between the send and the sent row reaching the deferred snapshot from freezing the tail again. The sent row then waited behind the pill, and the pin, which waits for that append, never fired. Only a later scroll to the floor would have released it. Virtua 0.49.3 makes that report while the reader sits at the floor. It applies a list length change to its store during render (Virtualizer.tsx). React discarded the deferred render that added the sent row, but the store kept the row's estimated height. When the cleared composer shrank the trailing spacer, the next scroll event put the reader 96 px from a floor the DOM didn't have: Virtua read 941 - 479 - 366 = 96, the DOM read 917 - 551 - 366 = 0. The freeze it queued renders at default priority, ahead of the deferred render, which then restarted against the frozen tail. #### Fix While the send's pin is armed, a not-at-bottom report doesn't freeze the tail. `useAnchoredScroll` exposes `isBottomPinArmed()`, which reads the pin's own flag, so the hold ends when the pin fires or the channel changes. `prepareForOwnMessage` also cancels a freeze queued before the send, which would otherwise land after its release. Bottom state still comes from Virtua. Reading it from the DOM would add false gaps of its own, because unmeasured rows overflow their slots: a passing run read 51 px from the DOM where Virtua read 0. A scroll up between the send and its row no longer freezes the tail. The pin brings the reader down to the sent row, as it already does when the row lands first. A send that adds no row leaves the pin armed until the next append, as before, and the hold follows it. #### Validation - New `MessageTimeline.ownSend.jsdom-test.mjs` renders the real MessageTimeline over Virtua with a modelled layout and manual frames. It sends with a gap reported after the send, and with a freeze already queued at the send. The sent row must render at the floor with no pill. Without the pin guard the first case fails, and without the cancel the second does, each showing "1 new message". - Repro, using temporary, uncommitted changes: the test with a 600 ms channel-head delay, filling during the skeleton, sending once rows render, with CPU throttled 4x over CDP. Without the fix: 3/20 failed with the CI signature, and instrumentation showed the 96 px report above. With the fix: 40/40 passed, and 6 of those runs logged a not-at-bottom report while the pin was armed. - `messaging.spec.ts --grep "long autolink"`: 30/30 passed at 4 workers, 10/10 serially. - messaging, virtualization, scroll-history, stream, smoke, thread-follow, file-attachment and message-target-highlight specs, run twice at 4 workers: 185 passed and 1 failed each time, a different test each run. Repeating those two fails as often without the fix: `:2785` (own avatar) 22/25 vs 20/25 at 4 workers and 8/10 vs 9/10 serially; `:3170` (thread panel width) 4/25 vs 8/25 and 1/10 vs 3/10. `:2785` fails on the avatar image after its message has rendered, and `:3170` never sends, so the change can't reach it. - `tsc --noEmit`, `biome check`, and the messages UI unit (337) and jsdom (9) tests pass. A green run doesn't prove the flake is gone. Hosted CI covers broad validation. ### fix(desktop): end an own send's freeze hold when its row lands #### Problem Review of |
||
|
|
bdd76baea5 |
fix(desktop): compensate head trims from the unclamped scroll offset (#9094)
## Summary
Re-entering a channel trims older pages from the head of the cached
timeline, and our Virtua patch compensates for each trim from the live
DOM `scrollTop`. When the reader was near the floor, the browser had
already clamped `scrollTop` to the shorter list, so the removal was
counted twice and the reader could be thrown to the top of history. That
is the `message-target-highlight.spec.ts` flake in Desktop Smoke E2E.
The patched scroller now remembers the last offset it observed or wrote,
and compensates from that offset when the live one was clamped to the
floor.
### fix(desktop): compensate head trims from the unclamped scroll offset
#### Problem
`message-target-highlight.spec.ts:79` ("message target flashes: ...")
flaked in Desktop Smoke E2E, always at :179: the linked row had left the
viewport 2.2 s after it flashed.
- main:
-
https://github.com/block/buzz/actions/runs/41568206043/job/125695139845
-
https://github.com/block/buzz/actions/runs/41669669478/job/126035103096
-
https://github.com/block/buzz/actions/runs/41669740241/job/126035480466
-
https://github.com/block/buzz/actions/runs/41694874145/job/126123865714
-
https://github.com/block/buzz/actions/runs/41728489172/job/126240193479
-
https://github.com/block/buzz/actions/runs/41737019256/job/126269365379
- branches:
-
https://github.com/block/buzz/actions/runs/41575774131/job/125719148353
-
https://github.com/block/buzz/actions/runs/41636043464/job/125918005733
-
https://github.com/block/buzz/actions/runs/41751914119/job/126319694104
-
https://github.com/block/buzz/actions/runs/41544910958/job/125618626645
-
https://github.com/block/buzz/actions/runs/41714110046/job/126190515793
All eleven runs predate #8742, and each failed in a case that picks
`mounted[2]`: the bottom-settle mode #8742 fixed by holding the target.
Run 41694874145 also failed once in "other channel, row near the
bottom", and that mode survives #8742: on current main it fails 40 of 60
local runs with tracing on, as CI's retries run.
#### Cause
Re-entering a channel refreshes its newest window, which drops the older
pages from the head of the cached timeline. #8660 passes that head trim
to Virtua as a `shift`, and Virtua compensates by the removed height in
a layout effect. Our Virtua patch (#2855) applies shift compensation to
the live DOM `scrollTop` so that corrections landing before the next
scroll event accumulate. But by then the browser has laid out the
shorter list and clamped `scrollTop` to the new floor whenever the
reader was within the removed height of it, so the removal is counted
twice. In a failing run the view sat 6px above the floor: trimming the
412px channel intro moved it 406px too far, and trimming 3054px of older
rows then wrote `scrollTop` -907, pinning it to the top.
`onStartReached` loaded older history and the target never came back. A
reader scrolled slightly above the bottom loses their place the same
way.
#### Fix
The patched Virtua scroller remembers the last offset it observed in a
scroll event or wrote itself. When a shift compensation finds the live
offset below that and pinned to the floor, the browser clamped it, so
the compensation starts from the remembered offset. Otherwise it keeps
#2855's live-offset base. The ESM and CJS builds change identically, and
the lockfile carries the new patch hash.
#### Validation
- New `virtuaShiftClampPatch.jsdom-test.mjs` drives the real patched
`VList` over a scroller that clamps like a browser. Head trims far from,
near and at the floor keep the reader's rows in place, a trim keeps a
scroll whose event has not fired yet, and a prepend then a trim before
any scroll event accumulate. Each part of the new condition fails a case
when removed: the previous patch line fails the near-floor, at-floor and
accumulation cases (3500 for 3900 and 4000, 3000 for 4000); upstream's
store-offset base fails the unreported-scroll and accumulation cases;
always using the remembered offset, or dropping the floor check, fails
the unreported-scroll case (2500 for 1900).
- "other channel, row near the bottom" with `--repeat-each=30
--workers=4 --trace=retain-on-failure`: 40/60 failed at :179 on current
main, 0/60 failed (60/60 passed) with the fix, and 25/30 failed again
with the previous patch reinstalled.
- All five cases, 20 traced runs each: 100/100 passed.
- scroll-history, virtualization, timeline-no-shift,
overscroll-boundary, dm-history-live-regression and
live-broadcast-reply-timeline, 3 runs each: 117/117 passed.
- Full smoke project: 1485 passed, 3 skipped, 2 failed.
`mentions.spec.ts:807` passed 5/5 on rerun. `messaging.spec.ts:2785`
(own-message avatar) fails the same way on a current-main build here
(3/3).
- `pnpm test`: 6933 unit and 176 jsdom tests passed.
**Rebased onto origin/main `64d12e297` (this branch):** Linux
workstation.
- `pnpm install --frozen-lockfile` installs virtua with the new patch
hash (`f4df71c39f6c...`).
- `pnpm typecheck` and `biome check --error-on-warnings` on the new
jsdom test are clean.
- Unit tests under `src/features/messages` and `src/features/channels`:
1990/1990. jsdom tests there: 12/12. Full desktop `pnpm test`: 6933 unit
and 176 jsdom tests passed.
- Playwright smoke after `pnpm build:e2e`, `--workers=4 --retries=0`:
messaging, send-channel-binding, scroll-history, virtualization,
timeline-no-shift, overscroll-boundary, live-broadcast-reply-timeline
and message-target-highlight, 137 passed and 1 failed. The failure is
`messaging.spec.ts:2785` (own-message avatar), which fails the same way
on main here, as noted above. `dm-history-live-regression.spec.ts` isn't
in the smoke project, so this run left it out.
- `message-target-highlight.spec.ts`, `--repeat-each=10`: 50/50 passed.
"other channel, row near the bottom", `--repeat-each=30`: 30/30 passed.
## Coordination
No open PR edits `patches/virtua@0.49.3.patch`. The overlaps are all in
`pnpm-lock.yaml`:
- #5547 ("fix(desktop): ship a bundle that parses on WebKit without
lookbehind (Intel/macOS 12)") and #7074 ("chore(deps): update react
monorepo (patch)") merge cleanly with main but conflict with this branch
on the `virtua@0.49.3` patch-hash lines. #5547 adds a patched dependency
next to the virtua entry, and #7074 changes the React peer versions in
the same lines. Whichever lands second resolves it by taking both sides'
package changes and re-running `pnpm install` so the lockfile records
`patch_hash=f4df71c39f6c...` for virtua.
- #7073, #7080, #7633, #8218 and #8643 also touch `pnpm-lock.yaml` and
merge cleanly with this branch. Older open PRs that touch it already
conflict with main, and none gains a new conflict from this branch.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Signed-off-by: mtoohey <mtoohey@squareup.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
8a113ed99e |
fix(desktop): show upgrade guidance (BUZZ-517) (#9033)
## Summary
Old Buzz desktop clients now check a community’s public requirement
before each connection. An explicit `requires_nip_fi: true` shows an
upgrade screen with a fixed https://buzz.xyz download action and manual
fallback. Missing or false requirements, and failed discovery, preserve
normal connection behavior.
Retry, community navigation and address editing preserve keys and saved
communities. Changing an address cancels the old onboarding transaction
and fences pending profile checks. Native discovery sends no
credentials, follows no redirects, and limits requests to five seconds
and 256 KiB. Wire fixtures are exported from main `6b8a72e835` with
reproducible provenance.
### Related issue
[BUZZ-517](https://linear.app/squareup/issue/BUZZ-517/v0-clients-show-an-upgrade-screen-for-communities-requiring-nip-fi).
Related: [#8984](https://github.com/block/buzz/pull/8984) contains
mobile parser preparation;
[#8920](https://github.com/block/buzz/pull/8920) repairs relay rejection
messages.
### Rollout
There is no client flag. The upgrade screen appears only when the relay
advertises `requires_nip_fi: true`. Public auto-update delivery requires
a desktop release containing this change and promotion to the [OSS
auto-update
feed](
|
||
|
|
ab399231c2 |
docs: mark desktop/ as the deprecated Buzz 0.X app (#8840)
## Summary `desktop/` is the Buzz 0.X desktop app. Buzz 1.0 lives in [block/buzz-app](https://github.com/block/buzz-app), and its README already says so. Nothing in this repo says it, so contributors and agents keep opening feature PRs against `desktop/`. This PR adds the rule in three places: - **`AGENTS.md`**: a notice at the top of "Desktop App" and a note in the repo layout. Agents read this file first. The notice also says that CLI PRs do not update the 0.X agent guidance in `nest_skill.md`. - **`CONTRIBUTING.md`**: a new item in "PRs We're Unlikely to Merge". - **`desktop/README.md`**: a notice at the top. The rule: build desktop features in block/buzz-app, or as a Buzz plugin. Change `desktop/` here only to fix a 0.X bug, or to keep it compiling after a shared crate changes. ## Testing - This PR changes only documentation. I checked the diff and the two block/buzz-app links. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Blake McAnally <bmcanally@squareup.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a1f79e3aba |
feat(relay): persist NIP-FI upgrades (BUZZ-510) (#8985)
## Summary Record that a community requires NIP-FI and provide an idempotent operator API to enable it and recover the original receipt. Acceptance checks the current owner, host, lifecycle and existing issuer trust in one transaction. It never creates trust. Receipts survive community deletion so an operation UUID cannot be reused. New acceptances default to disabled through `BUZZ_NIP_FI_UPGRADES_ENABLED`. Disabled calls return HTTP 503 `upgrades_disabled`; identical retries and authenticated readback still recover existing receipts. Policy is permanent: stopping new calls or rolling back code does not clear it. Last-issuer removal is refused for required communities, and bulk backfill leaves missing bindings for explicit operator repair. Apply additive migration 0082 before code. Existing communities remain optional. This layer records acceptance and reports only `pending` or `unavailable`; deploy and verify [per-request enforcement](https://github.com/block/buzz/pull/8988) and [session activation](https://github.com/block/buzz/pull/8991) before enabling acceptance. The global enforce floor remains. See [the operator contract](https://github.com/block/buzz/blob/seiler/BUZZ-510/relay-auth-upgrade/docs/community-auth-upgrades.md) for signing, recovery and containment. ### Related issue [BUZZ-510](https://linear.app/squareup/issue/BUZZ-510/relay-persist-community-nip-fi-requirements-and-expose-idempotent). Base of native stack `8989`; retains trunk's enforcement telemetry from [#8974](https://github.com/block/buzz/pull/8974). Includes a separate commit that repairs an observer timing flake in a desktop test required by the repository gate. ### Testing Controlled local regressions detect the old disabled error code and removal of the last issuer with `allow_last=true`; intended source is restored. Full PostgreSQL validation and the updated live relay/admin exercise remain pending. Human acceptance remains open. No deployment or production activation. Generated with Codex --------- Signed-off-by: Brad Seiler <seiler@squareup.com> Co-authored-by: Honor <e0bf479eeebe47dde1e35705212a4df70616a7bc834d907ddfb18cf0405f8b1b@seiler-test.communities.public.builderlabstaging.xyz> |
||
|
|
069cb9a0fe |
fix(desktop): hold a deep-linked message against bottom settles (#8742)
## Summary A product fix for the most frequent unfixed desktop E2E flake, `message-target-highlight.spec.ts:79`. A deep-linked message now holds its place against bottom settles until the reader scrolls. This is one of six PRs split from the reviewed `flakey-tests` branch (siblings below). - **Runs:** it failed in all 7 main runs whose SHA carries every earlier flake fix (40969965981, 40970005965, 40970053997, 40980258899, 40980404335, 40980513346, 40983029157). It also failed in 26 jobs since those fixes landed, on main and on PRs #8235, #8239, #8241, #8666 and #8680. Only the two cases that pick `mounted[2]` fail: "same channel, row loaded off screen" (15 jobs) and "other channel, row further up" (11 jobs). The target scrolls in and flashes, then the timeline lands on the newest message. `stillOnScreen` at :179 reads false (twice it was `flashedOnScreen` at :153). ### Cause The failure videos (run 40983029157, Smoke shard 6) show that the thread panel opening is a precondition, not the trigger. A later floor report moved the target: `TimelineMessageList`'s `onScroll` reported within 32 px of the bottom, during or after the older-page load that only a `mounted[2]` target sets off. `onVirtualizerAtBottomStateChange(true)` reset the anchor to at-bottom. Then the split-panel settle, the viewport-resize settle or the next append settle pinned the timeline to the floor. Which Virtua layout pass emits the floor report is still unidentified. ### Change Accepting a virtual route target sets `targetHoldRef` in `useAnchoredScroll`. Only the two `targetMessageId` paths set it, via `holdTarget`. The public `scrollToMessage`, which the oldest-unread pill and find-in-channel use, doesn't hold, and it replaces any earlier hold. While the hold is set: - The split-panel and viewport-resize settles are skipped. - A floor report still updates `isAtBottom` and the new-message count, but keeps the message anchor, so live appends don't settle either. - A commit that changes the newest message sets `isAtBottom` false and clears `virtualizerAtBottomRef`. It adds the rows after the previous newest one to the new-message count. So the "Jump to latest" / "N new messages" pill still appears for rows below the fold (Review-Proven Rule 6). A history page that fills the gap below a cold target raises no pill, and neither does deleting the newest message. - Viewport reflow re-centres the target through `virtualScrollToMessage`. It runs from the resize observer and when the split panel opens or closes. The hold is released by reader scroll input: a wheel, a touch drag, a pointerdown on the scroller itself, or a scroll key. Those listeners already cancel bottom intent in `useVirtualizedBottomSettle`, and this PR shares them as `listenForReaderScroll`. Jump to latest, the reader's own send and a channel change also release it. A release at the floor restores the at-bottom anchor, so the timeline follows live messages again. `onTargetReached` does not release the hold. ### Known limits - Programmatic scrolls (focus moves, `scrollTop` writes) don't release the hold, but a find or unread-pill jump replaces it. A resize after such a scroll re-centres the target. - ChannelPane's composer-height settle still calls `scrollToBottom` within 32 px of the floor, which releases the hold. - Live messages that arrive under a hold near the floor aren't followed until reader input, Jump to latest or the reader's own send. The pill shows them, with their count. - Under the hold, only rows after the previous newest message count. A same-second row that sorts just ahead of it raises no pill until a later row lands. - Agent review raised two optional gaps, and scrolling recovers from both: - If the previous newest message is deleted and a new row arrives in the same deferred commit, 0 rows are counted. - A thread reply sent with "also send to channel" doesn't arm the bottom intent, so it shows as "1 new message" instead of being followed. ### Related issue None found. ### Testing `useAnchoredScroll.lifecycle.test.mjs` replays the CI sequence: accept a route target, clear it, open the split panel, report the floor, resize and append a live message. It asserts no bottom settle, a re-centre on the panel opening and on the resize, and the pill on the append. Further cases cover: - each release path; - a find jump on a held timeline; - a descendant pointerdown keeping the hold; - a gap-fill page and deleting the newest message, neither of which raises the pill. The split-panel and viewport-resize policy tables each gain a held row. Mutations were run over the lifecycle file and both policy tables (29 tests, all pass intact). Each of the 20 mutations fails at least one test: | Mutation | Tests that fail | |---|---| | Hold never set (the old behaviour) | 12 | | Split-panel settle ignores the hold | 13 | | Viewport-resize settle ignores the hold | 13 | | Floor report resets a held anchor | 12 | | No split-panel re-centre | 12 | | No viewport-resize re-centre | 12 | | Reader input doesn't release the hold | 6 | | Held append raises no pill | 2 | The commit message lists the other 12, each failing 1 or 2 tests. `pnpm typecheck` and `pnpm check` pass. All 46 tests in the files that import the touched modules pass. Only hosted CI can show whether the spec now holds in Smoke shard 6 and in Flake Check's repeats, because Playwright was not run locally. It also has to show that `scroll-history.spec.ts` and the other timeline specs keep bottom-follow working. Pushed with `git push --no-verify`, because the pre-push suites fail when the laptop is under load. ### Readiness and human check An agent review of the combined `flakey-tests` branch (b432453af) found no blockers. This changes desktop app behaviour, so it needs a human in-app check before `buzz-review-completed` is added. Suggested check, in `just dev` against a channel with a few pages of history: 1. Open a message link (a mention or search hit) to a message several screens up, once in the same channel and once from another channel. The message should flash and stay on screen after older history loads, including when the thread panel opens or closes, or the window is resized. 2. With the target still held, have someone post in the channel. The timeline stays put, and the pill reads "1 new message". 3. Wheel-scroll to the bottom, or click Jump to latest. The next message that arrives is followed. ### Siblings - #8739 ci: Hermit fetch deadline and sccache fallback - #8740 test(relay): thread_window aux-page hold and `api::media` Postgres tests - #8741 test(mobile): RunnerTests title fade and alert wait - #8743 test(desktop): four specs racing scroll settles and focus - #8744 test(desktop): five settle classes and remote-owned-mentions 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f99807a79a |
fix(desktop): skip a starter channel the relay will never create (#8911)
## Problem On first run, the desktop app creates the `general` and `welcome-everyone` starter channels at fixed ids when it can't find a live one. Some refusals of that create are permanent. Any refusal aborted setup **before it joined the new member to the starters that do exist**: | Starter state in the community | Relay reply to the create | Before | |---|---|---| | Another listed channel holds the name, archived ones included (with #8782) | 400 `invalid: an open channel named "…" already exists` | setup fails, not joined to `general` | | An archived channel holds the fixed id | 400 `invalid: channel is archived` (the relay checks this before duplicates) | setup fails | | The channel at the fixed id was renamed or retyped | `duplicate` | waits, then `starter channels created but metadata not yet available` | | The channel at the fixed id was deleted | `duplicate`, and no metadata is ever served | same | Onboarding itself continues, because public starter failures are already caught in `initializeStarterChannels`. The new member just never gets joined to the other starter. With #8782 this happens to every new desktop user in a community whose `welcome-everyone` or `general` is archived. ## Fix `ensure_starter_channels_inner` now skips a starter it can never create or use, clears its pending-owner mark, and still joins the rest. Before skipping after a permanent refusal, it reads the open directory again, from the writer rather than a replica, once for all refused starters: a live starter created after the first read is joined instead of skipped. Any other refusal still fails setup. On the TS side, `ensureStarterChannels` reports a skipped starter as `null` instead of throwing. No caller reads those fields. ## Testing - New tests run `ensure_starter_channels_inner` against a stub relay that sends the relay's real reply shapes, one per row above, plus one checking that other refusals still fail, and one where a live `welcome-everyone` is listed only by writer-pinned reads (a lagging replica) and gets joined. Each test fails when its guard is removed. - Ran the real starter-setup code as a new user against a local relay built from #8782, one community per row: - On `main`, rows 1, 2 and 4 fail. - On this branch, all four join `general`, and each community still has exactly one `welcome-everyone`. - A rerun is a no-op. - `cargo test` (desktop Tauri, full), clippy, fmt, desktop `pnpm test`, `tsc`, and biome all pass. Related: #8782 (relay refuses taken names), #8807 (the same fix for mobile). The archived-id case returns `channel is archived`, not `duplicate`, so mobile needs to handle that reply too. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Salman Mohammed <smohammed@squareup.com> Co-authored-by: opus <f038cc09b32a776e7d95b65c267e9d851777ee2b6cdbd46a48edba01b1d10ebd@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
a83926da5d |
test(desktop): stop four E2E specs racing scroll settles and focus (#8743)
## Summary Units 2–5 of the 23–30 Oct desktop Playwright flake audit: four E2E specs that raced scroll settles and focus. All four fixes are test-only. This is one of six PRs split from the reviewed `flakey-tests` branch (siblings below). ### `navigation.spec.ts:472` "composer Buzz chip labels wrap without orphaning their icons" 11 jobs failed in the window, 6 of them after the earlier fixes, including 4 of the 7 main runs that carry every fix. Run 40970053997 (Smoke shard 7) failed all three attempts. - **Mode A** (jobs 122971493553, 123066839967, 123572467962, 123819180597 and retry 2 of 123784825042): `fragmentStep` is null at :634. Rows key on `renderKey = localKey ?? id`, and a head refetch or live echo after the ack drops the `localKey`. The row remounts and loses the 220 px width the test set earlier. **Change:** re-land the test half of closed #6852. Set the width and measure in one `expect.poll`, and set the width again before each fragment measurement. - **Mode B** (jobs 123573056202, 123784199542, 123784825042 twice, 123804428893, 123818728963, 123819543461, and 122924164125): the tooltip-centre poll at :667 receives 81.35 or Infinity. Radix Tooltip closes when an ancestor of its trigger scrolls. In the failing attempt, the post-send settle took its last step 21 ms or less before focus. **Change:** wait for the sent row's `message-send-status` to clear. Then poll until the timeline sits at its floor and holds there for two animation frames, and only then focus the chip. `positionOverFragment` asserts `data-state` is open. There is no retry loop around the race. ### `huddle-transcription.spec.ts:637` "animates the responding agent with the shared speaker ring" - **Runs:** 9 jobs, always Smoke shard 4, attempt 1. On main: 39896948198, 40718743531, 40732061485, 40840949170, 40906253761, 40969965981 and 40983029157. On PRs: #8715 and #8666. `toBeFocused()` at :697 reads inactive. - **Cause:** `useComposerAutofocus` runs again once the editor mounts and takes focus from the focused trigger. - **Change:** wait for `message-input` to be focused before focusing the trigger, as the same spec already does at :855. ### `scroll-history.spec.ts:1289` "fast middle-page scroll settles with continuous mounted coverage" - **Runs:** 9 jobs. After the fixes: main 40980404335 (job 123819180518) and two attempts on #8720. The `viewportCoverage` poll expects < 100 and receives 200, the bottom spacer. - **Cause:** the bottom-settle intent armed when the channel first positions survives the prepend and the spec's `scrollTop` writes. Virtua's re-measure then pulls the list back to the floor. - **Change:** hover the timeline and wheel up 120 px first, since reader input cancels the intent. Then poll until the timeline is more than 32 px from the floor and steady for two frames. - The commit message calls the cancelling listener `listenForReaderScroll`, a name introduced by sibling #8742. On `main`, the same wheel listener is inline in `useVirtualizedBottomSettle`, so this fix doesn't depend on that PR. ### `custom-emoji.spec.ts:370` "reacting with a custom emoji renders via the loopback media proxy" - **Runs:** main 40787609859 and 40983029157 (Smoke shard 3), and #8241. The inline "Add reaction" opacity reads "1", not "0". - **Cause:** the non-modal Radix Popover returns focus to its trigger in a `setTimeout(0)` after its 100 ms exit animation. That can land after the spec's blur. - **Change:** wait for the "Open reactions" trigger to be focused, then blur. ### Related issue None found. Sibling #8744 also edits `navigation.spec.ts`, in a separate hunk. The two merge cleanly in either order. #8622 adds an unrelated test at the end of `navigation.spec.ts`, and #7812 (stale since 10-06) adds tests elsewhere in three of these files. ### Testing Not run locally, because Playwright bogs this machine down. Flake Check will repeat the touched specs. `pnpm typecheck` and `pnpm check` pass. An ad hoc `tsc` run over the four specs found the same pre-existing errors as on their `main` copies (navigation 7, huddle 17, scroll-history 0, custom-emoji 2). Pushed with `git push --no-verify`, because the pre-push suites fail when the laptop is under load. ### Readiness An agent review of the combined `flakey-tests` branch (b432453af) found no blockers. `buzz-review-completed` is not added, because the human test step (AGENTS.md "Before opening a PR", step 3) hasn't happened yet. ### Siblings - #8739 ci: Hermit fetch deadline and sccache fallback - #8740 test(relay): thread_window aux-page hold and `api::media` Postgres tests - #8741 test(mobile): RunnerTests title fade and alert wait - #8742 fix(desktop): deep-linked message hold (product change) - #8744 test(desktop): five settle classes and remote-owned-mentions 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fc0bb778b0 |
test(desktop): settle five E2E flake classes and clear a draft by Backspace (#8744)
## Summary Units 6–9 and 14 of the 23–30 Oct desktop Playwright flake audit, in two commits. The specs now settle before they measure or act, and remote-owned-mentions clears its draft reliably. Everything is test-only. This is one of six PRs split from the reviewed `flakey-tests` branch (siblings below). ### Unit 6: measuring before an animation finishes Each spec now calls `waitForAnimations(page)` before measuring. - `onboarding.spec.ts:1487` (#8715, Integration shard 1; the centring check at :1545 receives 5.13): `showPage("owned")` slides content in from `translateX(48px)` over 650 ms. The spec reads its three boxes in separate `boundingBox` round trips, so they can come from different points of the slide. - `onboarding.spec.ts:2289` (main 40746677244 and 40925172215, and #8666; the spacing check at :2485 receives 4.255): the avatar dialog has a 200 ms zoom-in and a 250 ms height transition. - `projects-v3-screenshots.spec.ts:280`, at :893 (main 39643531998, 40759209995 and 40969909906, plus four runs with fractional menu-edge values): the context rail narrows from 380 to 280 px over 200 ms. ### Unit 7: `fill()` racing a caret move the app makes a frame later - `mention-recipients.spec.ts:376` and `:97` (main 40969965981 and 40969858676): edit mode focuses the end of the composer in a `requestAnimationFrame`. If that frame lands mid-`fill()`, the edit is appended instead of replacing the text. **Change:** use #8682's `replaceComposerText` at both call sites. - `workflow-local-controls.spec.ts:151` (main 40926393897 and 40957616146, and #8239): `selectVariable` restores the caret in a `requestAnimationFrame` after the spec's poll has passed. **Change:** wait two animation frames before filling. ### Unit 8: clicks that hit the wrong element - `profile.spec.ts:1128`, at :1543 (#8523): stacked Sonner success toasts intercept the pointer. **Change:** a local `pressActionRow` helper presses the five action rows with `toBeEnabled`, focus and Enter. - `config-bridge-screenshots.spec.ts:314` (main 40967147375, and #8235): `.last()` resolved while avatar fallbacks were still filling in on 200 ms timers, so it opened nadia's panel instead of Charlie's. **Change:** pick Charlie's row by its text, and check the panel's name before opening Runtime. ### Unit 9: the `createWorkflow` copies (preventive) - `navigation.spec.ts:153` (7 failures on main, none since #8681; 14 first-attempt failures in Smoke shard 7): an unscoped `getByLabel("Message text")` can fill the trigger's filter while `AnimatePresence` keeps it mounted. **Change:** wait for the step inspector, then fill inside it, as `workflows.spec` already does. The wait now lives in one `addSendMessageStep` helper in `tests/helpers/workflows.ts`, which `navigation.spec.ts` and `workflows.spec.ts` (`createWorkflow`, :716, :820) all call. - `workflow-reaction-picker.spec.ts:203-204` is left out. Its Reaction Added trigger has no control labelled "Message text". ### Unit 14 (second commit): `remote-owned-mentions.spec.ts:566` - **Run:** main 40998438103 (Smoke shard 9, job 123877347301). It passed on retry and failed 1 of the 6 attempts it got in the window. `sourceRecord()` at :673 returned thread A's draft instead of null. - **Cause:** on ProseMirror, `fill("")` selects through the DOM, then presses Delete. If ProseMirror hasn't seen the selection yet, it blocks the Delete. Navigating to B then saves A's live text as its draft. This is not a product bug: had the editor really been empty, the cleanup would have cleared A's draft. - **Change:** delete the text with one Backspace per character from the end, then wait for the composer to read empty. The guard assertions are unchanged. ### Related issue None found. Sibling #8743 also edits `navigation.spec.ts`, in a separate hunk. The two merge cleanly in either order. No open PR touches the tests changed here. #6134, #8187, #5778, #8622, #7452, #7812, #8244 and #3342 edit other tests in the same files. ### Testing Not run locally, because Playwright bogs this machine down. Flake Check will repeat the touched specs. `pnpm typecheck` and `pnpm check` pass, and biome's 3 warnings are in untouched files. Ad hoc `tsc` runs over the touched specs and the helper found the same pre-existing errors as on their `main` copies. Pushed with `git push --no-verify`, because the pre-push suites fail when the laptop is under load. ### Readiness An agent review of the combined `flakey-tests` branch (b432453af) found no blockers. `buzz-review-completed` is not added, because the human test step (AGENTS.md "Before opening a PR", step 3) hasn't happened yet. ### Siblings - #8739 ci: Hermit fetch deadline and sccache fallback - #8740 test(relay): thread_window aux-page hold and `api::media` Postgres tests - #8741 test(mobile): RunnerTests title fade and alert wait - #8742 fix(desktop): deep-linked message hold (product change) - #8743 test(desktop): four specs racing scroll settles and focus 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
d14e95f90e |
test: stop desktop, buzz-agent and mobile tests leaking temp files (#8748)
## Summary Six test files wrote into the shared temp dir on every normal run and never removed what they wrote. **Desktop and buzz-agent (first commit)** - `desktop/src/protectedFeatures/tauriCommand.test.mjs` wrote its fake Tauri CLI and three result files straight into `/tmp` under pid-suffixed names. They now live under one `mkdtempSync` root that an `after()` hook removes. - `crates/buzz-agent/tests/permission_boundary.rs` wrote a `buzz_perm_calllog_*` file for every test whose tool call reached `fake_mcp`, five per run. `call_log_path` now returns a `TempDir` guard with the log path inside it, so the log is deleted when the test ends, including when an assertion panics. - `cancel_kills_inflight_tool_via_mcp_notification` in `crates/buzz-agent/tests/regressions.rs` spawns `buzz-dev-mcp`, which the agent stops with SIGKILL, so the server's `buzz-dev-mcp-*` and `buzz-dev-mcp-session-*` TempDirs were never deleted. The test now passes the server a `TMPDIR` it owns and removes. **Mobile (second commit)** The three mobile test files left 12 `buzz-video-*` files and 4 `transcode_*` dirs per run. - `video_viewer_test.dart` and `video_controls_test.dart` pointed their fake `path_provider` at `Directory.systemTemp`. The viewer downloads each video there as `buzz-video-*`, and its async delete on close never completes under FakeAsync. Each test now gets its own temp dir for the fake, and an `addTearDown` removes it with `deleteSync`. In `video_controls_test.dart` that teardown is registered first, so it runs after the viewer is disposed. - Four `media_upload_test.dart` tests faked `transcodeVideoToMp4` by writing `out.mp4` into a new `systemTemp` `transcode_*` dir. The upload service deletes `out.mp4` but not its dir. The fakes now create that dir under the test's own temp root, which the test already deletes in `finally`. Production code is unchanged. **`buzz-dev-mcp`'s SIGKILL shutdown is left for a follow-up:** the agent still stops the server with SIGKILL, so outside tests the server's temp dirs are still left behind. Sending SIGTERM first is the real fix. ### Related issue None found. This continues the test-cleanup work in #8727. ### Testing Each run below was bracketed by listings of `/tmp` and the per-user temp dir (`/var/folders/.../T`), so new leftovers would show up. - `tauriCommand.test.mjs`: 3/3 pass, with no new `buzz-tauri-*` or `buzz-fake-tauri-*` entries. - `cargo test -p buzz-agent --test permission_boundary`: 9/9 pass, with no new `buzz_perm_calllog_*` files. The tests still check that the call log is written, so the log really is going into the guarded dir. - `cancel_kills_inflight_tool_via_mcp_notification` passes with `buzz-dev-mcp` built, so it ran rather than skipped, and left no new `buzz-dev-mcp-*` dirs. - Mobile: `video_viewer_test` (7 tests), `video_controls_test` (20) and `media_upload_test` (38) pass, each run on its own. With `TMPDIR` pointed at a scratch dir, the old versions left 12 `buzz-video-*` files and 4 `transcode_*` dirs, and the new versions left it empty. - After cherry-picking onto current main, these pass: - `cargo clippy -p buzz-agent --all-targets -- -D warnings` - biome and `node --check` on `tauriCommand.test.mjs` - `dart format` and `flutter analyze` on the three mobile files 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
704f2b8c27 |
test(desktop): keep unit tests away from the real ~/.buzz nest (#8749)
## Summary
`nest_dir()` fell back to `~/.buzz` whenever `init_nest_dir` had not
run. That is always true in unit tests, so a test that reached nest code
read the real nest and could rewrite `~/.buzz/AGENTS.md`.
- Under `cfg(test)` that fallback now panics. The message says how to
pin the nest: `pin_temp_nest_dir_for_thread()` for work on the test
thread, or `pin_nest_dir_for_test` in a child-process test for work on
other threads. The new thread pin returns a guard that owns a `TempDir`.
Dropping it unpins the thread and deletes the dir. A process-wide pin
does not fit in-process tests, because `cargo test` runs them all in one
process and the `OnceLock` can be set only once.
- Seven tests reached the fallback, all through
`default_agent_workdir()`:
`auth_command_uses_augmented_path_for_node_adapter`,
`test_prepared_install_command_uses_default_workdir` and five
`provider_deploy_*` tests. They now pin a temp nest. Under `cfg!(test)`,
`default_agent_workdir()` resolves on every call instead of caching for
the process, because a cached per-test nest would be deleted before the
next test used it.
- `nest_dir_is_under_home` and `init_nest_dir_prod_sets_buzz` could only
pass by reaching the fallback. A `should_panic` test of the guard
replaces them, plus a test that the thread pin and its dir go away on
drop.
- `init_nest_dir()` still resolved the real `~/.buzz{,-dev}` under
`cfg(test)`. An in-process test that reached `run_boot_migrations` would
have set the process-wide nest to the real one for every later test.
Now, under `cfg(test)`, it panics unless the nest is already pinned with
`pin_nest_dir_for_test`, and is a no-op when it is, as a second call is
in production. A thread pin does not count, because `init_nest_dir` sets
the nest for the whole process. No test reaches this today.
- Both panic messages point at `commands/agents_tests.rs`, which calls
`pin_nest_dir_for_test`, as the example to copy.
Production behaviour is unchanged.
### Related issue
None found. This continues the test-cleanup work in #8727.
### Testing
- `cargo test --workspace` in `desktop/src-tauri` passes: 3430 lib
tests, including both child-process pin tests.
- The new `init_nest_dir()` test runs in a child process, so a missing
guard cannot leak the real path to other tests. With the guard disabled,
it fails at "unpinned init_nest_dir() must panic".
- The `managed_agents::nest` tests pass with the final panic message.
- `just desktop-tauri-clippy` passes on the default and mesh-llm graphs,
re-run after cherry-picking onto current main.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
1eb67bb525 |
fix(desktop): fix the product races behind four E2E flakes (#8680)
## Summary Fixes the four desktop product races behind recurring E2E flakes in the 21–28 Oct 2026 CI audit. Each fix has a regression test: - In the channel browser, Enter acted on a stale deferred list. - A stale autocomplete swallowed the Enter that submits an empty edit. - Same-second messages swapped order on screen. - The unread pill read "Jump to latest" instead of "N new messages". Two E2E specs that failed for the same ordering reason also get test-side fixes. This is one of seven PRs split out of the `flakey-tests` branch (siblings below). ### Related issue None found for these. Draft #8559 does not fix the voice-note flake. Draft #8661 (Refs #6733) closes the `#channel` list synchronously when an edit is cleared. §11 below also covers that case, plus the `@mention` and `:emoji` lists, so the two PRs overlap in `useChannelLinks.ts` and `empty-edit-delete.spec.ts`. ### Flaky tests fixed | Test | Failing runs | Cause | Change | |---|---|---|---| | §5 `channel-browser.spec.ts:402` "Enter with no matches jumps to create" | 15 of 50 main runs, e.g. 40597484657, 40582387515, 40566841699, 40367495044 | **Product.** The list renders from `useDeferredValue(query)`. An Enter that landed before the deferred render read the old list, so it opened a stale match or skipped the create row. | While the deferred query lags, Enter filters the live query with the same `selectBrowserChannels` the list renders from. | | §11 `empty-edit-delete.spec.ts:88`, `:111`, `:132` | :88 in 3 jobs, :111 in 3, :132 in 2. :111 failed all 3 attempts on main 39822883395 and 39799090750 | **Product.** Clearing the draft went through the 120 ms query debounce. The `#channel` list stayed open, so Enter re-inserted the suggestion instead of submitting the empty edit. | `useChannelLinks`, `useEmojiAutocomplete` and `useMentions` (typed `@` only) close as soon as the trigger is gone. Typing a query still debounces. `ChannelAutocomplete` gains `data-testid="channel-autocomplete"`. | | §10 `video-attachment.spec.ts:1469` "right-click menus expose distinct selectors…" | 10 of 50 main runs, e.g. 40528171327 | **Product.** The live cache merge (`sortMessages`, ascending id within a second) and the channel-window projection (descending id) disagreed, so two same-second rows swapped on screen as each writer landed. | A new `shared/lib/eventOrder.ts` holds `compareRelayOrder` and its exact reverse, `compareChronologicalOrder`. `sortMessages` and the window and reconciliation sorts all use the window's order, so older-page paging is unchanged. `sortEvents` keeps ascending id. The spec gives its three messages distinct seconds. | | §16 `voice-note.spec.ts:906` "starting a duplicate voice-note player pauses the other instance" | 6 of 50 main runs. All 3 attempts on PRs 39622488737 and 40309414135 | Same-second messages plus index-based card lookup. | Distinct seconds, and each card is found through its own message row. | | §13 `stream.spec.ts:477` "keeps scroll position when new messages arrive above the fold" (integration) | 7 of 50 main runs. All 3 attempts on PR 39546556833 | **Product.** Frozen rows came back reordered (7 of 12 instrumented runs). The selector ended older history at the snapshot's first id, so moved rows were both prepended and kept in the frozen tail. The duplicates filled the row count, `pendingCount` fell to 0, and the pill read "Jump to latest". Deleting a frozen row also dropped the whole freeze. | `useBufferedTimelineMessages` keys rows by `renderKey`, which is stable across an optimistic send and its ack (`frozenRowKeys`). It drops only deleted frozen rows, and ends older history at the first frozen row of any key. `stream.spec` is unchanged. | New regression tests: - `channel-browser.spec`: two forced Enter variants. - `empty-edit-delete.spec`: a `#`/`@`/`:` autocomplete table. - `autocompleteTriggerClose.jsdom-test`. - `projectChannelWindow.test`: the cache merge and the window agree in both arrival orders. - `channel-dense-second-reach.spec`: a RAF sampler sees one order. - `useBufferedTimelineMessages.test`: deletion, reorder and ack rows. - `virtualization.spec`: deleting a row while reading keeps the next arrival counted. - A `useThreadReplies.test` row: the thread fetch and a live append agree on same-second order. Local before/after counts are from the original `flakey-tests` commits (238c08c7c, c66f411f3): | Test | Before | After | |---|---|---| | channel-browser | 0/20 | 20/20 | | autocomplete table | 0/30 | 30/30 | | jsdom | 0/2 | 2/2 | | dense-second sampler | 0/10 | 10/10 | | buffered-timeline unit | 4/7 | 7/7 | | virtualization deletion | 0/10 | 10/10 | | `stream:477` against an isolated relay | 25/30, all failures "Jump to latest" | 30/30, then 20/20 and 10/10 | ### Verification This branch was pushed without local re-verification, by the user's choice: local test runs were overloading the machine and failing for that reason. **Hosted CI is the check.** It was pushed with `git push --no-verify`, because the pre-push hook runs typecheck and the unit suites. #8613's flake-check lane will also repeat the changed specs and unit tests. - Every file except `empty-edit-delete.spec.ts` and `virtualization.spec.ts` is byte-identical to its final version on `flakey-tests` (67185df6a). Those two carry only this PR's hunks; see "Merge order". - Checked for this PR: `git diff --check`, biome on both partial specs (no changes, the same as on `main`), and a conflict-free merge with all six siblings onto `main`. ### Readiness These are **product fixes in the desktop app**. Under AGENTS.md "Before opening a PR", this needs an agent review, an agent run of the affected flows, and a **human in-app check** before `buzz-review-completed` is added. Suggested check, in `just dev` or a local build against any community: 1. **Channel browser:** open "Browse channels", type a name that matches nothing and press Enter straight away. The create-channel flow opens with that name. Then type an existing channel's exact name and press Enter straight away. That channel opens, not an earlier match. 2. **Empty-edit delete:** edit one of your own messages and type `#gen` so the channel list opens. Select all, delete, and press Enter at once. The empty edit is submitted (the delete-by-clearing flow) and no suggestion is re-inserted. Repeat with `@` (mention list) and `:sm` (emoji list). 3. **Same-second order:** send two short messages in quick succession, within one second. They appear in send order and never swap while they land. 4. **Unread pill:** in a busy channel, scroll up past the fold, and have a second client post one message. The pill reads "1 new message". While still scrolled up, delete one of your visible messages, then have the second client post again. The pill still counts the new message. ### Merge order No required order, but two files are shared: - `desktop/tests/e2e/empty-edit-delete.spec.ts` is also edited by **flakey-tests-desktop-e2e-actions**. This PR adds the autocomplete table after line 158. The sibling changes imports, helpers, `beforeEach` and line 143. - `desktop/tests/e2e/virtualization.spec.ts` is also edited by **flakey-tests-desktop-e2e-gates**. This PR adds one test after line 929. The sibling's hunks are all above line 551. The hunks don't touch, so each pair merges cleanly in either order, as checked with `git merge-tree`. Draft #8661 overlaps this PR in `useChannelLinks.ts` and `empty-edit-delete.spec.ts`; whichever lands second will need a rebase. ### Sibling PRs Split from `flakey-tests` (one PR per area): #8676 (PostgreSQL lease + lane), #8677 (relay E2E helper + Windows shell), #8678 (mobile), #8679 (Tauri apt cache + Windows Rust cap), #8681 (desktop E2E state gates), #8682 (desktop E2E composer + row actions). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Jitter <2b2e6415e748c35180847a37aadae06a11e30dddc76b784e7fd9354c4eb42e7a@buzz.block.builderlab.xyz> |
||
|
|
0052b2354e |
test(desktop): gate flaky E2E specs on the state they assume (#8681)
## Summary Test-side fixes for ten desktop E2E flake clusters and six long-tail rows from the 21–28 Oct 2026 CI audit. Each spec now waits for the state it assumes (a rendered row, a mounted listener, a finished animation, a held boot read), not a fixed delay or a one-shot probe. Two mock-bridge seams support this. The PR changes test code and mock-bridge seams only, apart from one product constant extracted with its value unchanged. It is one of seven PRs split out of the `flakey-tests` branch (siblings below). ### Related issue None found. - #8622, #8244 and #8386 touch some of these spec files, but not the tests fixed here. - §7 mode A (`navigation:472`) is left to open PR #6852, which already sets the width and measures in one polled evaluate. - Draft #8279 queues cold-launch nostr-bind links in the product. Its mock returns null, so the §12 listener seam is still needed. ### Mock-bridge seams - `__BUZZ_E2E_EVENT_LISTENER_COUNT__(event)` counts live **app** listeners per Tauri event. It wraps the mocked `plugin:event|listen`/`unlisten`. The spec-facing `__TAURI_INTERNALS__.listen` passes a marker target that the wrapper skips, so a spec's own subscription can't satisfy the wait. Tauri 2.11.1's mock ignores targets, so delivery is unchanged. - `deferFirstChannelsRead` arms the existing channels-read hold at boot. `__BUZZ_E2E_RELEASE_CHANNELS_READ__` releases it. - `CREATE_PROJECT_REFOCUS_DELAY_MS` moves to `createProjectFormFocus.ts` with its value unchanged (50 ms). Both `CreateProjectFormContent` and `project-commit-detail.spec` import it. ### Flaky tests fixed | Test | Failing runs | Cause | Change | |---|---|---|---| | §2 `overscroll-boundary.spec.ts:34` (and its sibling) | 39601763602, 40367495044, 40583830861, 40597484657, 40373404267 | The `message-timeline` test id first lands on the loading skeleton, which doesn't overflow, so the scroll lock rightly cancels the wheel. | Wait for a rendered row, and content taller than the viewport, before dispatching wheels. | | §4 `project-conversation-load-failure.spec.ts:73` | 40367495044 (all 3 attempts), 40608383847, 40583830861; PRs 39541263507, 40509460168 | The seeded root and reply were dated `now`. The panel's live REQ (`since = floor(now)`) still replayed the reply, so the error card never appeared. | Date both events a minute in the past, and correct the comment. | | §6 `workflows.spec.ts` `createWorkflow` cluster (:222, :365, :297, :525, :471, :406, :614, :997) | 14 of 50 main jobs; :222 ×4, e.g. 40353619362 | `AnimatePresence mode="wait"` keeps the trigger body mounted through its exit, so "Message text" could fill the trigger's field (:222 and the callers). For :297, `isVisible()` read "closed" one frame before the dialog opened its own channel list. | Wait for the step body's "Step details" control before filling. Create dialogs wait for the list; only the duplicate dialog clicks it. | | §12 `nostr-bind.spec.ts` (:156, :198, :228, :247, :260, :335, :417, :447) | 8 main jobs, each test once | The spec emitted `deep-link-nostr-bind` before the app subscribed, and the mock drops events that have no listener. | Wait for `__BUZZ_E2E_EVENT_LISTENER_COUNT__` to report the app listener before emitting. | | §15 `profile.spec.ts:1128` (integration) | 7 of 50 main jobs | The 320 ms motion-sampling window started before the click. | Start the window when `activeTab` reads "runtime". | | §17 `agent-availability.spec.ts:602`, both shutdown-failure variants (integration) | 6 jobs (4 disconnected-online, 2 failed-online) | The "shutdown refused" toast sits over the Delete row and pauses while the pointer is on it, so the retry click is intercepted. | Assert the toast, then retry with focus + Enter. The toaster has no close button. | | §19 `virtualization.spec.ts:247` "08 — cascading older pages…" | 4 of 50 main jobs; PR 40605736093 (all 3 attempts) | The 120 ms and 400 ms windows were timed from before the wheel bursts, and CDP can deliver wheels late. | Count the wheels the page receives. Each window opens at the first wheel and closes only after all have landed, then waits for still frames. **A second, product-side mode remains** (see below). | | §20 `message-copy-link.spec.ts:160` | 4 of 50 main jobs (5 failed attempts), e.g. 40595483966 | The huddle row reused `"d".repeat(64)`, which is a seeded ordinary `#general` message. | Use ids that no seed uses, and emit only after `waitForMockChannelHeadReady`. | | §21 `navigation.spec.ts:687` | 4 of 50 main jobs | The pending chip's `boundingBox()` was read once, and can be null while the row mounts. | Poll until the chip has a width. | | §23 `sidebar-snapshot.spec.ts:358`, `:388`, `:438` | PR 40427822003 (all 3 attempts); never on main | The 500 ms loading-state budget counted app boot, so a slow runner finished the channels read first. | Hold the first channels read with `deferFirstChannelsRead`, assert the loading state, then release it. | | `project-commit-detail.spec.ts:259` | 1–2 CI failures (audit long tail) | A 50 ms refocus timer can land mid-fill and type the description into the name field. | Once the name field is focused, wait out a page timer of `CREATE_PROJECT_REFOCUS_DELAY_MS`; equal-delay timers fire in order. | | `project-pr-review.spec.ts:3520` | 1–2 CI failures | The same timer pattern in `CreateProjectWorkItemDialog`. | Wait for `toBeFocused()` on the title, which only that timer can satisfy. | | `thread-load-failure.spec.ts:79` | 1–2 CI failures | `openWelcomeThread` branched on a one-shot `count()` and fell back to a hover-only Reply click. | Wait for the summary row, then click it. | | `virtualization.spec.ts:201` | 1–2 CI failures | The drop point sat 2 px above the target's bottom edge, after two fixed frames. | Drop at the centre, after dnd-kit's live region names the target. | | `channel-composer-overflow.spec.ts:217` | 1–2 CI failures | Same-second emits sort by id, and the clearance was checked once. | `createdAt: base + i` and `expect.poll`. | | `scroll-history.spec.ts:1289` | 1–2 CI failures | A fixed 250 ms wait before idle sampling. | Poll for coverage first. | Local before/after counts are from the original `flakey-tests` commits (e286a6ae0, 816c7c35e, 67185df6a). Each used a forcing that made `main`'s copy fail with CI's signature: | Test | Before | After | |---|---|---| | §2 | 3/15 | 15/15 | | §4 | 0/10 | 10/10 | | §6 :222 | 0/3 | 3/3 | | §6 :297 | 0/3 | 3/3 | | §12 | 0/3 | 3/3 | | §15 | 0/3 | 3/3 | | §17 | 1/4 | 4/4 | | §19 late-wheel modes | 0/3 | 3/3 and 2/3 | | §23 | 2/6 | 6/6 | | project-commit-detail | 0/3 | 3/3 | | project-pr-review | 0/3 | 3/3 | §20, §21, `thread-load-failure:79`, `virtualization:201`, `channel-composer-overflow:217` and `scroll-history:1289` were not reproduced; they get the fix that matches their CI signature. Not fixed (follow-up): `virtualization:247` also has a product-side mode. In one frame after a prepend, `scrollHeight` can drop 458–639 px while `scrollTop` moves only 50–114 px, so the anchor row jumps 382–457 px. That is lost anchor compensation, and this test can still fail on it. ### Verification This branch was pushed without local re-verification, by the user's choice: local test runs were overloading the machine and failing for that reason. **Hosted CI is the check.** It was pushed with `git push --no-verify`, because the pre-push hook runs typecheck and the unit suites. #8613's flake-check lane will also repeat the changed specs. - Every file except `virtualization.spec.ts` is byte-identical to its final version on `flakey-tests` (67185df6a). That file carries only this PR's hunks; see "Merge order". - Checked for this PR: `git diff --check`, biome on the partial spec (no changes, the same as on `main`), and a conflict-free merge with all six siblings onto `main`. ### Readiness Test and mock-bridge change; the one product edit is a constant extraction with its value unchanged. AGENTS.md's readiness steps (agent review, then human confirmation) are still outstanding, so `buzz-review-completed` is not set. ### Merge order No required order. `desktop/tests/e2e/virtualization.spec.ts` is also edited by **flakey-tests-desktop-races**, which adds one test after line 929; this PR's hunks are all above line 551. They don't touch, so the two merge cleanly in either order, as checked with `git merge-tree`. ### Sibling PRs Split from `flakey-tests` (one PR per area): #8676 (PostgreSQL lease + lane), #8677 (relay E2E helper + Windows shell), #8678 (mobile), #8679 (Tauri apt cache + Windows Rust cap), #8680 (desktop product races), #8682 (desktop E2E composer + row actions). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Jitter <2b2e6415e748c35180847a37aadae06a11e30dddc76b784e7fd9354c4eb42e7a@buzz.block.builderlab.xyz> |
||
|
|
428f0dcd22 |
test(desktop): settle composer edits and press hover-only row actions by keyboard (#8682)
## Summary Test-side fixes for the two largest desktop E2E flake clusters in the 21–28 Oct 2026 CI audit: - **Composer `fill()` races the app's deferred focus-to-end** (§1). Text was appended to the old draft, or a clear did nothing. - **Clicks on the hover-only message action bar while the timeline re-pins** (§3/§8/§18). Reply and more-actions clicks timed out or were intercepted. Two shared helpers replace the per-spec workarounds, and §9 rides along because its spec now imports them. The PR changes test code only. It is one of seven PRs split out of the `flakey-tests` branch (siblings below). ### Related issue None found. #8559, #8622 and #8613 touch these specs or helpers, but had no coverage for these clusters. Draft #8661 edits `empty-edit-delete.spec.ts`'s `submitEmptyEdit` and `beforeEach`, the same lines as this PR. ### New helpers - `tests/helpers/composer.ts`: - `clearComposer` waits for the app's focus, then repeats select-all + Delete through the editor keymap until the editor is empty (250 ms per try, 5 s overall). - `replaceComposerText` clears, then fills. On an empty document, a late collapse to the end is a no-op. - `tests/helpers/channelHeadReady.ts`: - `GENERAL_CHANNEL_ID`. - `openGeneral` clicks `#general`, then calls `waitForMockChannelHeadReady`. - `pressRowAction(button)` does focus + Enter. The action bar also shows on focus-within, and more-actions is a Radix trigger that opens on Enter. ### Flaky tests fixed | Test | Failing runs | Cause | Change | |---|---|---|---| | §1 `inbox-edit.spec.ts:525` | 39641311877, 39821769576, 40333705511, 40348069929, 40388246283 | **Append:** entering edit mode, or closing the non-member mention dialog, focuses the end of the draft in deferred steps: RAF, a nested RAF, then ProseMirror's 20 ms selection resync. `fill()` selects and inserts in separate protocol steps, so a step that lands in between collapses the selection and the text is appended. **Clear does nothing:** `fill("")` presses Delete before ProseMirror handles `selectionchange`, so the editor blocks it. | `clearComposer` / `replaceComposerText` | | §1 `inbox-edit.spec.ts:560` | 39538411386, 39821769576 | Same as above. | Same | | §1 `mention-recipients.spec.ts:591` (both variants) | 39799090750, 40374286094, 39641311877 | Same | Same | | §1 `mention-recipients.spec.ts:691` (both variants) | 40348069929, 40556019361, 39601763602 | Same | Same | | §1 `messaging.spec.ts:3799` | 39827234118, 40445481983 | Same, plus a bare `#general` click and a `force: true` Reply | Same, plus `openGeneral` and `pressRowAction` | | §1 `messaging.spec.ts:4022` | 40389392495 | Same | Same | | §1 `messaging.spec.ts:4289` | 39822883395, 40557808800, 40566841699 | Same | Same | | §1 `messaging.spec.ts:4368` | 40574058918 | Same | Same | | §1 `remote-owned-mentions.spec.ts:463` (both variants) | 39618692657, 40374286094, 40382726903, 40389392495, 40582387515 | Same, after the Invite dialog closes | Same | | §3 `messaging.spec.ts` :3531, :3647, :4122, :4208, :4450 (root and reply) and :4633 (keyboard and button) | :3531 40583830861, 40595483966. :3647 40566841699, 40597484657. :4122 39641311877, 40368315739, 40368363357. :4208 39525092486, 39796569342, 40367495044. :4450 39796569342, 40348069929, 40373404267. :4633 39530486096, 40348069929, 40368315739, 40582387515, 40368363357 | The action bar is `pointer-events-none` until its row is hovered. Opening `#general` refreshes the head after the live subscription lands, and the timeline re-pins, so the row moves out from under the pointer. `force: true` only hid this. | `openGeneral`, then `pressRowAction` on Reply or more-actions. Six more messaging tests that used hover + `click({ force: true })` get the same change, and each first waits for its last row to contain the sent root. | | §18 `thread-follow.spec.ts:350` | 39529963092, 40348069929, 40583830861 (all 3), 40595483966; PR 39541263507 (all 3) | Same; the composer overlay intercepts the more-actions click. | Same | | §8 `thread-head-stale-edit.spec.ts:55` | 39525092486, 39618692657, 39796569342, 39822883395, 40011714848, 40281448810, 40368363357, 40389392495, 40445481983, 40574058918; PR 40031299575 (all 3) | Same (used `force: true`) | Same | | `file-attachment.spec.ts:165` | 1–2 CI failures (audit long tail) | A more-actions click on a moving timeline | Same | | `empty-edit-delete.spec.ts` (`openMoreActionsMenu`, `submitEmptyEdit`, the non-empty edit, `beforeEach`) | Covered under §11 in the desktop-races sibling | The same hover and `fill` races | The head-ready wait before emitting, `pressRowAction`, `clearComposer` and `replaceComposerText`. The autocomplete table rows keep their back-to-back keys, which is what they test. | | §9 `file-attachment.spec.ts:419` "upload progress floats above the dock and lifts Jump to latest" | 10 of 50 main jobs, including 40583830861, 40597484657, 40608383847 | A programmatic `scrollTop` keeps the bottom intent armed. The upload card resizes the content, the timeline re-pins, and the pill unmounts under `boundingBox()`. The fixed 250 ms wait read the card mid-slide. | `scrollTimelineUp()` leaves the bottom with a real wheel, and one `expect.poll` checks all three layout relations. | Local before/after counts are from the original `flakey-tests` commits (e286a6ae0, 816c7c35e, c66f411f3, 67185df6a), running `main`'s copy and the changed copy side by side under forcings: - **§1 at 4× CPU throttle:** 130/130 after; `main`'s copies ranged from 5/10 to 10/10. - **§1 with `selectionchange` delayed, a stretched resync, or a gap inside `fill()`:** `main`'s copies 0/5 to 4/5 and 0/20; after 65/65 and 20/20. - **§3 at 4× throttle:** `main` had 3 Reply-click timeouts at :4208 and 3 at :4633 (keyboard); after 120/120. - **`file-attachment:165`:** 0/3 before, 3/3 after. - **§9 unforced:** 4/5 before, 5/5 after. These were measured with the helper's first version, which waited two frames plus the 20 ms resync. The final state-based helper ran unforced only: 157/157 and then 196/196 over the touched specs. Its retry was not checked under a forced late collapse. :3531, :3647, :4122, :4450 root, :4633 button, `thread-head-stale-edit:55` and the six force-click tests were not force-reproduced; they get the fix that matches their CI signatures and reproduced siblings. Dropped: - `mention-recipients.spec.ts:933` (40608383847) fails on `flavors.handled`, which is unrelated. - `persistent-agent-audience.spec.ts:828` (`@Mor@Morg`) did not reproduce in 79 forced runs. ### Verification This branch was pushed without local re-verification, by the user's choice: local test runs were overloading the machine and failing for that reason. **Hosted CI is the check.** It was pushed with `git push --no-verify`, because the pre-push hook runs typecheck and the unit suites. #8613's flake-check lane will also repeat the changed specs. - Every file except `empty-edit-delete.spec.ts` is byte-identical to its final version on `flakey-tests` (67185df6a). That file carries only this PR's hunks; see "Merge order". - Checked for this PR: `git diff --check`, biome on the partial spec (no changes, the same as on `main`), and a conflict-free merge with all six siblings onto `main`. ### Readiness Test-only change. AGENTS.md's readiness steps (agent review, then human confirmation) are still outstanding, so `buzz-review-completed` is not set. ### Merge order No required order. `desktop/tests/e2e/empty-edit-delete.spec.ts` is also edited by **flakey-tests-desktop-races**, which adds the autocomplete table after line 158. This PR changes imports, helpers, `beforeEach` and line 143. The hunks don't touch, so the two merge cleanly in either order, as checked with `git merge-tree`. Draft #8661 edits the same `submitEmptyEdit`/`beforeEach` lines; whichever lands second will need a rebase. ### Sibling PRs Split from `flakey-tests` (one PR per area): #8676 (PostgreSQL lease + lane), #8677 (relay E2E helper + Windows shell), #8678 (mobile), #8679 (Tauri apt cache + Windows Rust cap), #8680 (desktop product races), #8681 (desktop E2E state gates). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
425284aa2c |
test(desktop): measure inline video controls against their player (#8683)
## Summary
`video-attachment.spec.ts:856` (inline video hover reveals a timeline
without a second play control) fails in about 1 of 15 runs. This change
fixes the test only.
The test reads the inline controls' page `y` at rest and again after
hovering, then asserts the two differ by less than 0.5px, so the
controls don't slide in. In failing runs the whole message row moves 1px
in the timeline between the two reads. The controls stay in the same
place relative to their player:
```
pass {"restControls":529,"hoverControls":529,"restPlayer":392,"hoverPlayer":392}
fail {"restControls":529,"hoverControls":528,"restPlayer":392,"hoverPlayer":391}
```
Now the test measures the controls' offset from their `video-player` in
a single `evaluate`, so it reads both values in the same frame. Moving
the row no longer matters, and a real slide-in still fails.
### Why it showed up now
On [buzz #8680](https://github.com/block/buzz/pull/8680), Flake Check
failed 6 of 14 Playwright shard runs on this test. That PR edits shared
helpers in the same spec file, so `scripts/flake-check.mjs` selects the
whole file and repeats every test in it 10 times. The test hadn't
changed on main since September. It just had never been repeated before.
## Testing
- Reproduced on clean `main` with `playwright test --project=smoke
video-attachment -g "inline video hover reveals" --repeat-each 15`: 1
failed, 14 passed.
- With this change: `--repeat-each 40` gave 40 passed. The full
`video-attachment` spec also passed, 13 of 13.
- To confirm the test can still fail, I temporarily added `translate-y-1
group-hover/video:translate-y-0` to the inline controls. The test failed
with `Received: 4`.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Signed-off-by: Matt Toohey <contact@matttoohey.com>
Co-authored-by: Jitter <2b2e6415e748c35180847a37aadae06a11e30dddc76b784e7fd9354c4eb42e7a@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
4b21a10c4b |
fix: don't notify workflow owners through their attribution tag (#8694)
A workflow message names its owner in a marked p tag, ["p", owner, "", "owner"], for attribution. #p live, history and push matching, operator listeners, desktop, mobile and the iOS NSE treat that tag as attribution rather than a mention. A workflow that mentions its owner adds a plain p, so the owner is notified only when actually mentioned. NOSTR.md records the #p exception. Co-authored-by: studio <dc2fc481343672df31a689840c09299972e704dac592e0da6f18486646a7ee3e@buzz.block.builderlab.xyz> Signed-off-by: Tom Brow <tomb@squareup.com> |
||
|
|
ae689ab54d |
fix(desktop): resume reconnect replay from the last live point (#8652)
## Summary A relay rolling deploy closes every socket with 1012. On reconnect, desktop replayed each live subscription's **original filter** (startup `since`, full `limit`) unless that subscription had received an event. Quiet subscriptions therefore re-read history back to app start every time. Paged channels did this twice: they resent the original filter *and* ran the HTTP repair pages over the same window. A 1012 close also reset the backoff to the 1s minimum, so a client bounced from pod to pod during a deploy came back at roughly the same moment as every other client. **This PR makes three changes:** 1. **Resume cursor.** The subscription records `liveUntil`, when the connection closes, if it reached EOSE on that connection. That time is the last frame received from the relay, not the close itself. The session tracks that time per peer frame, independent of the stall watchdog, so a close delivered late (after a sleep, or while replay is still running) doesn't overstate how long the subscription was live. Close and error notices don't count as liveness. A frame processed more than 70s after the previous one (the stall watchdog's idle timeout plus its check interval) may have been queued through a sleep, so from then on it doesn't extend liveness for the rest of that connection. Liveness also starts fresh on every connection. If the time is unknown, the previous `liveUntil` is kept rather than guessed from the clock. The replay then uses `since = max(original since, lastSeen − 5s, min(liveUntil − 960s, now − 3600s))`. - **Why the one-hour window:** the relay can drop a fan-out frame for a client whose send buffer is full without closing the socket (`ConnectionState::send`), so continued pings don't prove a subscription missed nothing. The liveness term therefore never resumes later than an hour ago, which is the same window as the agent harness's silent-drop repair (#8653). The saving applies to history older than an hour, so it matters most for long-running clients, which is the deploy-bounce case. Older drops still reach the timeline, because auto-heal refetches the open channel's newest window on reconnect. What they miss are live-only side effects such as unread and mention badges. - **Why 960s:** the relay accepts `created_at` up to ±900s from its clock (`MAX_TIMESTAMP_DRIFT_SECS` in `crates/buzz-relay/src/handlers/ingest.rs`). An event stored after the close can therefore be backdated by that much, and the extra 60s covers client clock skew. - **Gap floors win:** a pinned failed-backfill floor still takes precedence. - **No cursor:** a subscription without one replays as before. - **Paged channels** keep their 1865s event-time lookback. 2. **Live-only REQ for paged channels.** When the HTTP repair covers the gap, the live REQ goes out as the original filter with `limit: 0`. The relay serves no stored events for it (`req.rs` passes the limit through to a SQL `LIMIT 0`). - **Backfill/live overlap:** the repair waits for that REQ's EOSE before it starts. EOSE arrives only after the subscription is registered, so live and repair always overlap. - **If EOSE never arrives**, a single 25s deadline, measured from when the REQ was sent, leads to the bounded history fallback below. The stall watchdog runs during replay, so a half-open socket that never sends EOSE or a close is reset by the watchdog rather than holding recovery. - **A reset, disposal or supersession** wakes that EOSE wait immediately, and the wait never starts for a retired connection, so a dead attempt never holds `connect()` (and the next reconnect) behind it. A `connect()` whose connection was replaced during replay settles as superseded instead of arming the watchdog. - **If EOSE never arrives, or all three repair attempts fail**, the live REQ is re-issued under the same ID with the window's bounded history (`since` = floor, `limit` ≤ 500): the pre-change behavior. The missed window still fills on this connection, and the floor stays pinned for anything the cap left out. Duplicate suppression stays on until that request's own EOSE, so events live already delivered aren't dispatched twice. If the original EOSE was never seen, a late one can't be told apart from the fallback's, so only the deadline or teardown ends suppression. Teardown also ends it, and so does a 25s deadline in case the relay drops the EOSE. A subscription disposed while the fallback is in flight still ends with a CLOSE. 3. **Reconnect spreading.** After a 1012 or 1001 close, the first attempt waits a uniform random delay in [0, 10s] instead of resetting to 1s. Ordinary backoff jitter is a symmetric ±25%, capped at 30s. The backoff resets only after the existing 60s stable-connection check, never just because a socket opened. **Simpler?** The backoff state, including its 60s stable-connection reset timer, moved out of `relayClientSession.ts` into a small `RelayReconnectBackoff` class in `relayReconnectPolicy.ts`. That makes the jitter math testable on its own. It also keeps the session file at 1199 lines, the same as `main` (its cap is 1200), even with the new liveness and replay logic; the liveness gap rule lives in `relayResumeCursor.ts`. The stall watchdog's API is unchanged from `main`. **Identified debt (not changed here):** - `relayClosedRecovery.ts` retries a retryable live CLOSED with the original filter (1s → 30s backoff), with no terminal state. - `useRelayAutoHeal.ts` sends an invalidation wave after reconnect. - The `lastSeen − 5s` term keeps its existing gap for events backdated after a close. - `liveUntil` means the subscription was registered, not that every event was delivered. The one-hour window bounds this: a dropped fan-out frame older than an hour on a quiet subscription isn't re-read over the websocket. The fix is a relay-side drop signal, the same follow-up as #8653's. - An HTTP repair page that is already in flight when the socket resets still holds `connect()` until it returns, as on `main`. The native HTTP client sets no request timeout. - A `limit: 0` REQ still makes the relay acquire a reader for an empty query. ### Related issue N/A. This is part of a set of reconnect-load reductions. Companion PRs: relay read-pool warm (#8651), agent harness (#8653) and mobile (#8659). ### Testing - **Full desktop gate** on a Linux workstation at `045030c4`: - `pnpm typecheck`, `pnpm check` and the file-size check pass. - `pnpm test`: 6901/6901, plus 165/165 jsdom. - `pnpm build:e2e` passes. - The `relay-reconnect` Playwright spec passes, including the restart spread draw, which passed 10/10 repeats. - **New or extended tests** (`node:test`): - `relayResumeCursor.test.mjs`: a table test over the cursor inputs, including the one-hour window, plus the liveness gap rule. - `relayClientResumeCursor.test.mjs`: drives the real `RelayClient`. It covers a dropped fan-out followed by pings, a frame queued through a sleep, fresh credit per connection, a half-open socket reset by the watchdog during replay, and a reset or disposal while parked on EOSE. - Extended: `relayReconnectReplay` (deadline from send, a single timeout → fallback, a late original EOSE, fallback dedupe and CLOSE convergence), `relayReconnectPolicy` and `relayClosedRecovery`. - **Mutations:** each guard was reverted and failed at least one test. That covers every cursor term, the 900 + 60 skew, the one-hour window, `limit: 0`, the EOSE wait, its deadline and its timeout, floor precedence, EOSE generation stamping, the last-inbound cap and gap rule, the per-connection reset, the watchdog starting before replay, fallback dedupe, the late-EOSE guard, the 1001 match, the backoff reset, the restart spread and jitter symmetry. - **Exercised against a real relay** (PG16 and Redis) in a relay-backed desktop build, with Chromium on a Linux workstation, base `b310fea0` vs head `045030c4`. Each channel was seeded with 200 reactions and 3 messages, all two hours old: | | Base | Head | |---|---:|---:| | Quiet channel, stored events replayed over the websocket | 204 | 1 | | Paged channel, stored events replayed over the websocket | 50 | 0 (`limit:0`) | | Paged HTTP repair, events returned | 204 | 1 | | Event-query calls / rows returned | 68 / 895 | 73 / 441 (−50.7% rows) | - **21-minute process freeze:** the watchdog fired first. The quiet cursor matched `max(lastSeen − 5, min(preFreeze − 960, now − 3600))` exactly, and 3/3 messages posted during the freeze were recovered once each. - **Drop before the paged EOSE:** `connect()` released in 13ms. The next dial followed on the normal backoff, and the message arrived once. - **Half-open socket** (all relay frames withheld, no close): the watchdog reset it at 60.0s, the next dial came 1.2s later, and both messages were recovered once each. - **Earlier run at `180dc9ae`, before the one-hour window, relay restart (1012):** the quiet channel resumed from last inbound − 960s. At this head that cursor is also capped at now − 3600s. The paged channel sent `limit:0`, with repair starting about 45ms after EOSE. Both messages posted during the bounce were delivered, including one backdated 600s. The first reconnect came about 8s after the close. buzz-review-completed 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Luke Tornquist <tornquist@squareup.com> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
5ea5ae1727 |
fix(desktop): always flash a linked message in the channel timeline (#8660)
## Problem When you follow a link to a message, the channel timeline should scroll to it and flash the row for about 2 seconds. On `main` the flash often does not appear: | How you reach the message | Flash on `main` | |---|---| | Same channel, row already on screen | yes | | Same channel, row loaded but off screen | **no** | | Same channel, row in older history not loaded yet | yes | | From another channel, row near the bottom | **no** | | From another channel, row further up | **no**, and the timeline stays at the bottom | The main timeline is always virtualized. `useAnchoredScroll` had three ways to scroll to a target there. Only one of them, "the row is already fully on screen", fired the flash. In the other cases the scroll returned without the flash, and the follow-up target effect marked the target handled without checking the result, so the flash never fired. Opening a link from another channel had a second problem. The row could look on screen while the channel's first bottom pin was still in flight in Virtua. The shortcut used a direct DOM `scrollTo`, which does not cancel that pin, so Virtua kept going to the bottom and took the target with it. Top-level message links also open the thread panel, and that resize settled the timeline at the bottom again. A third problem also exists on `main`. Centering a row near the top of the loaded history loads an older page. Opening a channel also starts a refresh of the newest channel window, and that refresh then drops the older pages from the head of the list. A head trim was not passed to Virtua as a `shift`, so the rows above the reader vanished with no compensation and the view fell to the bottom about 0.3–0.8 s after the flash started. The deep-link e2e test stopped checking the flash in #5306, so CI did not catch this. ## Change - In virtualized mode, every target scroll now goes through Virtua's `scrollToIndex` (centered). That replaces any bottom pin still in flight. Smooth scrolling is kept only when the row is already in the viewport. - Every target scroll that Virtua accepts now fires the flash. The highlight is keyed by message id, so a row that Virtua mounts after the scroll still shows it. - The scroll returns `true` once Virtua accepts the target and `false` while the target is not loaded yet. The follow-up target effect and the search retry now just read that value. This removes the "phase 1" special case and the duplicate DOM lookup. - A target scroll also clears the virtualizer's at-bottom state, so a resize right after it (for example the thread panel opening) does not pin back to the bottom. - Only an accepted target scroll retires bottom intent. It also cancels the hook's queued second mount-to-bottom frame. A target that is not loaded yet leaves the opening bottom pin alone while it retries. - `didPrependVirtualizedTimeline` now also admits a pure head trim (the new keys are an exact suffix of the previous keys), so Virtua's `shift` keeps the reading row in place when a window refresh drops older pages. The oldest loaded day has no divider while more history exists, so a head trim stays an exact suffix even when it cuts inside a day. The non-virtualized path (thread panel) is unchanged. ## Screenshots Before and after, from another channel, for a link to a row further up: https://github.com/block/buzz/pull/8660#issuecomment-6481877304 ## Testing Evidence at head `653d89766`: - New `desktop/tests/e2e/message-target-highlight.spec.ts`: a 5-row table with the cases above. Each case asserts that the target row is on screen **and** has the flash class at the same time, and that the row is still on screen 2.2 s later, after the flash ends. 15/15 passed (3 runs each). - On unfixed `main`, 3 of the 5 cases fail the flash check. - With only the head-trim change reverted, "same channel, loaded off screen" and "other channel, further up" fail the 2.2 s check. - `useAnchoredScroll.lifecycle.test.mjs`: an on-screen virtual target scrolls through the virtualizer, makes no direct DOM write, retires bottom intent and does not re-pin later. New: a target that is not loaded yet keeps the bottom pin. It fails with the old cancel order. - `virtualizedTimelineItems.test.mjs`: new rows for a head trim inside the oldest day and at its boundary (admitted, with the size cache aligned), and for a trim plus append and a middle removal (rejected). - `pnpm test` (6837 + 161 pass), `pnpm check` (tsc via pre-push, biome, px-text, pubkey truncation). - These e2e specs, run together: message-target-highlight, scroll-history, virtualization, timeline-no-shift, unread-pill, overscroll-boundary, dm-history-live-regression, live-broadcast-reply-timeline, message-copy-link, thread-focus-mode, thread-follow, thread-reply-anchor-roleplay, thread-unread, channels, messaging. 257 passed, 0 failed. Later commits at `b8e172672` add no production behavior. They move one helper above `computeAnchor`'s doc comment, and add a lifecycle test (from Cerveau's probe): a target that loads before the queued second mount frame is not scrolled back to the bottom. That test fails without the mount-frame cancellation. Pre-push `pnpm test` passed at `b8e172672`. Adversarial review by Auto (virtualizer and callers) and Cerveau (lifecycle, tests and thread panel). Their must-fix findings are fixed above, and both approved `653d89766`. Follow-up, not in this PR: after a jump to a row at the very bottom, centering is clamped, so no scroll event fires and the at-bottom state stays false until the next scroll. `main` has the same gap for `isAtBottom`. Not tested yet in the native app. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cerveau <fc91c5708d48aecea993d9219505dc841bc7b0222b7ed325b8edfd026bfb43b5@buzz.block.builderlab.xyz> |
||
|
|
eda26b9423 |
ci: repeat changed tests ten times on pull requests (#8613)
This ports the advisory flake check from block/buzz-app (https://github.com/block/buzz-app/pull/890) to this repository: pull requests repeat the tests they change ten times, with retries off, so a test that passes CI by luck is caught before it merges. ## What it does `flake-select` diffs the PR merge commit against its first parent (`HEAD^1..HEAD`, `fetch-depth: 2`) and maps changed hunks to tests per runner: a hunk inside a test body selects that test; a change anywhere else in a test file, or a new file, selects the whole file. One lane per runner then repeats only that selection 10× with retries disabled and writes a test × iteration table to its step summary. Lanes skip when nothing was selected, run only on `pull_request`, and are not read by any required gate in `ci.yml` — their own statuses carry the result. ## Per runner | Runner | Selection granularity | Repeat mechanism | |---|---|---| | Rust (cargo-nextest) | `crates/<crate>/<path>.rs::<module::test>` from `#[test]`-style attribute spans (comments and literals blanked first). A hunk outside any test body selects the file's tests. `#[ignore]`d and Postgres / `external_infra` tests are excluded. | One `cargo nextest run --profile flake` per package per iteration, with target flags derived from the path (`src/**` → `--lib --bins`, `tests/<stem>.rs` → `--test <stem>`), a filterset of exact `binary_id & test` pairs, and JUnit parsed per iteration. New `[profile.flake]`: `retries = 0`, `fail-fast = false`. | | node:test | Whole changed `desktop/src/**/*.test.mjs`, `*.jsdom-test.mjs`, `desktop/scripts/*.test.mjs` | 10× loop, each file alone, with the flag sets from `desktop/package.json` `test` | | Playwright `smoke` | `file:line` journeys from `test()`/`it()` spans (TypeScript AST) intersected with `playwright test --list --project=smoke`, deduplicated | Native `--repeat-each=10 --retries=0`, sharded `min(10, ceil(journeys / 5))` ways; `playwright-summary` tabulates the JSON report | | Flutter | Whole changed `mobile/test/**/*_test.dart` | 10× `flutter test --file-reporter=json` loop with the mobile lane's `--dart-define` | A runner whose selection exceeds 64 KiB of entries, or more than 250 Playwright journeys, is skipped with a note rather than overrunning its timeout. ## Differences from the buzz-app version - Lanes live in a reusable `_ci-flake.yml` called from `ci.yml`, matching this repo's `_ci-*.yml` convention, instead of inline jobs. - Playwright specs are parsed with the desktop app's own `typescript` package rather than `rolldown/parseAst`. - No Playwright `integration` project lane: it needs the relay artifact chain. Integration-only specs list nothing for `smoke` and the selection stays empty. - Playwright shards are capped at 10, the same bound as the regular smoke lane. - Rust entries carry the file and in-file module path, and the filterset is anchored to exact `(binary, test)` pairs, so a same-named test in another module is not repeated. - nextest target flags are derived from the changed file's path, so a `buzz-relay` change does not compile every test target of the crate. - A journey-count cap (250) skips the Playwright lane for selections ten repetitions could not finish within its 45-minute timeout. ## Heads-up - The Unit Tests `rust-cache` moves from `key: sherpa-cache-v1` to `shared-key: unit-tests-sherpa-cache-v1` so the flake lane can restore it read-only. This changes the cache key once: the first Unit Tests run after merge builds cold. - This PR itself changes no file the selector recognises (`scripts/flake-check.test.mjs` is a root script, run by the Desktop lane), so its own Flake Check lanes are expected to skip. ## Verification - `node --test scripts/flake-check.test.mjs` — all unit tests pass (also wired into the Desktop lane). - `actionlint` on the changed workflows; `biome check` on the two scripts. - `scripts/test-rust-cache-contract.sh` and its regression harness pass with the new shared key. - Synthetic-diff selection runs for each runner, plus `select` against this PR's own merge-base (empty, as expected). - 2-iteration repeats of a real selection on all four runners locally. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
d6196a16c1 | fix(relay): revoke invitations and restrict claims (#8237) | ||
|
|
6861e18a86 |
fix(desktop): keep FileCard inside narrow message rows (#8612)
## Summary Non-image attachment cards ran past their message row in the thread side pane, and the download icon was clipped off. `FileCard` is an `inline-flex` box, so it shrinks to fit its content and is never narrower than its min-content width. That width comes from the `truncate` (nowrap) filename. The only width cap was `max-w-sm` (24rem), so a long filename pushed the card out to the full 24rem even when the pane body was only about 300px wide (the default 380px thread pane). - Add `max-w-full`, so the cap becomes `min(24rem, 100%)`. The flex column ancestors already have `min-w-0`, so the percentage resolves against the pane width. The card shrinks, the filename gets an ellipsis, and the size and download icon stay visible. Wider surfaces (forum posts, DMs, wide timelines) look the same because 100% is wider than 24rem there. - Add a Playwright regression test in `file-attachment.spec.ts`. It seeds a text attachment with a long filename and opens its thread. It then checks that the card and its trailing icon stay inside the row and that the filename is actually truncated. Without the fix, the card's right edge lands about 82px past the row. ## Before / After | Before | After | | --- | --- | |  |  | Measured in the default 380px thread pane: before, the card's right edge sits at 1342px against a row edge of 1260px (+82px overflow); after, it sits at 1252px (8px inside the row). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Matt Toohey <contact@matttoohey.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
8216db1c71 |
chore(toolchain): bump Rust to 1.98.1 for macOS 27 proc-macro loading (#8598)
## Summary
Bump the pinned Rust toolchain from 1.95.0 to 1.98.1 so release builds
work on macOS 27 hosts.
Rust ≤ 1.97 strips Apple-target artifacts with its bundled
`llvm-objcopy`, which leaves the Mach-O `LC_SYMTAB` string pool
(`__LINKEDIT`) only 4-byte aligned. macOS 27's dyld refuses to `dlopen`
such images (`mis-aligned LINKEDIT string pool`). Because cargo applies
the release profile's default `strip = "debuginfo"` to proc-macro dylibs
too, `cargo build --release` of any crate that uses sqlx macros
(`buzz-db`, `buzz-relay`, `buzz-admin`, …) fails on macOS 27: rustc
cannot load `libsqlx_macros-*.dylib`. Dev builds pass because the dev
profile does not strip, which is why this only shows up as a
release/packaging failure.
The fix (llvm/llvm-project#203680, pulled into rustc by
rust-lang/rust#158410 for rust-lang/rust#157750) first shipped in Rust
1.98.0 and was never backported to 1.97.x. `block/buzz-app` pinned
1.98.1 for the same failure in block/buzz-app#529. With this bump the
`CARGO_PROFILE_RELEASE_BUILD_OVERRIDE_STRIP=none` / custom-linker
workarounds people have been using locally are no longer needed.
### What changes
Two commits: the toolchain bump (with the desktop clippy fixes), then
the workspace clippy fixes.
| Location | Change |
|---|---|
| `rust-toolchain.toml` | `channel = "1.95.0"` → `"1.98.1"` (profile
unchanged) |
| `Dockerfile`, `Dockerfile.push-gateway` | `ARG RUST_VERSION=1.95` →
`1.98` |
| `Dockerfile.sprig` | `rust:1.98-alpine3.22` with the manifest-list
digest verified on Docker Hub (shared by the `1.98` and `1.98.1` tags) |
| `.github/workflows/release.yml` | `dtolnay/rust-toolchain` pinned to
the tip of its `1.98.1` branch (`18f04874`) for the Windows desktop
release job |
| `benchmarks/harbor-buzz-orchestra` | `RUST_IMAGE = rust:1.98-alpine` |
| `_ci-rust.yml`, `windows-canary.yml`, `app_state_tests.rs` | stale
"1.95" comments reworded; the `unknown_lints` allow stays with a
version-agnostic rationale |
| `desktop/src-tauri` | lints clippy 1.98 newly raises under `-D
warnings`: `chunks_exact_to_as_chunks` at five fixed-width PCM/UTF-16
decoders, `needless_borrows_for_generic_args` on the `Copy` `norm`
closure in `config_bridge/reader.rs`, and a redundant `Engine as _`
import in `hpke_key_backup_tests.rs` that `use super::*` already
provides |
| Workspace crates (9) | the 30 lints clippy 1.98 newly raises in `cargo
clippy --workspace --all-targets -- -D warnings`, listed below |
Workspace lint fixes:
| Lint | Sites | Fix |
|---|---|---|
| `result_large_err` (20) | `buzz-relay` `api/git/transport.rs` (8),
`api/git/settings.rs` (6), `api/gifs.rs`, `api/workflows.rs`,
`api/profiles.rs`; `buzz-push-gateway` `verify_installation_assertion` |
Clippy 1.98 now checks `async fn`s, so every `async fn … -> Result<_,
axum::response::Response>` trips it. These follow the existing allow
pattern in `api/media.rs` and `nip_fi_http.rs` ("Response is the natural
error type for axum handlers"): module-level in the two git files,
per-function elsewhere. Boxing `Response` would touch every handler for
no runtime benefit. |
| `chunks_exact_to_as_chunks` (3) | `buzz-media` MP4 brand scan;
`buzz-voice` npy f32 reader and stereo test fixture | `chunks_exact(N)`
→ `as_chunks::<N>().0` |
| `question_mark` (3) | `buzz-acp` queue flush fallback and
slash-command mention skip; `buzz-agent` `find_git_root` | `match … {
None => return None }` → `?` |
| `useless_format` (2) | `buzz-auth` nip_fi startup test policies |
`format!("…")` → `"…".to_string()` |
| `useless_borrows_in_formatting` (1) | `buzz-pairing-cli` secret print
| drop the borrow |
| `manual_noop_waker` (1) | `buzz-sdk` broker test `block_on` |
`Waker::noop()` (stable since 1.85) |
All `as_chunks` rewrites keep `chunks_exact`'s remainder-dropping
semantics, and `as_chunks` is stable since 1.88, which is the MSRV.
One change in `api/git/transport.rs` is not a lint fix. Staging that
file trips the pre-commit secret scanner on its existing test-only ES256
fixture (`GIT_TEST_EC_PEM`), and the scanner only honours a suppression
marker on the flagged line. So the `\`-continued string literal is
rewritten as a byte-identical `concat!` whose `BEGIN` line carries
`sadscan:disable kingfisher.privkey.2`.
Left as is on purpose: `Cargo.toml` `rust-version = "1.88.0"` (MSRV
floor, a separate concern), the README/CONTRIBUTING "1.88+" text, Hermit
(no Rust packages there), the lockfiles, and the `(1.95.0)` comment in
`scripts/start-isolated-test-relay.sh` (the pre-commit secret scanner
flags that file's pre-existing dev-only `DATABASE_URL` whenever it is
staged).
### Related issue
Fixes #8074
### Testing
All under rustc/clippy 1.98.1 on a macOS 27 host (Darwin 27.0.0,
aarch64), with no `RUSTFLAGS` / `SDKROOT` / strip overrides in the
environment:
- `cargo build --release -p buzz-db` (the #8074 repro: the release
profile strips the proc-macro dylib): **builds**. The resulting
`libsqlx_macros-*.dylib` (14 MB) has `LC_SYMTAB stroff % 8 == 0` per
`otool -l`. The same build on 1.95.0 died with `mis-aligned LINKEDIT
string pool`.
- `just clippy` (workspace, `-D warnings`) and `cargo fmt --check`
(workspace and `desktop/src-tauri`): pass
- `just desktop-tauri-clippy` (default and `mesh-llm` feature sets):
pass
- `just test-unit`: every crate passes. The `buzz-acp` binary failed
only under pre-push lane contention; rerun solo it is 1060 passed, 0
failed.
- `just desktop-tauri-test`: 3422 passed, 0 failed
- After the lint fixes: lib/bin unit tests for `buzz-acp`, `buzz-agent`,
`buzz-auth`, `buzz-media`, `buzz-voice`, `buzz-sdk`, `buzz-pairing-cli`,
and `buzz-relay`'s `api:
|
||
|
|
a38fedb234 |
fix(desktop): recognize agents without NIP-OA in Huddles (#8544)
🤖 ## Summary In a Huddle (a voice call in a channel), Buzz Desktop decides which participants are agents. It uses that answer to choose whose text it speaks aloud, which agents can publish voice, and when the last person leaves and the Huddle ends. Desktop currently treats a participant as an agent only when its channel role is `bot`. #8494 changes the relay so that it gives `bot` only to agents with a verified owner attestation (NIP-OA). After that change, agents without an attestation join as ordinary members, and Desktop treats them as people: - their messages are not spoken, - their voice publisher is refused, - the Huddle stays open after the last person leaves while such an agent is still connected. With this PR, Desktop also counts a member as an agent when a trusted person has published a managed-agent record for it. Desktop already publishes such a record for every agent it runs, on every machine. Trusted people are: - **you**, so your own agents count as agents on all of your machines; - **the Huddle's owner**, usually the person who started it, so the agents they added count as agents for everyone who joins. Some agents without an attestation still count as people: an agent added by someone who does not own the Huddle, as seen from any machine other than that person's. The only way to recognize those agents is an owner attestation. This PR does not sign one. ### Details - `huddle/roster.rs` (`fetch_huddle_roster`) is now the only place that decides agent or person for Huddles. A member is an agent if its role is `bot`, or if the current user or a channel `owner` published a kind:30177 managed-agent record whose `d` tag is that member's key. Records from other authors are ignored. Records whose signature does not verify are ignored. - All Huddle checks use the roster: voice-publisher authorization, `get_huddle_agent_pubkeys` (spoken-text gating and the participant list), the periodic roster refresh, adding an agent, cleanup at the end of the Huddle, and the last-person check. - Trusting the owner's records is a compatibility policy for Huddles, not proof of identity. A record creates no owner. It lets the channel owner mark a member as an agent for every Huddle behavior that reads the roster: spoken text, voice publishing, remote transcription and turn-taking (both skip agent audio), cleanup, and the last-person check. Before #8494 the client chose the role, so an owner could already do this by labeling a member `bot`. - If the managed-agent records cannot be read, Desktop does not drop agents that have the `bot` role. Spoken text, voice publishing, adding agents and cleanup keep working for those agents. The Huddle does not end automatically, because an agent without an attestation could be counted as a person. When you join, and on each periodic refresh, Desktop adds the `bot` agents to its stored list and keeps stored agents that are still members. ### Related issue Follow-up to #8494 (the relay stores `bot` only for attested agents). No existing issue found. ### Testing - New helper-level unit tests in `huddle/roster.rs`. They do not run the relay query, the pipeline writers or the leave path. - the `bot` role, the user's own record and the owner's record each mark a member as an agent; - records from other members are ignored; - a record with a forged signature is ignored; - when the record read fails, `bot` agents stay agents, the Huddle cannot end automatically, and the next successful read restores the full roster. - when the record read fails, a joiner with no stored agents still stores the `bot` agents, an agent known only by its record stays stored, and a stored agent that left drops out. - New tests in `huddle/roster_hydration_tests.rs` run the real `post_connect_setup` and hot-start entrypoints against a fake relay whose managed-agent record read fails. The voice-model, audio and pipeline steps are passed in, so the tests replace them. Post-connect stores the `bot` agent before media setup starts; the hot-start refresh also keeps a still-member agent known from an earlier record and drops one that left; the same incomplete roster gives no human count for the last-person check. The tests fail when either entrypoint skips its roster read, and when the roster code accepts only complete rosters. - `cargo test --lib huddle::` (Huddle tests only), `cargo clippy --tests -D warnings` and `cargo fmt --check` pass in `desktop/src-tauri`. - Not yet tested in a live Huddle. --------- Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
d0dee1cb50 |
feat(relay): add several people with one kind:9000 (#8575)
## What
A NIP-29 kind:9000 (put-user) can now name up to 100 people, one `p` tag
each. A client sharing a channel with many people can do it in one
signed write instead of one event per person.
- **Role per person:** NIP-29 style `["p", pubkey, role]`. When the
third element is absent or empty, the event's `["role", r]` tag applies.
- **Same checks, per person:** each person gets the checks a single add
does, in the same order: `decide_put_user`, then the agent's
`channel_add_policy`, then the admission rules again under the channel's
membership lock. One refused person no longer refuses the others.
- **Per-person results:** when the event names two or more people, the
OK message reports each result. Only non-empty lists appear, and
refusals are grouped by reason so 100 people stay well under a client's
16 KB receipt limit:
```
members:
{"added":[hex…],"updated":[hex…],"already_member":[hex…],"rejected":[{"reason":"…","pubkeys":[hex…]}]}
```
- **When the event is refused:** if it is malformed (bad `p` hex,
unknown role, more than 100 people, missing `h`, archived channel), or
if nobody it names may be added. With one person the message keeps
today's `invalid: <reason>`. With several it is `invalid: members: {…}`.
- **One-person events don't change:** a successful single add still
returns an empty OK message, which an existing e2e test pins.
- **Re-adding someone at the role they already have** no longer posts a
second `member_joined` timeline notice. The member-added notification
(kind:44100) still goes out, so an agent that missed its first one is
still re-signaled.
- **Group-state events (39000–39002)** are now published once per event
instead of once per person.
## Cost of a batch
The database work for one event does not grow with the number of people:
- **One membership transaction.** `buzz-db` gains `put_members`: one
transaction, one membership lock, one channel read, one read of every
involved role, the owner count only if a demotion needs it, and one
`UNNEST` upsert with `RETURNING`. `put_member` is now `put_members` with
one person, so there is one implementation of the admission and
last-owner rules, and a single add makes two fewer round trips than
before.
- **Same answers as one write per person.** People are decided in tag
order, and each decision sees the roles earlier people were given,
including the actor's own (an owner who steps down mid-batch can't then
grant admin), and the owners they add or remove. A new buzz-db test pins
this.
- **One policy query.** Validation reads every third-party target's
`channel_add_policy` with one `= ANY($2)` query
(`get_agent_channel_policies`) instead of one per person.
- **One channel-type read** for all the member-added notifications,
instead of one per notification.
What still scales with people is what each person must receive: their
`member_joined` timeline notice and their kind:44100 notification, one
stored event each.
## Why
This is for Solo chat sharing in buzz-app.
[block/buzz-app#1605](https://github.com/block/buzz-app/pull/1605)
routes every share through one helper, `shareChatsWithChannel`, which
today sends one kind:9000 per missing member. A small follow-up there
switches it to one batch, then re-reads the roster and adds anyone still
missing one at a time. That keeps it correct on relays without this
change, which accept the event but ignore every `p` after the first.
The batch stays a signed NIP-29 event rather than a new HTTP endpoint,
per VISION.md and AGENTS.md ("Prefer Nostr events over new HTTP
endpoints").
## Wire changes for existing senders
- Any malformed `p` tag now refuses the event. Before, malformed `p`
tags were skipped and the first valid one was used.
- Extra `p` tags are now targets. Before, they were ignored.
- A third `p` element that isn't a role, such as a NIP-01 relay hint,
refuses the event with `invalid role: …`. No sender in this repo does
that: `build_add_member` sends one bare `p`.
## Tests
**buzz-db (`channel_members` Postgres tests):** new
`put_members_decides_each_person_in_order`: the sole owner can't step
down before an heir is named in the same call, but the heir is still
added; with the heir counted the owner can step down, and the admin
grant after it is then refused; a no-op re-add reports its prior role; a
repeated pubkey refuses the call. The other `channel_members` Postgres
tests pass, including the announce-admission and concurrent `join_role`
tests that exercise `put_member`.
**buzz-relay (`put_user_postgres_tests.rs`), 5 tests:**
- mixed results in one event: added, already a member, an agent with
`nobody` policy rejected, a per-`p` `bot` role, a duplicate `p`, a
single roster publish
- `updated` for a role change, with the event `role` tag as fallback
- a plain member may add someone, but may not grant an elevated role on
a `p`
- a single add keeps its empty OK message
- an all-refused event: single text and grouped JSON
- a malformed `p` tag
- the 100-person cap: 101 refused, 100 accepted
Related relay Postgres groups (`group_state`, `create_with_members`,
`edit_metadata`, `membership`) pass.
**Live relay:** `e2e_relay` against a local relay at 9c27a0f15: 58 of 61
passed, including `test_nip29_batch_put_user_wire` (added to the CI
Relay E2E list) and every NIP-29 put-user, announce, guest and departure
test in that list. The 3 failures don't touch this change: the two
invite tests assume the relay's seeded host, and this relay ran on
`:3030`. `test_unarchive_emits_member_added_notification` timed out once
and passed when re-run, as it did on the first commit.
**Local caveats:** the local `channel_members` roster-fence test and the
deletion tests fail on a shared local database because other tests leave
its fence trigger disabled, so the PostgreSQL Tests job in CI is the
authority for the full suite. The pre-push `rust-tests` hook passed.
`desktop-tauri-checks` failed on `chunks_exact` lints in desktop files
this PR doesn't touch, so the push used `--no-verify`. `cargo clippy
--all-targets` (buzz-db, buzz-relay) and `cargo fmt --check` are clean.
---------
Signed-off-by: Bradley Axen <baxen@squareup.com>
Signed-off-by: Luke Tornquist <tornquist@squareup.com>
Co-authored-by: ART <ea39c1c8acf57180161ef4ba4ed68abf8589f5291956b909a5aa991fc66664ca@buzz.block.builderlab.xyz>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: claude00 <fb8b76a01257932b8ca35060862c29bd09b2137638573731c2d5074fc02c5385@buzz.block.builderlab.xyz>
Co-authored-by: mbot <3754f8729004d95654c46dbab3129e4ab9ef05cc2534e2a3fbfc155983bd637b@buzz.block.builderlab.xyz>
Co-authored-by: Luke Tornquist <tornquist@squareup.com>
Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz>
|
||
|
|
9e8c9fc04d |
fix(deps): bump ctutils to 0.4.5 for RUSTSEC-2026-0385 (#8560)
🤖 ## Summary On 2026-10-23, RustSec published RUSTSEC-2026-0385 against `ctutils`, a constant-time helper crate used by the RustCrypto hashing and HMAC crates. Some of its functions were meant to run in constant time, so their timing would reveal nothing about secret values, but in versions before 0.4.4 they could compile to code that branches on those values. Since the advisory came out, `cargo-deny check` reports advisories as failed. That breaks `Security Domain / Security` on `main` and on every PR that runs the Rust security job. This PR changes `ctutils` from 0.4.2 to 0.4.5 in the three lockfiles that pin it: `Cargo.lock`, `desktop/src-tauri/Cargo.lock`, and the `buzz-feature-flags` relay-feature-selection test fixture. No other packages change. | Advisory | Crate | Change | |---|---|---| | [RUSTSEC-2026-0385](https://rustsec.org/advisories/RUSTSEC-2026-0385) | `ctutils` (non-constant-time mask generation and `u128` comparisons) | 0.4.2 → 0.4.5 | ### Related issue N/A. I searched open and closed PRs for `ctutils` and `RUSTSEC-2026-0385` and found none. ### Testing - `cargo deny check advisories`: `advisories ok`. On `origin/main` the Security job fails on this advisory. - `cargo check --workspace` passes. - `cargo test -p buzz-feature-flags --test relay_feature_selection_contract` passes. This test compiles the fixture with `--locked`. - The pre-push hook's Rust and Tauri lanes pass. Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
76d792f288 |
BUZZ-357: feat(cli): token-lean agent output format for event reads (#8234)
### Why?
Agents read channel history with `buzz messages get/thread`. Every
returned event includes its 128-hex `sig`. Every agent-authored message
also carries a NIP-OA `auth` tag, which is another owner pubkey and a
128-hex signature. An agent can't use either of these, but pays for them
on every history read. Measured with Claude's tokenizer, that is about
88 tokens per human message and about 180 per agent message, so a
history read shrinks 33% for short human messages and 40% once agent
messages are in the mix (details in the comment below). The existing
`compact` format is fixed at `{id, content, created_at}` (#6884), which
drops the author, tags and reply target, so agents can't use it.
### What?
- **New `--format agent` for event reads.** Applies to `messages
get/thread/search` and `feed get`.
- Drops `sig`, and trims the NIP-OA `auth` tag to `["auth", <owner>]` so
agents can still tell an author is an agent and who owns it.
- Folds the NIP-10 `root`/`reply` tags into a `reply_to` field, using
the same resolver as relay ingest, plus `thread_root` on nested replies.
That's the value `messages send --reply-to` takes.
- Keeps every other tag exactly as `json` output shows it: channel,
mentions, edit targets, diff metadata.
- Non-event reads return the same output as `json`.
- **Default for agents.** Without `--format`, the CLI falls back to
`BUZZ_OUTPUT_FORMAT`; an empty or unknown value means `json` with a
warning, so a harness/CLI version mismatch never breaks commands.
buzz-acp sets it to `agent` for every agent runtime through
`default_agent_env` and forwards the effective value in the explicit MCP
server environment, so operator or persona env still wins, and `--format
json` still returns full signed events. buzz-agent adds the variable to
its MCP env allowlist.
- **Docs and cleanup.** The managed-agent skill doc (version bumped to
7) and `AGENTS.md` describe the new default. The two duplicate
`format_events` copies in `messages.rs` and `feed.rs` are now one
`commands/event_output` module, which also does the NIP-10 tag parsing.
The `json` and `compact` output contracts are unchanged.
### Example output
https://github.com/block/buzz/pull/8234#issuecomment-6259679347
### Testing
Started `buzz-acp run` with the deterministic task-agent fixture, then
passed its `session/new` MCP environment to Codex 0.163.0 (`codex
app-server`). With no format override the MCP child received `agent`;
operator overrides of `json` and `compact` reached the MCP child
unchanged. The probes used temporary local sessions and made no model
requests.
### Related
Same change on the other agent read surfaces: each drops `sig`, trims
`auth` to `["auth", <owner>]` and keeps thread context.
- block/buzz-app#1388: buzz-app MCP read lines (text, `claimed
owner`/`reply_to`)
- squareup/cash-server#139132: hosted agents' read tools (JSON events,
raw NIP-10 tags)
Generated with Codex
---------
Signed-off-by: Nikolaj Goodger <ngoodger@squareup.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
||
|
|
427488e2fc |
fix(desktop): surface a missing DATABRICKS_HOST instead of loading models forever (#8504)
## Summary If the `databricks` / `databricks_v2` provider is selected but `DATABRICKS_HOST` isn't set, the Model field stays on "Loading models..." forever and never shows an error. How it happens: 1. Desktop discovers Databricks models in-process only when `DATABRICKS_HOST` is set. Without it, discovery falls back to `buzz-acp models`. buzz-agent then fails at config time with `DATABRICKS_MODEL required` (model is checked first) or `DATABRICKS_HOST required`. 2. `formatModelDiscoveryErrorStatus` maps those errors, and `BUZZ_AGENT_PROVIDER is required`, to a `null` status. The idea was that the field's Required row would explain the problem. 3. `deriveModelDiscoveryPending` treats "no data and no status" as still pending. So a failed discovery with a `null` status looks like it is still loading. The fix: - `formatModelDiscoveryErrorStatus` now always returns a status, and its return type is no longer nullable. A failed discovery therefore always settles. - A missing Databricks host shows: "Set DATABRICKS_HOST to your Databricks workspace URL under Advanced to load Databricks models." - A missing provider shows a muted "Choose a provider to load models." Builds that bake `DATABRICKS_HOST` in never reach this path. ### Screenshots Settings → Agents → Agent defaults, with Buzz Agent + Databricks v2 selected, no `DATABRICKS_HOST`, and model discovery failing with `DATABRICKS_MODEL required`. Captured from the desktop e2e build with the mock bridge. The only difference between the two builds is `personaModelDiscoveryStatus.ts`. | Before | After | |---|---| |  |  | ### Related issue None found. Searched open PRs that touch `personaModelDiscoveryStatus` / `usePersonaModelDiscovery`, and issues mentioning "loading models". ### Testing - New jsdom test drives the real `usePersonaModelDiscovery` hook with a rejected `DATABRICKS_MODEL required` discovery. It asserts that loading settles and that the status names `DATABRICKS_HOST`. On `main` the test fails (`modelDiscoveryLoading` stays `true`), and it passes with this change. - Updated unit tests in `personaModelDiscoveryStatus.test.mjs`. - `pnpm test` in `desktop/` passes (6834 + 161 tests). `tsc --noEmit` and biome are clean. - Exercised in the desktop e2e build with the mock bridge (screenshots above). Not yet exercised in the real Tauri app. Signed-off-by: Fizz <6be7be23ec5bba868f3e1db5ec86013d76bd1a293dce5b435a192158fec13641@buzz.block.builderlab.xyz> Co-authored-by: Fizz <6be7be23ec5bba868f3e1db5ec86013d76bd1a293dce5b435a192158fec13641@buzz.block.builderlab.xyz> |
||
|
|
8bbfe44f77 |
fix(deps): bump webpki crates for RUSTSEC-2026-0381/0382/0383 (#8499)
## Summary On 2026-10-19, RustSec published three advisories against the TLS certificate crates in our lockfiles. Since then, `cargo-deny check` fails on advisories. That breaks `Security Domain / Security` on `main` and on every PR that runs the Rust security job. | Advisory | Crate | Change | |---|---|---| | RUSTSEC-2026-0382 | `rustls-webpki` (distrusted intermediates) | 0.103.15 → 0.103.16 | | RUSTSEC-2026-0381 | `webpki-roots` (distrusted emSign roots) | 1.0.7/1.0.8 → 1.0.10 | | RUSTSEC-2026-0383 | `webpki-root-certs` (same roots) | 1.0.7/1.0.8 → 1.0.10 | This PR bumps all three in `Cargo.lock` and `desktop/src-tauri/Cargo.lock`. Only these packages change. `webpki-roots 0.26.11` stays because `tokio-tungstenite` 0.28/0.29 (via axum and nostr-sdk) still require `^0.26`, and no patched 0.26 release exists. 0.26.11 is a semver-trick shim that bundles no certificates of its own. It re-exports `webpki-roots` 1, which now resolves to the patched 1.0.10. `deny.toml` ignores RUSTSEC-2026-0381 for that shim and explains why. The ignore should come out once those consumers move to `webpki-roots` 1. ### Related issue N/A. I searched for open PRs bumping these crates and found none. ### Testing - `cargo-deny check`: `advisories ok, bans ok, licenses ok, sources ok`. On `origin/main` it reports `advisories FAILED`. - `cargo check --locked -p buzz-ws-client -p buzz-cli -p buzz-relay` passes. - The pre-push hook's Rust unit tests pass. Signed-off-by: Fizz <6be7be23ec5bba868f3e1db5ec86013d76bd1a293dce5b435a192158fec13641@buzz.block.builderlab.xyz> Co-authored-by: Fizz <6be7be23ec5bba868f3e1db5ec86013d76bd1a293dce5b435a192158fec13641@buzz.block.builderlab.xyz> |
||
|
|
5fefec604f |
fix(mobile): restore channel icons and align chat colors with desktop (#8478)
## Summary - Restores `#` and lock icons for public and private channels on Home, with matching loading placeholders. - Updates the dark-mode navigation colors to the desktop design-system tokens (read and unread text, section headings). - Replaces the chat color picker with the seven desktop accents (Gray, Chartreuse, Flame, Magenta, Iris, Cobalt, Jade). Saved legacy colors map to the closest one. - Uses white text in your own bubbles, and black where white falls below 4.5:1 (Chartreuse, dark-mode Gray). Links (underlined), quotes, tables and code use the same text color. - Insets code blocks in bubbles evenly, with corners concentric to the bubble and even inner padding. - The swipe-to-reply indicator uses the chosen chat color. ## Testing - `flutter analyze` and `dart format` are clean. The full mobile suite passes (3,705 tests). - Installed and checked on a Pixel 10 and an iPhone Air against a real community. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> Co-authored-by: Terp <5856f6900e8e0a4e0ea1f4f8d2ee33e4753d2119bc122f94ecafc660b46c0511@buzz.block.builderlab.xyz> Co-authored-by: Queue <874bf2d815c2c4b459b90438c669c57f2702f3a76bb6e16fa97184259aac3e9e@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
0b53fc699d |
test(desktop): read only the open effort menu in effort-picker e2e (#8476)
## Summary
`menuValues()` in `claude-effort-picker.spec.ts` read every
`menuitemradio` on the page right after it opened a menu. Dropdown menus
stay mounted for their 100ms exit animation
(`POPOVER_RADIX_MOTION_CLASS`). That means the model menu, which
`pick()` had just closed, could still be in the DOM, and its rows ended
up in the effort list:
```
- Expected - 0
+ Received + 6
+ "Default model (Opus)", "Opus", "Claude Sonnet 5", "Haiku", "Claude Opus 4.8", "Custom model...",
"Adapter default", "max",
```
The helper now waits for the trigger to report `aria-expanded="true"`,
then reads rows only from the menu named in its `aria-controls`. The fix
only touches the test; no product code changes.
### Related issue
The flake was introduced with the spec in #8059. In CI it failed
`Desktop Smoke E2E (2)` on #8473 (all three attempts,
[job](https://github.com/block/buzz/actions/runs/39336482034/job/118347673219))
and failed once before passing on retry on #8474.
### Testing
- **Forced repro.** I temporarily stretched the closed-menu exit
animation to 1500ms using `page.addStyleTag` (not committed):
- Before: 5/5 failed with the same `toEqual` diff.
- After: 5/5 passed.
- **Natural repeats.** I ran `--repeat-each` locally with 3–4 workers,
100 runs on each version:
- Before: 3 failures from the menu-row leak.
- After: 0.
- Both versions also had the same local-only startup timeouts (`waiting
for getByTestId('open-agents-view')`), which this change doesn't affect.
- **Full spec.** `pnpm exec playwright test --project=smoke
tests/e2e/claude-effort-picker.spec.ts`: 27 passed.
- `biome check` is clean.
Signed-off-by: Fizz <6be7be23ec5bba868f3e1db5ec86013d76bd1a293dce5b435a192158fec13641@buzz.block.builderlab.xyz>
Co-authored-by: Fizz <6be7be23ec5bba868f3e1db5ec86013d76bd1a293dce5b435a192158fec13641@buzz.block.builderlab.xyz>
|
||
|
|
139f8903d5 |
fix(desktop): set window title to "Buzz" for Windows taskbar (#6317)
## What
The Tauri window title field in tauri.conf.json was an empty string. On
Windows, the taskbar draws its label from the window caption, so the
Buzz entry rendered as an icon with no name.
## How
Set app.windows[0].title to "Buzz" in desktop/src-tauri/tauri.conf.json
(line 19).
This is the only change — one line in one file. The hiddenTitle: true
field on line 26 only affects the macOS title bar and does not interfere
with the Windows taskbar label.
## Before / After
| | Before | After |
|-----------------|--------------------|---------------|
| Windows taskbar | Icon only, no text | Icon + "Buzz" |
## Testing
- Verified the JSON is valid after the change
- hiddenTitle: true is unchanged — macOS behavior is unaffected
Fixes [#6295](https://github.com/block/buzz/issues/6295)
Signed-off-by: Fatima Nur <fatimanur424@example.com>
Co-authored-by: Fatima Nur <fatimanur424@example.com>
|
||
|
|
e73ba22e15 |
fix(desktop): release the live tail after a single scroll back to bottom (#8465)
## Summary
This fixes the Desktop Smoke E2E failure that turned `main` red at
|
||
|
|
6f67dcdac4 |
fix(desktop): bump zip to 9 for RUSTSEC-2026-0367/0368/0369 (#8412)
🤖 Bumps the desktop app's direct `zip` dependency from 8 to 9 to clear RUSTSEC-2026-0367, RUSTSEC-2026-0368 and RUSTSEC-2026-0369 on the managed Node.js archive extraction path. zip 9 makes `ZipArchive::name_for_index` and `ZipFile::name` fallible. Managed Node.js zip validation now rejects an entry whose name can't be decoded, and the "unsafe path" error prints the raw entry name lossily. The absolute-path, drive-prefix, traversal and `enclosed_name()` checks are unchanged. The lockfile update is scoped to `zip`; it pulls in newer `zstd`, `lzma-rust2` and `ppmd-rust` that zip 9 requires. **`zip` 8 is still in release builds.** With `--features mesh-llm`, `mesh-llm` v0.76.2 (`mesh-llm-system`, `mesh-llm-plugin-manager`, `mesh-llm-host-runtime`) still depends on `zip` 8.6.0. That goes away when the `mesh-llm` pin moves to a release that includes Mesh-LLM/mesh-llm#2471. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
7b11c6147d |
test: fix recurring CI flakes in desktop and Postgres lanes (#8343)
Fixes the CI flakes that failed most often across `main` and unrelated branches from Oct 5 to Oct 14. The counts come from 150 failed `CI` runs. Every change except the nextest config is test-only. | Flake | Runs hit | Root cause | Fix | |---|---|---|---| | `probe_codex_acp_version_…descendant_holds_pipe_open` | 5 | The test writes its script in-process and then execs it. If a sibling test spawns a process in that window, the child inherits the open write fd and Linux rejects the exec with `ETXTBSY`. The probe swallows that error as `None`. | Write probe scripts from a short-lived `/bin/sh` (`write_executable_script`) | | `empty-edit-delete.spec.ts` (`:51`, `:74`, `:95`) | 11 | The seeded `mock-general-welcome` id isn't hex, so `formatTimelineMessages` can't overlay its edit. The post-subscribe window refetch then reverts the row. | Emit a per-test message with a real id after the exact channel subscription is live | | `huddle-transcription.spec.ts:798` | 9 | The composer's delayed initial autofocus (TipTap `focus()` runs in rAF) lands after the voice popover opens and dismisses it. | Wait for `message-input` focus before opening the popover | | `voice-note.spec.ts:906` | 6 | The fixture `ping.mp3` is about 1s long, so playback can end before `paused === false` is observed. | Loop the fixture audio | | `workflows.spec.ts:291` | 5 | The `Message text` fill runs before the inspector swaps to the new step, so it fills the trigger's condition instead. | Scope the fill to the step inspector after its `Action` menu appears | | `cluster_global_probe_commits_tokens_and_sessions_prove_coverage` | 3 | The `max-threads = 1` group only serializes these tests with each other. Other tests' sessions still mask cluster-wide `pg_stat_activity` (`MaskedActivity`). | `threads-required = "num-test-threads"` | | `test_usage_metrics_lock_has_single_owner_and_releases_on_drop` | 1 (this PR) | The test retries the lock right after dropping the leader. Postgres releases a session advisory lock only after the backend notices the disconnect, so the retry can run first. | Poll for the successor, bounded at 5s | ### Validation - **ETXTBSY:** a Linux experiment with Rust 1.95 using the same write→chmod→spawn pattern under concurrent spawns hit `ETXTBSY` 4–7% of the time. Using the `sh` writer, it hit 0 of 6,000. I did not catch it inside the full suite locally, so this fix is based on the mechanism, not an in-suite repro. All 11 probe tests pass on Linux and macOS. - **empty-edit-delete:** the baseline failed 16 of 45 runs (`--repeat-each`, 6 workers, no retries). The spec fix alone brings that to 4/45. The remaining failures come from a separate product race, described below. - **huddle:** the baseline failed 5 of 10 runs. With the fix, 60 of 60 passed without retries. - **nextest:** checked with a throwaway crate on 0.9.136. A grouped test with `num-test-threads` ran alone, even with `max-threads = 1`. - I have not stress-run the voice-note and workflows fixes; they rely on CI. ### Not in this PR - **Product bug in `useChannelLinks`:** the debounced `#channel` autocomplete close lets Enter accept a stale suggestion for about 120ms. That accounts for the remaining `empty-edit-delete` failures, and it affects users too. With the spec fix plus a synchronous close, the spec passed 120 of 120 runs. This needs a product decision, so it's held for a separate PR. - **Composer autofocus race:** the delayed autofocus can close an overlay the user just opened. The real fix is to revalidate when the delayed focus runs. Overlaps #6530. - **`thread-head-stale-edit.spec.ts:55`:** a forced click on a pointer-events-none action bar is suspected, but I couldn't reproduce it. - **Remaining ETXTBSY exposure:** about 40 other Rust tests write and then exec scripts, and 17 specs still edit the non-hex seed message. Root-cause lanes by Princess Donut, Mongo and Mordecai. Carl integrated and verified them. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Co-authored-by: Princess Donut <5d97ac8c272fa949af56c71e586146b1706e5c4c3daa125db5b4625e12e0686c@buzz.block.builderlab.xyz> Co-authored-by: Mongo <81cabd2ca1792494372ed410283c00770c0cf35afca052e940fa522883212b45@buzz.block.builderlab.xyz> Co-authored-by: Mordecai <f314b6033b6e13df94c8e9e1187d3666b6b9ce54e66ee986e31cdaa909e8dcbc@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
67c4fe5e17 |
fix(deps): bump rustls to 0.23.46 for RUSTSEC-2026-0346 and -0347 (#8319)
🤖 ## Summary The required Security check now fails on every branch, including main. The dependency audit (`cargo-deny`) reports two new advisories for rustls 0.23.45, the TLS library behind the relay, CLI and desktop connections: - **RUSTSEC-2026-0346:** printing a TLS client session value with `Debug` (for example, in a log line) shows its TLS secret. - **RUSTSEC-2026-0347:** when a server rejects Encrypted Client Hello (ECH, which hides the server name a client asks for), data the client buffered can go to the outer server. rustls 0.23.46 fixes both. This PR moves the root and desktop lockfiles to it, so the Security check passes again. ### Related issue None found. #8277 fixed an earlier advisory in the same way. ### Testing - `cargo-deny check` passes: "advisories ok, bans ok, licenses ok, sources ok". - `cargo tree --locked` resolves both lockfiles without more changes. Only the rustls version and checksum change. A plain `cargo update -p rustls` also moved unrelated `syn` and `getrandom` edges, so this PR leaves those out. Signed-off-by: Logan Johnson <loganj@squareup.com> Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz> |
||
|
|
5dc693aeaf |
Extract shared Huddle audio client for desktop consumers (#8129)
Huddle audio was embedded in the v0 desktop client, forcing other clients to duplicate its protocol and codec behavior. This extracts `buzz-audio-client` so v1 can depend on a pinned library revision without building the v0 application. The crate owns the bounded v2 audio handshake, roster parsing, wire format, Opus sender, and per-peer NetEQ jitter buffer. The v0 human and agent senders use it, and the desktop retains room admission, identity/signing, TLS policy, device routing, mute, and teardown. Receive decoding preserves v1's support for 120 ms Opus packets. No relay protocol changes, speech models, or model downloads are included. Docker builder stages include CMake because cargo-chef compiles the workspace dependency graph. Dependency security: Opus 0.4 replaces the unmaintained `audiopus_sys` backend with `opusic-sys`. NetEQ is pinned exactly to 0.8.3 because its published Rust sources never reference its declared ringbuf dependency. RUSTSEC-2026-0293 is documented as unreachable for this audited source, and a wrapper restriction rejects every other consumer of vulnerable ringbuf versions. Independent review confirmed the scope; re-audit/remove the exception when changing NetEQ. NetEQ 0.9.1 still declares ringbuf 0.3, so a version bump alone does not remove it. Local `cargo deny check` passes with this constrained exception. Validation: - All 21 shared-crate tests pass, including real loopback WebSocket authentication/cancellation, real Opus-to-NetEQ playback, malformed input, and 120 ms decoding. These tests are included in `just test-unit`. - Shared-crate, root workspace, and desktop Tauri Clippy pass; `just desktop-tauri-check` passes. - The desktop Tauri test workspace passes; native two-person listening/device checks remain separate. - All mandatory push checks pass at `d06bac5dc`, including the complete Rust unit gate and desktop workspace checks. Existing agent timing and relay log-capture tests failed under concurrent load, passed separately, and passed in the full serial test run; no tests or hooks were skipped. - One test-only fixture change allows cold Perl startup before the escaped-process test's inner timeout (2 seconds instead of 300 ms). Its independent 5-second watchdog, 300-second surviving pipe writer, and all assertions remain intact. All eight process-helper tests pass; independent review confirmed this still tests bounded drain completion. - Dependency inspection confirms the library brings in no desktop application, Tauri, identity store, relay server, or speech-model crates. - Independent source review found no correctness blockers. Draft: a native two-person call, mute/leave/cancellation, and microphone/speaker switching still need a runtime tryout and human review. Full `just ci` was stopped during the unrelated mobile toolchain download because of disk pressure; it is not a passing gate. Consumer integration is tracked in block/buzz-app#489. --------- Signed-off-by: kenny lopez <klopez4212@gmail.com> |
||
|
|
30873ba46b |
docs(nest): replace commit attribution policy with the runtime facts (#8236)
## What The nest `AGENTS.md` template (`desktop/src-tauri/src/managed_agents/nest_agents.md`) ships to every Buzz user's agents. Its "Git Commit Attribution" section is cut from a general attribution policy (separate claims, author "per applicable policy", hypothetical repos that require a human author, "stop and ask") down to what the runtime actually does: - `buzz-acp` sets a default git identity and signing. Agents use it, but change the author when a repository or their human needs a different one. The operator can opt out with `BUZZ_GIT_IDENTITY=user`. - Credit the other party with `Co-authored-by`: the human you work for when you commit as yourself, or yourself when you commit as them. - Follow each repository's own `AGENTS.md` for sign-off (DCO) and other attribution rules. The buzz-acp base prompt gets the same co-author default. `NEST_AGENTS_VERSION` goes from 6 to 7 so existing nests refresh, and the template test now asserts the new content. ## Why Agents read the old section as a puzzle to solve on every commit. They looked for an applicable policy, wondered whether the human should be the author, and stalled. DCO and sign-off rules are per-repository and already live in each repo's own `AGENTS.md` (this repo says `git commit -s`), so they don't belong in a template that every OSS user's agents read. Companion buzz-app change: block/buzz-app#833. ## Validation `cargo test --lib managed_agents::nest` in `desktop/src-tauri`: 55 passed, including the legacy-attribution upgrade test. I pushed with `--no-verify`: the change is Markdown plus a version constant and a test, and the nest tests above are the relevant suite. CI runs the full set. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Bradley Axen <baxen@squareup.com> Co-authored-by: bundled00 <f172256d14c11991d58561c98a594e7397d64087502e7bc90f2c0e8d0d45363f@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
6c388bde62 |
feat(relay): accept typing indicators over HTTP POST /events (#8207)
## Summary Agents hosted inside the Buzz app sign their own events but don't hold a WebSocket, so they couldn't show "typing…" in a channel. `POST /events` rejected kind 20002 (typing indicator) with `restricted: unknown event kind`. Now, when `POST /events` receives a kind 20002 event, the bridge passes it to the same channel broadcast the WebSocket path uses. Live subscribers in the channel get the pulse. Nothing is written to Postgres. Before broadcasting, the relay checks, in order: - the event has an `h` tag naming the channel - the serialized event is at most 2 KiB (real typing events are ~600 bytes). Measuring the whole JSON, not just content and tag values, means piling on empty tags can't get around the cap - the signature and id are valid - bans and timeouts - the community write fence (over HTTP, a quiescing community's host already fails to bind with a 404, so this catches quiescing that starts after the tenant is bound) - the sender is a channel member, or the channel is open (same rule as WebSocket) - the signer is the pubkey that authenticated the request (checked first; a mismatch returns 403) The existing HTTP admission rate limit and relay-membership gate still run before any of this. Other ephemeral kinds are still rejected over HTTP. `publish_channel_ephemeral` is the old WebSocket channel-broadcast code moved into a shared helper, so both paths use the same membership check and fan-out. Unlike the WebSocket path, HTTP requires the `h` tag. Over WebSocket, a 20002 with no channel goes to every subscriber. Points left for review: - HTTP typing pulses count against the same per-pubkey HTTP quota as message posts (default 300/min). One pulse every 3 s is 20/min per active channel. - A failed Redis publish is logged and the POST still returns `accepted: true`, same as the WebSocket path. A pulse is resent every 3 s and expires after 8 s, so a retry would only resend a stale pulse. The `sadscan:disable` comment on an existing test URL in `ingest.rs` is there because the secret scanner reads every file a commit touches. It uses the same form as the other local test fixtures in the repo. ### Related issue Needed for Agents2 typing indicators in buzz-app. The client PR depends on this one and will follow as a draft. ### Testing - 9 new Postgres-backed tests in `api/typing_postgres_tests.rs` cover these cases: a member's pulse is published and not stored; a non-member gets a 400; a timed-out member gets a 403; a signer mismatch gets a 403; a missing `h` tag gets a 400; a tampered event gets a 400; kind 20001 is still rejected; oversized events get a 400 (2 KiB of content, one 2 KiB tag value, or `h` + 10k empty tags); a community that starts quiescing after the tenant is bound gets a 400 `community writes are fenced`, and nothing is published. Two mutation checks: with the fence removed, the fence test fails; with the old content-plus-tag-values cap, the empty-tags case is accepted and the test fails. - The full buzz-relay Postgres lane on |
||
|
|
c1370cc654 |
chore(release): release Buzz Desktop version 0.5.28 (#8222)
## Buzz Desktop release v0.5.28 - **Frozen main:** `5142cf6b2a0f44686eeda1ed07905ee3560fd78e` - **Reviewed candidate:** `f87c10895ea35a9a2cea629fef1d7532a3fecbc5` - **Previous desktop release:** `desktop-v0.5.27` - **Proposed immutable tag:** `desktop-v0.5.28` This PR may be **squash merged** after the Desktop Release Candidate check and all protected-branch checks pass. Merging authorizes publication of the exact reviewed candidate; later or unrelated changes on `main` cannot alter it. The checked-in changelog accounts for every non-merge commit in the release range. The Desktop tag points to the reviewed candidate commit, not the later squash commit. Publication remains bound to that immutable candidate tag. Signed-off-by: Will Pfleger <wpfleger@block.xyz> Co-authored-by: Release Automation <release-automation@users.noreply.github.com> |
||
|
|
5142cf6b2a |
test(desktop): move CSP guards from Rust to the desktop JS suite (#8172)
## Summary Moves the packaged-app CSP guards from `desktop/src-tauri/tests/csp.rs` to `desktop/src/app/contentSecurityPolicy.test.mjs`. No production code changes beyond one added `export`. `csp.rs` never touched Rust app code. All seven tests parse `tauri.conf.json` and compare CSP strings. The MediaPipe check also `include_str!`'d `desktop/src/features/profile/lib/animatedAvatarCapture.ts` and pulled the `MEDIAPIPE_WASM_BASE` literal out by splitting the text. That made a Tauri test depend on a frontend file. With path-filtered CI (#8156), a change to only that `.ts` file would skip the check. The JS version imports `MEDIAPIPE_WASM_BASE` directly (it's now exported) and reads `src-tauri/tauri.conf.json` from disk. The `desktop/**` CI filter covers both files, so the check runs in Desktop JS whenever either one changes, and the filter needs no cross-boundary special case. The seven tests and their assertions carry over one-for-one, along with their rationale comments. **What got simpler:** we delete a 204-line Rust integration test, the `include_str!` of a frontend file, and the hand-rolled string-literal scraper, and the CI path filter doesn't need to know about this file. ### Related issue Addresses the CSP coupling raised in review of #8156. Related: #4614 (introduced the guards). ### Testing - New suite on Linux: 7/7 pass. - I falsified each test by mutating the real policy or code, one mutation at a time. Each mutation fails exactly the matching test, and the suite passes again once restored: - moved `MEDIAPIPE_WASM_BASE` outside `@mediapipe/` - dropped `'wasm-unsafe-eval'` - dropped the MediaPipe scope from `script-src` - added a bare `https://cdn.jsdelivr.net/` origin - added `'unsafe-inline'` - removed `img-src` - dropped `ws:` from `connect-src` - made the pinned-source predicate always pass - Blox `just ci` at tree `5664c6ce` (same tree as head `f798f94b`): - Passed: fmt, clippy, desktop/Tauri/web checks, security-review/file-size checks, `desktop-test` (6,819 + 139 jsdom), `desktop-build`, `desktop-tauri-check`, `desktop-tauri-test`, `web-build`. - `test-unit`: 94/95 suites pass. The two failing `buzz-acp` `git_bootstrap` tests fail because the workstation's Git 2.43 predates the credential `authtype` protocol. They pass 4/4 with Git 2.51, and this PR touches no `crates/`. - Not run: `mobile-check`/`mobile-test`, because the workstation can't reach pub.dev. No `mobile/` changes. - Hosted CI green: https://github.com/block/buzz/actions/runs/37801901659. This PR selects the Tauri lanes because it deletes `src-tauri/tests/csp.rs`. Later edits that only touch the constant select Desktop JS only. --------- Signed-off-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> Co-authored-by: Elrond <28d6302a099e5225b02c4155ac4236e4912603df2ab08dbfc2f4fef08ce598c8@buzz.block.builderlab.xyz> |
||
|
|
99924a5a69 |
fix(desktop): repair the Windows notification shim's false denial on read (#5600)
Addresses #2445. Closely related to #2483 -- see "Relationship to #2483" below; I am happy to close this in favour of that one. ## The problem On Windows the notification plugin's injected shim decides its permission at startup like this ([`guest-js/init.ts:12-18`](https://github.com/tauri-apps/plugins-workspace/blob/v2/plugins/notification/guest-js/init.ts)): ```ts if (window.Notification.permission !== 'default' || __TEMPLATE_windows__) { return await Promise.resolve(window.Notification.permission === 'granted') } return await invoke('plugin:notification|is_permission_granted') ``` `__TEMPLATE_windows__` is replaced with the literal `true` on Windows, so the `||` short-circuits and the shim compares its own freshly-initialised `'default'` against `'granted'` -- `false` -- and stamps **`denied`**. The line that would ask the backend is unreachable, and the backend would have said yes: `permission_state()` returns `PermissionState::Granted` unconditionally on desktop. So on every launch of the Windows app, `window.Notification.permission` is `denied` and nothing in the OS is denying anything. ## Why the toggle is only half of it `getDesktopNotificationPermissionState()` trusts that value, so `refreshPermission()` reports `denied` at mount, and this effect in `hooks.ts` runs before the user touches anything: ```ts React.useEffect(() => { if (settings.desktopEnabled && (permission === "denied" || permission === "unsupported")) { setSettings((current) => ({ ...current, desktopEnabled: false })); } }, [permission, settings.desktopEnabled]); ``` It writes `desktopEnabled: false` and persists it. Measured on a real install: with alerts successfully enabled and `desktopEnabled` persisted as `true`, a clean relaunch comes back with it persisted as `false`. That means fixing only the enable path leaves a Windows user re-enabling alerts after every restart. ## What this changes One place: `getDesktopNotificationPermissionState()` repairs the bogus value instead of reporting it. ```ts if ( window.Notification.permission === "denied" && isTauri() && isWindowsPlatform() ) { try { return await requestDesktopNotificationAccess(); } catch { return window.Notification.permission; } } ``` `requestPermission()` is the only call that reaches the backend, and on Windows it raises no prompt, returns `Granted`, and rewrites the shim's cached value -- so this is self-limiting: the next read short-circuits as `granted` and never requests again. There is a test for exactly that. Repairing at the read rather than at the toggle fixes every consumer at once, including `sendDesktopNotification()`, which gates delivery on `=== "granted"` and would otherwise drop notifications at boot. - `denied` stays terminal on macOS, on Linux, and on ordinary Windows web pages. - A denial that survives the request is still reported as `denied`. - A rejected request falls back to the current value instead of propagating; callers do not handle rejections today. - Adds `isWindowsPlatform()` to `shared/lib/platform.ts`, alongside the existing `isMacPlatform()` / `isLinuxPlatform()`. - No new dependencies. No changes to `hooks.ts` or to notification delivery. ## Verification on a real Windows 11 install Buzz Desktop 0.5.9 release build, Windows 11 Home 10.0.26200, WebView2 151.0.4129.72, attached over CDP. Clean boot, nothing injected: ``` window.Notification.permission -> "denied" localStorage[...].desktopEnabled -> false (written by the effect above) await window.Notification.requestPermission() -> "granted" window.Notification.permission -> "granted" new window.Notification("Buzz", { body: ... }) ToastNotificationManager::History .GetHistory("xyz.block.buzz.app") -> 3 -> 4 (real toast delivered) CreateToastNotifier(...).Setting -> Enabled ``` The OS was permissive throughout: no `ToastEnabled` override, no notification policy keys under HKCU or HKLM, and the app present under `HKCU\...\Notifications\Settings\xyz.block.buzz.app`. Full detail in [this comment](https://github.com/block/buzz/pull/2483#issuecomment-5255972424). To be precise about scope: this confirms the mechanism the patch relies on, measured on a stock release build. The patched build itself is covered by the unit tests below, not by a packaged Windows run. ## Tests `pnpm test` -- full desktop suite. New cases in `lib/desktop.test.mjs`: - Windows Tauri repairs the shim's false denied state on read - the repaired state is cached, so later reads do not request again - a denial that survives the request is reported as denied - denied stays terminal outside the Windows Tauri app (Windows web, Linux Tauri, Linux web) - granted is returned untouched and never triggers a request Plus `shared/lib/platform.test.mjs` for `isWindowsPlatform()` across `Win32` / `Win64` / `Windows` and `MacIntel` / `Darwin` / `Linux x86_64`. ## Relationship to #2483 #2483 fixes the same root cause at the toggle: it retries the request inside `setDesktopEnabled`. That makes the toggle work, which is what #2445 reports, and its `ensureDesktopNotificationPermission` helper is a nicer shape than an inline condition. The difference is where the repair happens. Because #2483 leaves the mount-time read as `denied`, the effect quoted above still turns the setting off on every launch, so the fix does not survive a restart. Repairing at the read covers the toggle path too, since `setDesktopEnabled` calls `refreshPermission()` first and finds `granted`. The two overlap: both add `isWindowsPlatform()` to `platform.ts`, so whichever lands second needs a trivial rebase. If maintainers prefer #2483's shape, the equivalent of this PR is to call its helper from `refreshPermission` as well as from `setDesktopEnabled` -- I am glad to close this and send that instead. I did not want to push changes into someone else's open PR uninvited. ## Once this lands #2445 can close. I am running a per-machine launcher workaround for a small Windows team until then, which this makes unnecessary. Signed-off-by: VSCteam <144167621+Joxyko@users.noreply.github.com> |
||
|
|
8f66f7051c |
feat: raise hosted community limit to fifty (#8186)
## Summary - Raise the stock relay active ownership quota from 5 to 50 for creation and ownership transfer. Raise the absolute lifetime quota from 20 to 50 reserved hosts; completed owner deletions still permanently consume lifetime capacity to bound create/delete host squatting. - Honor the owner-list `can_create` projection across Desktop settings, create flow, and onboarding instead of imposing a client-side count. Display server `quota_used` / `quota_limit` when available; when the projection is missing, let the server's `limit_reached` decide. - Cover 49→50 and 50→51 admissions, deleted-owner lifetime exhaustion, and rendered create gates under 5/50, 0/50 exhausted, and missing projections. ## Verification - Desktop full `pnpm test` and `pnpm typecheck` pass after the 50-cap update; `cargo fmt --all -- --check` passes. Four targeted Playwright cases and `pnpm build:e2e` passed for the original dynamic-quota change at the previous commit, before changing test fixtures from 20 to 50. - `cargo test -p buzz-db --lib` cannot compile here: macOS dyld rejects the `sqlx_macros` dylib with `mis-aligned LINKEDIT string pool` on this host before the crate's tests run. CI must validate the DB package. ## Rollout / manual validation - Confirm `BUZZ_MAX_COMMUNITIES_PER_OWNER` is unset or 50 on **every** production relay replica; this per-process cached override takes precedence over the stock default. - Confirm Builderlab's list/create/transfer proxy passes the relay's `quota_used`, `quota_limit`, `can_create`, and `limit_reached` without its own 5-community gate. Its backing implementation is not in this repo and was not verified here. - Human test: with an owner at 49 live communities, create the fiftieth, verify the fifty-first returns `limit_reached`; transfer into an owner at 49 then at 50 and confirm the same boundary. Check a deleted-owner account at 0 active/50 lifetime displays the generic limit and disables creation. This manual check remains outstanding. --------- Signed-off-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> Co-authored-by: am <6e30cd56c30e030cd31bb0939b94a7c257c9a09d5ba2d92cf2735da45629f248@buzz.block.builderlab.xyz> |
||
|
|
836e7d0d06 |
feat(desktop): browse and moderate every community in the Admin Console (#8027)
The Admin Console only ever saw the community the desktop happened to be connected to. With #8011's community reads, the console can now browse every community on the relay and, for signed staff, moderate any of them by name without switching connections. 🤖 ## What changes - **Communities tab.** Lists every live community on the relay, searchable by host prefix with "Load more" paging. The connected community is pinned at the top by exact host match. Report rows get a community badge that opens that community. - **Community page.** Reports, Restrictions, Members and Actions, each scoped to the community on the page. The Actions form has no host field: it always acts on the page's community, and the TypeScript host normalization is gone from the Actions tab. - **Native reads** (`admin_list_communities`, `admin_search_members`, `admin_get_member`, `admin_get_event`). Every request validates the explicit community host and NIP-98-signs the real method and URL. Failures come back as a structured error (`status`, `bodyComplete`, `bodyEmpty`, `code`), so the UI can tell an older relay's empty 404, a coded `event_not_found` and a transport failure apart, and never says "not found" unless the relay did. - **Explicit-host lifts.** `admin_lift_restriction` sends the page's community host, not the active relay's. It freezes the relay and signer the intent was reviewed under, and refuses to send if either has changed. - **Pending actions survive navigation.** A panel-level `DirectActionsProvider` holds the frozen intent, so switching communities or tabs mid-confirm doesn't lose it. Only Discard drops it. A signer change remounts the controller and drops it. - **Fenced lookups.** Member and event lookups are keyed by origin, signer, community, target and a generation counter, so a late answer for one community can't unlock Review in another. - **Shared member picker** (`MemberSearchResult`, also used by Add Member), showing the truncated npub so members with the same name can be told apart. Delete accepts a note link and previews the message before Review. Relay staff are blocked before Review. - **Disabled admin auth is read-only.** With `BUZZ_ADMIN_AUTH=disabled` the Communities tab and community pages still render, and report badges still open them, but every ban, timeout, delete, add-member and lift control is disabled. The settings badge says the console is read-only and that acting needs `nip98`. Staffing stays hidden. - #7904's protections in the Actions tab are kept: frozen intent, `requestId`, Retry, `notSent` and the key-backup guard. - **Readable load errors.** Admin list and detail loads show the relay's error message (for example "request is not authorized"), never the raw JSON envelope. - **Failed kicks reload the report.** A resolve that fails with `enforcement_failed` reloads the report, so its failed action and "Cancel & reopen" appear immediately. - **Key backups never leave in a search.** Every admin API request URL passes the NIP-49 key-backup guard before it is signed or sent: community and legacy reads, mutations, the connection probe (before its first, unsigned request) and feedback-attachment fetches. The community and member search boxes, including the directory's Load more, refuse a pasted `nsec` or `ncryptsec` anywhere in the input without calling the relay. - **`just admin-seed` for multi-community testing.** Besides the local community, it seeds `beta.localhost:3000`, `gamma.localhost:3000` and 50 filler communities, with named members, a ban and a timeout in each named community, a report with Kick available, and a message for the delete preview. Rerunning it restores all of that to the starting state. It refuses, writing nothing, while a moderation action is unfinished on a seeded report. - The bodyless `GET /events/{id}` preview is added to the events-URL egress inventory. It sends no body, so it carries no key material. ## Tests Native (Rust): - `empty_404_and_405_read_as_complete_and_empty`, `coded_404_carries_the_relay_code`, `truncated_and_over_cap_bodies_are_incomplete`, `transport_failure_has_no_status`: the structured read error tells an older relay, a coded refusal, a cut-off body and a transport failure apart. - `a_401_retry_is_signed_by_the_same_keys`, `lift_signs_the_send_and_401_retry_with_the_checked_keys`: the 401 retry never switches keys. - `read_urls_validate_the_explicit_host_and_encode_the_query`, `lift_url_sends_the_page_host_not_the_active_relay`, `list_url_carries_host_and_cursor_and_refuses_a_changed_relay`, `lift_refuses_before_sending_on_signer_change_or_bad_host`: requests target the explicit host and refuse to send after a relay or signer change. jsdom: - `actions-pending-nav`: a pending intent survives a community change, and only Discard drops it. - `actions-fenced-lookup`, `fenced-load`: a late lookup for an old key never shows under, or unlocks, the new one. - `actions-same-ids`: the same pubkey and event id are read separately in each community. - `actions-community-change-drops-member`, `actions-page-host`, `actions-success`, `actions-identity`: the form acts only in the page's community, and confirm sends the frozen intent. - `actions-preview-errors`, `actions-delete-link`, `actions-staff-target`, `actions-disabled-auth`: preview errors, delete by note link, the staff block, and community pages rendering read-only with every action control disabled under disabled admin auth. - `communities-directory`, `communities-unsupported`, `community-badge`, `community-badge-disabled-auth`: the directory, older relays, and report badges, which open community pages under disabled admin auth too. - `restrictions-lift-signer-change`. The existing restriction tests now run on the community page and use `admin_lift_restriction`. Mutation checks: removing the pending-navigation guard turns `actions-pending-nav` red, and dropping the host from the fence key turns `actions-fenced-lookup`, `actions-same-ids` and `fenced-load` red. Ungating restriction lifts on `canMutate` turns `actions-disabled-auth` red. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz> |
||
|
|
d742fc5f30 | Use Silo for bundled object storage (#8023) | ||
|
|
5ff83d82f8 |
fix(desktop): address workflow deletions at the workflow's actual owner (#2669)
Fixes #2671. ## Problem Deleting an agent-created workflow from the desktop app appears to succeed, but the workflow keeps running and stays in the list. Since workflows are commonly created by agents, this makes most workflows undeletable from the UI. ## Root Cause `delete_workflow` builds the NIP-09 `a`-tag coordinate with the **caller's** pubkey: ```rust let builder = events::build_workflow_delete(&workflow_id, ¤t_pubkey_hex(&state)?)?; ``` kind:30620 is addressable by `(author, d-tag)`. For an agent-created workflow, `30620:<caller>:<workflow-id>` names a record that does not exist. The relay accepts the kind:5 (a self-addressed delete always passes validation) and deletes nothing; the UI reports success. Observed live: a delete of an agent-owned workflow produced `["a","30620:<human-pubkey>:<workflow-uuid>"]` while the kind:30620 event is authored by the agent — the coordinate can never match. ## Fix Fetch the workflow's kind:30620 event first (the same `#d` query `update_workflow` already uses) and build the coordinate from the event's author. Relay-side validation already authorizes the owning human for agent-authored coordinates via `is_agent_owner`. Deleting an unknown workflow id now surfaces `workflow not found` instead of silently publishing an unmatchable delete. This is the client half of the fix. The relay half is #2242: today the relay executes a-tag deletions scoped by the delete's signer, so a human-signed delete of an agent workflow still no-ops server-side. Without this change, #2242 alone won't help desktop users, because the client addresses a record that doesn't exist. Together they make delete work end to end. Related but deliberately not included: deleted workflows leaving their kind:30620 event queryable (ghost list entries) is #2390, with a relay-side fix open in #1340. ## Validation - `cargo test` in `desktop/src-tauri`: 1617 passed, 0 failed, 14 ignored. - `cargo clippy --all-targets` and `cargo fmt --check`: clean. - Live repro against the hosted relay: the stock client's kind:5 for an agent-owned workflow named the signer in the coordinate; the relay returned `accepted: true` and the workflow remained live and kept firing. - With the fix: the delete event carries `30620:<agent-pubkey>:<workflow-id>`; an owner-signed deletion of that exact coordinate was verified to remove the workflow record. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
001290a34d |
chore(release): release Buzz Desktop version 0.5.27 (#8125)
## Buzz Desktop release v0.5.27 - **Frozen main:** `af5bb0af488784c3707e99c87e0675a729711a6e` - **Reviewed candidate:** `7a2fb3cc2d401e91e67d9819741d9025278cbec0` - **Previous desktop release:** `desktop-v0.5.26` - **Proposed immutable tag:** `desktop-v0.5.27` This PR may be **squash merged** after the Desktop Release Candidate check and all protected-branch checks pass. Merging authorizes publication of the exact reviewed candidate; later or unrelated changes on `main` cannot alter it. The checked-in changelog accounts for every non-merge commit in the release range. The Desktop tag points to the reviewed candidate commit, not the later squash commit. Publication remains bound to that immutable candidate tag. Signed-off-by: Will Pfleger <wpfleger@block.xyz> Co-authored-by: Release Automation <release-automation@users.noreply.github.com> |
||
|
|
403c19f673 |
fix(desktop): show OpenAI-compatible endpoint settings in the baked-env UI (#8117)
## Summary `is_safe_to_reveal` (`desktop/src-tauri/src/managed_agents/env_vars.rs`) is the single default-deny allowlist for showing baked env values. It lists `DATABRICKS_HOST`/`DATABRICKS_MODEL`/`DATABRICKS_MODEL_FILTER` but not their OpenAI-compatible counterparts, so a build that bakes `OPENAI_COMPAT_BASE_URL`, `OPENAI_COMPAT_MODEL`, `OPENAI_COMPAT_API` shows them as `••••••`. Adds those three non-secret settings to the allowlist: an endpoint URL, a model id, and an `auto|chat|responses` enum. **No secret is revealed: `OPENAI_COMPAT_API_KEY` is deliberately not added and remains masked.** ### Related issue None found. ### Testing - New test `baked_env_openai_compat_endpoint_is_unmasked_but_key_stays_masked` covers the three revealed settings and asserts the API key stays masked; `baked_env_allowlist_is_case_insensitive` extended. - `cargo test --lib baked_env` in `desktop/src-tauri`: 11 passed. `cargo fmt` clean. - Not exercised in the running desktop UI. Signed-off-by: Tom Bedor <tbedor@squareup.com> Co-authored-by: Tom Bedor <tbedor@squareup.com> |
||
|
|
9c6fa14aa0 |
feat(desktop): offer Claude effort levels in agent edit and create (#8059)
Claude Code agents now get a thinking-effort dropdown in the agent Edit and Create dialogs, matching the one buzz-agent and Codex agents already have. ## Behavior - **Where levels come from:** Claude effort levels come from the Claude model data for the model that will actually run after Save, resolved in order: explicit model, then inherited default (linked definition or global), then adapter default. Stored Claude session levels are not used, because the stored session data does not record which model produced them. - **Edit:** for an agent linked to a definition, effort follows the definition's model, which stays authoritative. Save never sends a model for linked agents. - **Create:** effort is offered for local agents only and is stored on the instance. Allowed levels are recomputed on every render, so collapsing Advanced and then switching to a model without effort (e.g. Haiku) drops effort from the request. Non-local effort is rejected before any create side effects. - **Persistence:** `create_managed_agent` sets the picked effort when it builds the record and drops stale effort env aliases. Its body runs over any Tauri runtime, so a mock-app test drives the real create path and reads the record back from disk. That test runs in a child process with temp app-data and nest folders, and a test-only switch turns keychain use off so agent keys stay inline in the temp agent file and the OS keychain is never touched. - **Observer store:** live session-config captures are written to the store before the dialog refreshes; archived captures are read-only. - **Codex:** its existing session-native effort picker is unchanged. --------- Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Hayt <211b96e6a2b7f45fd4047988976c7bbbeeda0c15f3ae7b32eec20834b5a55118@buzz.block.builderlab.xyz> |
||
|
|
fcf5f044b3 |
fix(desktop): use object-scale-down for inline markdown images (#4686)
Fixes #4384. A dim-less markdown image with no cached decode gets `DEFAULT_IMAGE_RESERVE = { height: 256, width: 384 }` and `useFixedReserveBox: true` in `resolveImageReserveBox`. The frame is that big on first view, and the inline `<img>` uses `object-contain` which scales UP to fill the frame — so a 46×20 shields badge renders as a giant blurry block. The same message renders at the natural size on the second view because `rememberDecodedImageDimensions` populates the cache, but the inconsistency between first and subsequent view is the bug. Switches `object-contain` → `object-scale-down` on `ProgressiveImage.tsx`'s shared `IMAGE_CLASS`. Both `object-contain` and `object-scale-down` scale down to fit, but only `object-scale-down` also refuses to scale up — a small image now sits centered in the reserve box on first view, never upscaled. The layout-shift guarantee for large images is preserved (they still scale down to fit the reserve), matching the issue's stated constraint. Chose option 2 ("Alternatively/minimally") from the issue over option 1 because: - It's a one-class-name change with zero new state, zero new effects, and zero risk to the existing layout-shift guarantee that the surrounding `useFrozenImageReserve` is explicitly designed to preserve. - Option 1 (collapse the frame on decode) is the issue author's preferred direction but requires deciding whether the layout shift is acceptable *shrink*-only — a direction question that should be confirmed with the maintainer first. The issue explicitly says "Happy to open a PR for option 1 if the direction is acceptable" — that's the sign-off step, not this PR. If maintainers want the full first-view-equals-second-view behavior, option 1 is a follow-up that builds on this base. Not added: a regression test. The current desktop test infra (`desktop/src/shared/ui/markdown/markdown.test.mjs` and siblings) doesn't cover `ProgressiveImage` directly; a DOM-style snapshot would need a new test harness. The change is one CSS class name; the visual regression is plain to eyeball in the screenshots CI already captures. Verified: - `pnpm check` (biome + file-sizes + px-text + pubkey-truncation) — clean - `npx tsc --noEmit` — clean Signed-off-by: kushaim <carlossilvajimenez@gmail.com> Co-authored-by: kushaim <carlossilvajimenez@gmail.com> |
