mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
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.
This commit is contained in:
@@ -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"],
|
||||
|
||||
@@ -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`,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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<string, unknown>) => ({
|
||||
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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user