From 2fbb83099df258653c6ba087a8786a31e59d6652 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Mon, 15 Jun 2026 16:12:39 -0700 Subject: [PATCH] test(e2e): observer-seed screenshot spec (light+dark populated capture) Adds the playwright spec that drives the seeded agent-session thread panel and captures populated light (catppuccin-latte) + dark (houston) screenshots for #1061 review. Exports canonical OBSERVER_SEED_AGENT_PUBKEY + observerSeedFrames from the fixture and registers the spec in the smoke project testMatch. --- desktop/playwright.config.ts | 1 + .../e2e/observer-seed-screenshots.spec.ts | 169 ++++++++++++++++++ desktop/tests/helpers/observerSeedFixture.ts | 49 +++-- 3 files changed, 203 insertions(+), 16 deletions(-) create mode 100644 desktop/tests/e2e/observer-seed-screenshots.spec.ts diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts index 3dd4e6c2f..88d6c7dcb 100644 --- a/desktop/playwright.config.ts +++ b/desktop/playwright.config.ts @@ -41,6 +41,7 @@ export default defineConfig({ "**/identity-archive.spec.ts", "**/identity-archive-hide.spec.ts", "**/relay-connectivity-screenshots.spec.ts", + "**/observer-seed-screenshots.spec.ts", ], use: { ...devices["Desktop Chrome"], diff --git a/desktop/tests/e2e/observer-seed-screenshots.spec.ts b/desktop/tests/e2e/observer-seed-screenshots.spec.ts new file mode 100644 index 000000000..3f9a81c93 --- /dev/null +++ b/desktop/tests/e2e/observer-seed-screenshots.spec.ts @@ -0,0 +1,169 @@ +import { expect, test } from "@playwright/test"; + +import { KIND_TYPING_INDICATOR } from "../../src/shared/constants/kinds"; +import { installMockBridge } from "../helpers/bridge"; +import { + OBSERVER_SEED_AGENT_PUBKEY, + observerSeedFrames, +} from "../helpers/observerSeedFixture"; + +// Channel the seeded agent is a member of (via the managedAgents seed below). +// The agent must be a member of the navigated channel so it classifies as a +// channel-session agent — that's what renders the composer activity trigger. +const SEED_CHANNEL_NAME = "general"; + +// Poll until the mock relay has a live typing-indicator subscription for the +// channel. Without this, the typing event is emitted before the channel +// subscribes and is silently dropped, so the composer trigger never paints. +async function waitForMockLiveSubscription( + page: import("@playwright/test").Page, + channelName: string, + kind?: number, +) { + await expect + .poll(async () => + page.evaluate( + ({ currentChannelName, kind }) => + ( + window as Window & { + __BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?: (input: { + channelName: string; + kind?: number; + }) => boolean; + } + ).__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?.({ + channelName: currentChannelName, + kind, + }) ?? false, + { currentChannelName: channelName, kind }, + ), + ) + .toBe(true); +} + +const SHOTS = "test-results/observer-seed"; + +// Themes the populated panel is captured against. Values map to the real +// THEME_STORAGE_KEY entries read by ThemeProvider (light = catppuccin-latte, +// dark = houston). +const THEMES = [ + { label: "light", value: "catppuccin-latte" }, + { label: "dark", value: "houston" }, +] as const; + +function asWindow(page: import("@playwright/test").Page) { + return page; +} + +async function waitForBridge(page: import("@playwright/test").Page) { + await page.waitForFunction( + () => + typeof ( + window as Window & { + __BUZZ_E2E_SEED_OBSERVER_FRAMES__?: unknown; + } + ).__BUZZ_E2E_SEED_OBSERVER_FRAMES__ === "function", + null, + { timeout: 10_000 }, + ); +} + +// Drives the app from the channel list into the agent-session thread panel for +// the seeded agent, then injects the populated observer transcript. The agent +// is surfaced in the composer activity bar via a mock typing indicator, which +// is what renders the `bot-activity-composer-*` controls. +async function openSeededAgentSession( + page: import("@playwright/test").Page, + themeValue: string, +) { + // Set the theme before the app boots so the first paint is already themed. + await page.addInitScript((value) => { + window.localStorage.setItem("buzz-theme", value); + }, themeValue); + + await page.goto("/", { waitUntil: "domcontentloaded" }); + await waitForBridge(page); + + // Open #general and surface the agent as "typing" so the composer activity + // bar exposes the trigger + per-agent item. Wait for the typing-indicator + // subscription to go live first so the emitted event isn't dropped. + await page.getByTestId(`channel-${SEED_CHANNEL_NAME}`).click(); + await waitForMockLiveSubscription( + page, + SEED_CHANNEL_NAME, + KIND_TYPING_INDICATOR, + ); + await page.evaluate((pubkey) => { + ( + window as Window & { + __BUZZ_E2E_EMIT_MOCK_TYPING__?: (input: { + channelName: string; + pubkey: string; + }) => void; + } + ).__BUZZ_E2E_EMIT_MOCK_TYPING__?.({ + channelName: "general", + pubkey, + }); + }, OBSERVER_SEED_AGENT_PUBKEY); + + await expect(page.getByTestId("bot-activity-composer-trigger")).toBeVisible({ + timeout: 10_000, + }); + await page.getByTestId("bot-activity-composer-trigger").click(); + await page + .getByTestId(`bot-activity-composer-item-${OBSERVER_SEED_AGENT_PUBKEY}`) + .click({ force: true }); + + const panel = page.getByTestId("agent-session-thread-panel"); + await expect(panel).toBeVisible({ timeout: 10_000 }); + + // Inject the already-decrypted observer transcript through the production + // appendAgentEvent -> processTranscriptEvent pipeline. + await page.evaluate( + ({ agentPubkey, events }) => { + ( + window as Window & { + __BUZZ_E2E_SEED_OBSERVER_FRAMES__?: (input: { + agentPubkey: string; + events: unknown[]; + }) => void; + } + ).__BUZZ_E2E_SEED_OBSERVER_FRAMES__?.({ agentPubkey, events }); + }, + { agentPubkey: OBSERVER_SEED_AGENT_PUBKEY, events: observerSeedFrames }, + ); + + return panel; +} + +test.describe("observer-seed populated panel screenshots", () => { + test.use({ viewport: { width: 1280, height: 800 } }); + + for (const theme of THEMES) { + test(`populated transcript — ${theme.label}`, async ({ page }) => { + await installMockBridge(asWindow(page), { + managedAgents: [ + { + pubkey: OBSERVER_SEED_AGENT_PUBKEY, + name: "Fizz", + status: "running", + channelNames: ["general"], + }, + ], + }); + + const panel = await openSeededAgentSession(page, theme.value); + + // The seeded transcript renders a user prompt, an assistant message, and + // tool/shell summaries — assert one stable marker before capturing so the + // shot isn't taken mid-render. The compact tool row renders the friendly + // label ("Read file"), not the raw tool name. + await expect(panel).toContainText("Read file", { timeout: 10_000 }); + + await panel.screenshot({ + path: `${SHOTS}/populated-${theme.label}.png`, + }); + }); + } +}); diff --git a/desktop/tests/helpers/observerSeedFixture.ts b/desktop/tests/helpers/observerSeedFixture.ts index b77884de0..7476eba55 100644 --- a/desktop/tests/helpers/observerSeedFixture.ts +++ b/desktop/tests/helpers/observerSeedFixture.ts @@ -31,6 +31,17 @@ type ObserverEvent = { const SESSION_ID = "sess-pr3-001"; const TURN_ID = "turn-pr3-001"; +// Canonical seed identities, mirroring the conventions used by the other +// screenshot specs (active-turn-screenshots.spec.ts): +// - agent pubkey is a deterministic all-`aa` hex string +// - the channel is the well-known mock "general" channel id +export const OBSERVER_SEED_AGENT_PUBKEY = "aa".repeat(32); +export const OBSERVER_SEED_CHANNEL_ID = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50"; + +// Human author whose prompt triggers the seeded turn (drives the prompt avatar). +const OBSERVER_SEED_AUTHOR_PUBKEY = + "e5ebc6cdb579be112e336cc319b5989b4bb6af11786ea90dbe52b5f08d741b34"; + // A tiny 1x1 transparent PNG data URL — stands in for a view_image thumbnail // so the mediaInset rendering path lights up without shipping a binary asset. const SAMPLE_IMAGE_DATA_URL = @@ -54,16 +65,19 @@ export function buildPopulatedObserverEvents( kind: string, payload: unknown, offsetMs: number, - ): ObserverEvent => ({ - seq: (seq += 1), - timestamp: at(offsetMs), - kind, - agentIndex: 0, - channelId, - sessionId: SESSION_ID, - turnId: TURN_ID, - payload, - }); + ): ObserverEvent => { + seq += 1; + return { + seq, + timestamp: at(offsetMs), + kind, + agentIndex: 0, + channelId, + sessionId: SESSION_ID, + turnId: TURN_ID, + payload, + }; + }; const sessionUpdate = (update: Record) => ({ method: "session/update", @@ -72,11 +86,7 @@ export function buildPopulatedObserverEvents( return [ // ── lifecycle ────────────────────────────────────────────────────────── - ev( - "turn_started", - { triggeringEventIds: ["evt-aaaa", "evt-bbbb"] }, - 0, - ), + ev("turn_started", { triggeringEventIds: ["evt-aaaa", "evt-bbbb"] }, 0), ev("session_resolved", { isNewSession: true }, 200), // ── user prompt with [Buzz event] context (drives grouped prompt + avatar) @@ -155,7 +165,7 @@ export function buildPopulatedObserverEvents( toolCallId: "tool-read-1", status: "completed", content: { - text: "[server]\nport = 3000\nrelay = \"wss://sprout-oss.stage.blox.sqprod.co\"\n[features]\nobserver = true", + text: '[server]\nport = 3000\nrelay = "wss://sprout-oss.stage.blox.sqprod.co"\n[features]\nobserver = true', }, }), 1200, @@ -229,3 +239,10 @@ export function buildPopulatedObserverEvents( ), ]; } + +// Pre-built populated transcript for the canonical seed agent + channel. The +// screenshot spec feeds this straight into `__BUZZ_E2E_SEED_OBSERVER_FRAMES__`. +export const observerSeedFrames = buildPopulatedObserverEvents({ + channelId: OBSERVER_SEED_CHANNEL_ID, + authorPubkey: OBSERVER_SEED_AUTHOR_PUBKEY, +});