mirror of
https://github.com/block/buzz.git
synced 2026-10-28 21:11:48 +00:00
Stack: [#5112](https://github.com/block/buzz/pull/5112) → this PR Stacks on the team catalog backend PR (#5112). Contains the `desktop/**` changes that turn the add-agent surface into a single Community Catalog browsing both shared agents and shared teams. ## Owner catalog sync pipeline `usePersonaSync.ts` gains a hydration pipeline in `startPersonaSync` so a fresh device that comes online after another already published the owner's 30178 catalog head reconstructs the complete state without falsely retracting it. The owner's persona/team/managed-agent/30178/deletion history is backfilled up front (a live-only subscription gets no history — reconnect-replay's since-cursor is undefined until the first live event), then a live subscription takes over. Four properties keep a fresh sync from purging the owner's valid shared head: - **Paged backfill with a safe termination guarantee.** The relay serves each REQ newest-first and clamps `limit` to its advertised `max_limit`, so a large owner's history overflows one page — a newer 30178/30176 could return in-page while an older required 30175 constituent falls beyond it. `fetchOwnerHistoryToExhaustion` pages the full window with the `until` time cursor (the only cursor the WS REQ filter exposes; the DB `before_id` keyset is REST-only), deduping the inclusive-boundary rows. A short page terminates normally. A full page whose oldest event cannot advance the time-only cursor is a **dense boundary** — more than one page of events share one `created_at` second, which the WS filter has no `(created_at, id)` cursor to escape — and raises `PersonaHistoryDenseBoundaryError` rather than silently completing as if the history were exhausted and dropping the older constituents behind it. - **Constituents-before-catalog ordering.** Over the complete batch, `orderCatalogHeadsLast` stably defers every 30178 head past its 30175/30176 constituents. Reconciling a 30178 head before its personas hydrate makes the inbound team refresh fail member resolution and queue a dominating false tombstone; deferring the heads guarantees the constituents are all applied first, while newest-wins order within every other coordinate is untouched. - **Hydration boundary for concurrent live events.** The backfill fetch and the live subscription start concurrently into one reconcile chain. A live or replayed 30178 that arrives before the backfill reconciles its constituents would reproduce the same false-tombstone purge, so live events are buffered until the ordered backfill is dispatched and then drained in arrival order. Steady-state live events (after hydration completes) reconcile immediately. - **Explicit backfill failure policy.** A transient history-fetch rejection is retried with bounded backoff. When backfill cannot complete — retries exhausted, or a deterministic dense boundary — the pipeline enters a **degraded-live** state rather than leaving the subscription permanently inert with live events accumulating in the buffer: the hydration boundary still opens so buffered and future live events keep reconciling, but the whole catalog dependency set is dropped — the 30178 head, its 30175/30176 constituents, and any kind-5 deletion carrying a dependency-targeting `a` tag (classified by scanning *all* `a` tags, matching the backend's deletion router, which `find_map`s across every tag and routes the first signer-owned coordinate — so a malformed or foreign first `a` tag ahead of an owned 30176 cannot slip a destructive deletion through) — because backfill never fully hydrated the owner's constituents. Dropping only the 30178 head is not enough: the backend refreshes the catalog head after every team/persona save, and live delivery is newest-first, so a 30176 edit that adds a *new* member would reach the backend before that member's 30175 and falsely tombstone a witness-holding device's valid team. Holding the prior hydrated run's constituents on disk only proves the *old* revision is resolvable — it says nothing about a new member — which is why the entire dependency set is held rather than just the head. 30177 managed-agent runtime policy stays live (it drives no catalog refresh). A degraded device stays stale on team/persona edits until it self-heals on the next effect re-run (restart, or an identity/community switch) — the correct trade against destroying valid shared state. ## Data layer Relay paging, signature verification, NIP-33 head selection, and untrusted-content parsing for the kind `30178` team catalog live natively in the `fetch_team_catalog` Tauri command (`team_catalog.rs`), structurally mirroring `fetch_persona_catalog` (`persona_catalog.rs`). A catalog refresh crosses IPC once and never verifies a signature on the webview thread. `teamCatalogRelay.ts` is now a thin presentation and local-linkage layer over the verified projection — it shapes entries for display and links each to a local team, and never parses or verifies. Parsing is all-or-nothing, identical to the add-time re-fetch in `add_team_from_catalog`: a team with any invalid member fails to parse and the publication is dropped from the catalog, matching persona behavior. **Behavior delta:** the previously reviewed partial-render of invalid-member teams — a warning banner on an entry that could never be added — is removed. Invalid publications are dropped entirely rather than surfaced as un-addable. ## Hooks `useTeamCatalogRelay.ts` mirrors the persona catalog hook: a community-keyed query over `fetch_team_catalog`, live invalidation on kind `30178`, share/unshare, and add-from-catalog (which invalidates both the teams and personas stores, since adopting a team copies its members as local personas). ## CommunityCatalogDialog Single unified surface replacing the former separate dialogs. Agents and Teams appear as labeled sections with type-tagged selection and a teams-preferred launch. `TeamsSection`'s discover entry and the new-agent card both open this one dialog. `PersonaCatalogDialog.tsx` is removed; persona browsing now lives inside the unified dialog. ## TeamShareDialog Publishes and unshares team catalog entries via `set_team_shared`. ## e2e + screenshots `team-catalog.spec.ts` covers the browse + adopt flow; `team-catalog-screenshots.spec.ts` produces the pixel-regression set. `e2eBridge.ts` gains `mockTeamCatalogPublications`, which mirrors the native command's head selection and shared gate and performs only a shallow `v`/`name`/`members`-array shape check; per-member validation stays in the Rust command. ## Follow-ups None. The shared agent-definition text-safety policy (Unicode-control/bidi/zero-width rejection) already covers the team surface: #5112's `validate_team_catalog_content` chokepoint gates every field delivered verbatim to the harness on both the publish and adopt paths, and this PR's parse layer consumes that verified projection rather than re-validating on the webview thread. Signed-off-by: Will Pfleger <pfleger.will@gmail.com> Co-authored-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
206 lines
7.5 KiB
TypeScript
206 lines
7.5 KiB
TypeScript
import { defineConfig, devices } from "@playwright/test";
|
|
|
|
export default defineConfig({
|
|
testDir: "./tests/e2e",
|
|
timeout: 30_000,
|
|
retries: process.env.CI ? 2 : 0,
|
|
workers: 1,
|
|
reporter: [
|
|
["list"],
|
|
["html", { open: "never", outputFolder: "playwright-report" }],
|
|
],
|
|
use: {
|
|
baseURL: "http://127.0.0.1:4173",
|
|
screenshot: "only-on-failure",
|
|
trace: "on-first-retry",
|
|
video: "retain-on-failure",
|
|
},
|
|
projects: [
|
|
{
|
|
name: "smoke",
|
|
testMatch: [
|
|
"**/smoke.spec.ts",
|
|
"**/thread-head-stale-edit.spec.ts",
|
|
"**/sidebar-offcanvas-rail.spec.ts",
|
|
"**/tooltip-semantics.spec.ts",
|
|
"**/search-scope-screenshots.spec.ts",
|
|
"**/onboarding-docked-cta-screenshots.spec.ts",
|
|
"**/identity-key-help.spec.ts",
|
|
"**/key-import-reveal.spec.ts",
|
|
"**/navigation.spec.ts",
|
|
"**/channels.spec.ts",
|
|
"**/channel-shared-header-backdrop.spec.ts",
|
|
"**/auxiliary-pane-close-visibility.spec.ts",
|
|
"**/channel-composer-overflow.spec.ts",
|
|
"**/badge.spec.ts",
|
|
"**/channel-browser.spec.ts",
|
|
"**/channel-add-screenshots.spec.ts",
|
|
"**/add-community-screenshots.spec.ts",
|
|
"**/hosted-communities-settings-screenshots.spec.ts",
|
|
"**/invites-settings-screenshots.spec.ts",
|
|
"**/messaging.spec.ts",
|
|
"**/message-feedback-snapshots.spec.ts",
|
|
"**/message-copy-link.spec.ts",
|
|
"**/custom-emoji.spec.ts",
|
|
"**/profile-custom-emoji-status.spec.ts",
|
|
"**/custom-emoji-ui.spec.ts",
|
|
"**/channel-mute.spec.ts",
|
|
"**/channel-star.spec.ts",
|
|
"**/channel-controls.spec.ts",
|
|
"**/channel-activity-popover.spec.ts",
|
|
"**/active-turn-resilience.spec.ts",
|
|
"**/profile-active-turn.spec.ts",
|
|
"**/config-bridge-screenshots.spec.ts",
|
|
"**/observer-feed-screenshots.spec.ts",
|
|
"**/core-memory-screenshots.spec.ts",
|
|
"**/activity-scope-label-screenshots.spec.ts",
|
|
"**/welcome-agent-modal-screenshots.spec.ts",
|
|
"**/local-archive-screenshots.spec.ts",
|
|
"**/voice-settings.spec.ts",
|
|
"**/agent-readiness-screenshots.spec.ts",
|
|
"**/agent-error-state-screenshots.spec.ts",
|
|
"**/edit-agent.spec.ts",
|
|
"**/doctor-cta-screenshots.spec.ts",
|
|
"**/pubkey-display-screenshots.spec.ts",
|
|
"**/file-attachment.spec.ts",
|
|
"**/image-attachment-gallery.spec.ts",
|
|
"**/composer-image-draw.spec.ts",
|
|
"**/video-attachment.spec.ts",
|
|
"**/spoiler.spec.ts",
|
|
"**/composer-link-shortcut.spec.ts",
|
|
"**/entity-link-recipient-cards.spec.ts",
|
|
"**/composer-selection-formatting.spec.ts",
|
|
"**/composer-tooltip-dismiss.spec.ts",
|
|
"**/mentions.spec.ts",
|
|
"**/team-mentions.spec.ts",
|
|
"**/persistent-agent-audience.spec.ts",
|
|
"**/relay-reconnect.spec.ts",
|
|
"**/relay-reconnect-affordance.spec.ts",
|
|
"**/workflows.spec.ts",
|
|
"**/workflow-reaction-picker.spec.ts",
|
|
"**/workflow-local-controls.spec.ts",
|
|
"**/workflow-title-stability.spec.ts",
|
|
"**/identity-archive.spec.ts",
|
|
"**/identity-archive-hide.spec.ts",
|
|
"**/relay-connectivity.spec.ts",
|
|
"**/unread-pill.spec.ts",
|
|
"**/sidebar-more-unread-overlap.spec.ts",
|
|
"**/sidebar-snapshot.spec.ts",
|
|
"**/home-collapsed-top-chrome.spec.ts",
|
|
"**/top-chrome-zoom-clearance.spec.ts",
|
|
"**/thread-unread.spec.ts",
|
|
"**/thread-load-failure.spec.ts",
|
|
"**/project-conversation-load-failure.spec.ts",
|
|
"**/huddle-thread-load-failure.spec.ts",
|
|
"**/workspace-rail.spec.ts",
|
|
"**/community-rail.spec.ts",
|
|
"**/boot-splash.spec.ts",
|
|
"**/thread-reply-anchor-roleplay.spec.ts",
|
|
"**/threadpane-ultrawide.spec.ts",
|
|
"**/thread-focus-mode.spec.ts",
|
|
"**/animated-avatar.spec.ts",
|
|
"**/reminders.spec.ts",
|
|
"**/reminder-click-repro.spec.ts",
|
|
"**/virtualization.spec.ts",
|
|
"**/scroll-history.spec.ts",
|
|
"**/channel-dense-second-reach.spec.ts",
|
|
"**/channel-window-mock-paging.spec.ts",
|
|
"**/channel-head-restart.spec.ts",
|
|
"**/live-broadcast-reply-timeline.spec.ts",
|
|
"**/markdown-parse-cache.spec.ts",
|
|
"**/overscroll-boundary.spec.ts",
|
|
"**/terminal-wheel.spec.ts",
|
|
"**/cold-switch-longtask.perf.ts",
|
|
"**/timeline-no-shift.spec.ts",
|
|
"**/human-edit-agent-content.spec.ts",
|
|
"**/empty-edit-delete.spec.ts",
|
|
"**/reaction-order.spec.ts",
|
|
"**/reaction-names.spec.ts",
|
|
"**/inbox-reactions.spec.ts",
|
|
"**/inbox-edit.spec.ts",
|
|
"**/send-channel-binding.spec.ts",
|
|
"**/project-cold-start.spec.ts",
|
|
"**/project-commit-detail.spec.ts",
|
|
"**/project-empty-state-alignment.spec.ts",
|
|
"**/project-inbox.spec.ts",
|
|
"**/projects-v3-screenshots.spec.ts",
|
|
"**/project-issue-comments.spec.ts",
|
|
"**/project-pr-review.spec.ts",
|
|
"**/persona-model-combobox-screenshots.spec.ts",
|
|
"**/drafts-screenshots.spec.ts",
|
|
"**/drafts-all-fix-screenshots.spec.ts",
|
|
"**/inbox-refactor-screenshots.spec.ts",
|
|
"**/buzz-theme-screenshots.spec.ts",
|
|
"**/appearance-previews.spec.ts",
|
|
"**/channel-sort.spec.ts",
|
|
"**/identity-lost.spec.ts",
|
|
"**/deep-link-invite.spec.ts",
|
|
"**/invite-link-copy.spec.ts",
|
|
"**/global-agent-config-screenshots.spec.ts",
|
|
"**/doctor-states.spec.ts",
|
|
"**/onboarding-avatar-skip.spec.ts",
|
|
"**/onboarding-backup.spec.ts",
|
|
"**/onboarding-agent-defaults.spec.ts",
|
|
"**/nostr-bind.spec.ts",
|
|
"**/mobile-pairing-qr.spec.ts",
|
|
"**/profile-nsec-reveal.spec.ts",
|
|
"**/profile-backup-settings.spec.ts",
|
|
"**/signout-confirmation.spec.ts",
|
|
"**/settings-section-layout.spec.ts",
|
|
"**/agent-provider-dropdowns.spec.ts",
|
|
"**/agent-lifecycle-feedback.spec.ts",
|
|
"**/agent-access-warning.spec.ts",
|
|
"**/edit-agent-run-on.spec.ts",
|
|
"**/inbox-live-update.spec.ts",
|
|
"**/mesh-compute.spec.ts",
|
|
"**/observer-archive-policy.spec.ts",
|
|
"**/harness-management.spec.ts",
|
|
"**/harness-catalog-screenshots.spec.ts",
|
|
"**/inline-custom-harness.spec.ts",
|
|
"**/where-to-run-config.spec.ts",
|
|
"**/huddle-transcription.spec.ts",
|
|
"**/agent-numeric-tuning.spec.ts",
|
|
"**/needs-restart-screenshots.spec.ts",
|
|
"**/team-catalog-screenshots.spec.ts",
|
|
],
|
|
use: {
|
|
...devices["Desktop Chrome"],
|
|
},
|
|
},
|
|
{
|
|
name: "integration",
|
|
testMatch: [
|
|
"**/agents.spec.ts",
|
|
"**/agent-snapshot-recipient.spec.ts",
|
|
"**/onboarding.spec.ts",
|
|
"**/stream.spec.ts",
|
|
"**/integration.spec.ts",
|
|
"**/dm-double-notification.spec.ts",
|
|
"**/profile.spec.ts",
|
|
"**/sidebar.spec.ts",
|
|
"**/sidebar-relay-card.spec.ts",
|
|
"**/tokens.spec.ts",
|
|
"**/persona-env-vars.spec.ts",
|
|
"**/persona-sync.spec.ts",
|
|
"**/team-snapshot.spec.ts",
|
|
"**/team-catalog.spec.ts",
|
|
"**/agents-everywhere.live.spec.ts",
|
|
"**/relay-restart.live.spec.ts",
|
|
"**/parity-ancestor-island.spec.ts",
|
|
],
|
|
use: {
|
|
...devices["Desktop Chrome"],
|
|
},
|
|
expect: {
|
|
timeout: process.env.CI ? 15_000 : 10_000,
|
|
},
|
|
},
|
|
],
|
|
webServer: {
|
|
command: "python3 -m http.server 4173 -d dist",
|
|
cwd: ".",
|
|
reuseExistingServer: !process.env.CI,
|
|
url: "http://127.0.0.1:4173",
|
|
},
|
|
});
|