From b76b1813ef44727efed3ad83104cb5e7da94ed70 Mon Sep 17 00:00:00 2001 From: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 Date: Wed, 17 Jun 2026 01:42:20 -0400 Subject: [PATCH] test(desktop): add leadership E2E seed hook + screenshot spec Co-authored-by: Will Pfleger Signed-off-by: Will Pfleger --- desktop/playwright.config.ts | 1 + .../src/features/agents/observerRelayStore.ts | 25 +++ desktop/src/testing/e2eBridge.ts | 8 + .../tests/e2e/leadership-screenshots.spec.ts | 188 ++++++++++++++++++ 4 files changed, 222 insertions(+) create mode 100644 desktop/tests/e2e/leadership-screenshots.spec.ts diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts index 5c431eba7..aada89e46 100644 --- a/desktop/playwright.config.ts +++ b/desktop/playwright.config.ts @@ -34,6 +34,7 @@ export default defineConfig({ "**/team-management-screenshots.spec.ts", "**/active-turn-screenshots.spec.ts", "**/active-turn-resilience-screenshots.spec.ts", + "**/leadership-screenshots.spec.ts", "**/profile-active-turn-screenshots.spec.ts", "**/file-attachment.spec.ts", "**/video-attachment.spec.ts", diff --git a/desktop/src/features/agents/observerRelayStore.ts b/desktop/src/features/agents/observerRelayStore.ts index 426b564a2..49cd0f413 100644 --- a/desktop/src/features/agents/observerRelayStore.ts +++ b/desktop/src/features/agents/observerRelayStore.ts @@ -328,6 +328,31 @@ export function useManagedAgentObserverBridge( }, [hasActiveAgent]); } +// Test-only: inject synthetic `leadership_status` frames through the real +// ingest path (`appendAgentEvent`), so the cached-map rebuild the consumer +// reads is exercised — not a fake. Registers the agent as known so the row +// renders. Production ingest (`handleRelayObserverEvent`) is untouched. +export function seedLeadershipForTest( + agentPubkey: string, + instances: readonly { instanceId: string; isLeader: boolean }[], +) { + knownAgentPubkeys.add(normalizePubkey(agentPubkey)); + let seq = Date.now(); + for (const { instanceId, isLeader } of instances) { + seq += 1; + appendAgentEvent(agentPubkey, { + seq, + timestamp: new Date().toISOString(), + kind: LEADERSHIP_EVENT_KIND, + agentIndex: null, + channelId: null, + sessionId: null, + turnId: null, + payload: { instanceId, isLeader }, + }); + } +} + export function resetAgentObserverStore() { generation += 1; const unsubscribe = unsubscribeRelay; diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index a4f457d74..57fad5ec2 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -8,6 +8,7 @@ import { relayClient } from "@/shared/api/relayClient"; import type { ConnectionState } from "@/shared/api/relayClientShared"; import type { RelayEvent } from "@/shared/api/types"; import { syncAgentTurnsFromEvents } from "@/features/agents/activeAgentTurnsStore"; +import { seedLeadershipForTest } from "@/features/agents/observerRelayStore"; import { CUSTOM_EMOJI_SET_D_TAG, KIND_EMOJI_SET, @@ -653,6 +654,10 @@ declare global { channelId: string; turnId: string; }) => void; + __BUZZ_E2E_SEED_LEADERSHIP__?: (input: { + agentPubkey: string; + instances: { instanceId: string; isLeader: boolean }[]; + }) => void; __BUZZ_E2E_EMIT_MOCK_READ_STATE__?: (input: { clientId: string; contexts: Record; @@ -6339,6 +6344,9 @@ export function maybeInstallE2eTauriMocks() { }, ]); }; + window.__BUZZ_E2E_SEED_LEADERSHIP__ = ({ agentPubkey, instances }) => { + seedLeadershipForTest(agentPubkey, instances); + }; const meshNodeStatus = ( state: "off" | "running", mode: "serve" | "client" | null, diff --git a/desktop/tests/e2e/leadership-screenshots.spec.ts b/desktop/tests/e2e/leadership-screenshots.spec.ts new file mode 100644 index 000000000..42df8aa14 --- /dev/null +++ b/desktop/tests/e2e/leadership-screenshots.spec.ts @@ -0,0 +1,188 @@ +import { expect, test } from "@playwright/test"; + +import { installMockBridge } from "../helpers/bridge"; + +const SHOTS = "test-results/leadership"; + +// Mock agent pubkeys (distinct from the relay agents seeded by default). +const AGENT_PAUL = "aa".repeat(32); +const AGENT_DUNCAN = "bb".repeat(32); + +type LeadershipInstance = { instanceId: string; isLeader: boolean }; + +async function waitForBridge(page: import("@playwright/test").Page) { + await page.waitForFunction( + () => + typeof (window as Window & { __BUZZ_E2E_SEED_LEADERSHIP__?: unknown }) + .__BUZZ_E2E_SEED_LEADERSHIP__ === "function", + null, + { timeout: 10_000 }, + ); +} + +async function openAgentsView(page: import("@playwright/test").Page) { + await page.goto("/", { waitUntil: "domcontentloaded" }); + await waitForBridge(page); + await page.getByTestId("open-agents-view").click(); + await expect(page.getByTestId("unified-agents-groups")).toBeVisible({ + timeout: 10_000, + }); +} + +// The freshest-leader rule selects max(lastSeen), tie-broken by seq. The seed +// hook seeds in array order with a monotonic seq, so list the intended leader +// LAST to make selection deterministic even when timestamps collide at ms. +async function seedLeadership( + page: import("@playwright/test").Page, + agentPubkey: string, + instances: LeadershipInstance[], +) { + await page.evaluate( + ({ pubkey, frames }) => { + const win = window as Window & { + __BUZZ_E2E_SEED_LEADERSHIP__?: (input: { + agentPubkey: string; + instances: { instanceId: string; isLeader: boolean }[]; + }) => void; + }; + win.__BUZZ_E2E_SEED_LEADERSHIP__?.({ + agentPubkey: pubkey, + instances: frames, + }); + }, + { pubkey: agentPubkey, frames: instances }, + ); +} + +const MANAGED_AGENTS = [ + { + pubkey: AGENT_PAUL, + name: "Paul", + status: "running" as const, + channelNames: ["general", "engineering"], + }, + { + pubkey: AGENT_DUNCAN, + name: "Duncan", + status: "running" as const, + channelNames: ["general", "design"], + }, +]; + +async function openLeadershipSubmenu( + page: import("@playwright/test").Page, + agentPubkey: string, +) { + await page.getByTestId(`managed-agent-actions-${agentPubkey}`).click(); + const submenuTrigger = page.getByRole("menuitem", { name: "Leadership" }); + await expect(submenuTrigger).toBeVisible(); + await submenuTrigger.hover(); + // Settle the submenu open animation before capture. + await submenuTrigger.evaluate((el) => + Promise.all( + el + .closest("[data-state]") + ?.getAnimations() + .map((a) => a.finished) ?? [], + ), + ); +} + +test.describe("leadership UI screenshots", () => { + test.use({ viewport: { width: 1280, height: 720 } }); + + test("01 — single instance shows Leader badge, no submenu", async ({ + page, + }) => { + await installMockBridge(page, { managedAgents: MANAGED_AGENTS }); + await openAgentsView(page); + + await seedLeadership(page, AGENT_PAUL, [ + { instanceId: "4821-1718600000000000", isLeader: true }, + ]); + + const row = page.getByTestId(`managed-agent-${AGENT_PAUL}`); + await expect(row).toContainText("Leader", { timeout: 5_000 }); + + await page.getByTestId(`managed-agent-actions-${AGENT_PAUL}`).click(); + await expect( + page.getByRole("menuitem", { name: "Leadership" }), + ).toHaveCount(0); + await page.keyboard.press("Escape"); + + await page.getByTestId("unified-agents-groups").screenshot({ + path: `${SHOTS}/01-single-instance-leader.png`, + }); + }); + + test("02 — multi-instance badge reflects the freshest leader", async ({ + page, + }) => { + await installMockBridge(page, { managedAgents: MANAGED_AGENTS }); + await openAgentsView(page); + + // Leader seeded last → highest seq → wins the freshest-leader tie-break. + await seedLeadership(page, AGENT_PAUL, [ + { instanceId: "4821-1718600000000000", isLeader: false }, + { instanceId: "5190-1718600100000000", isLeader: false }, + { instanceId: "6033-1718600200000000", isLeader: true }, + ]); + + const row = page.getByTestId(`managed-agent-${AGENT_PAUL}`); + await expect(row).toContainText("Leader", { timeout: 5_000 }); + + await page.getByTestId("unified-agents-groups").screenshot({ + path: `${SHOTS}/02-multi-instance-badge.png`, + }); + }); + + test("03 — leadership submenu lists each instance", async ({ page }) => { + await installMockBridge(page, { managedAgents: MANAGED_AGENTS }); + await openAgentsView(page); + + await seedLeadership(page, AGENT_PAUL, [ + { instanceId: "4821-1718600000000000", isLeader: false }, + { instanceId: "5190-1718600100000000", isLeader: false }, + { instanceId: "6033-1718600200000000", isLeader: true }, + ]); + + const row = page.getByTestId(`managed-agent-${AGENT_PAUL}`); + await expect(row).toContainText("Leader", { timeout: 5_000 }); + + await openLeadershipSubmenu(page, AGENT_PAUL); + await expect(page.getByRole("menuitem", { name: /Leader/ })).toBeVisible(); + + await page.screenshot({ + path: `${SHOTS}/03-leadership-submenu.png`, + clip: { x: 0, y: 0, width: 1280, height: 720 }, + }); + }); + + test("04 — Make leader action on a non-leader instance", async ({ page }) => { + await installMockBridge(page, { managedAgents: MANAGED_AGENTS }); + await openAgentsView(page); + + await seedLeadership(page, AGENT_PAUL, [ + { instanceId: "4821-1718600000000000", isLeader: false }, + { instanceId: "5190-1718600100000000", isLeader: false }, + { instanceId: "6033-1718600200000000", isLeader: true }, + ]); + + const row = page.getByTestId(`managed-agent-${AGENT_PAUL}`); + await expect(row).toContainText("Leader", { timeout: 5_000 }); + + await openLeadershipSubmenu(page, AGENT_PAUL); + + // A non-leader instance's item is the enabled cooperative-steal entry point. + const makeLeaderItem = page + .getByRole("menuitem") + .filter({ hasText: "4821" }); + await expect(makeLeaderItem).toBeVisible(); + await makeLeaderItem.hover(); + + await page.screenshot({ + path: `${SHOTS}/04-make-leader-action.png`, + clip: { x: 0, y: 0, width: 1280, height: 720 }, + }); + }); +});