import { expect, test } from "@playwright/test"; import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge"; const SHOTS = "test-results/observer-feed"; // A running managed agent whose pubkey maps to the "agents" channel. // Using tyler's pubkey so the mock bridge already knows this identity. const OBSERVER_AGENT_PUBKEY = TEST_IDENTITIES.tyler.pubkey; const CHANNEL_ID = "94a444a4-c0a3-5966-ab05-530c6ddc2301"; // #agents const NOW = new Date("2025-06-15T12:00:00Z").toISOString(); const MANAGED_AGENTS = [ { pubkey: OBSERVER_AGENT_PUBKEY, name: "Observer Agent", status: "running" as const, channelNames: ["agents"], }, ]; // Helper: wait until the seed hook is available in the page. async function waitForSeedHook(page: import("@playwright/test").Page) { await page.waitForFunction( () => typeof window.__BUZZ_E2E_SEED_OBSERVER_EVENTS__ === "function", null, { timeout: 10_000 }, ); } // Helper: open the observer feed panel by navigating to #agents, clicking the // agent avatar to open the profile panel, then clicking "View activity". async function openObserverFeedPanel( page: import("@playwright/test").Page, agentPubkey: string, ) { await page.goto("/", { waitUntil: "domcontentloaded" }); await waitForSeedHook(page); await page.getByTestId("channel-agents").click(); await expect(page.getByTestId("chat-title")).toHaveText("agents"); // Click any message row button to open the profile panel. const messageRow = page .getByTestId("message-row") .filter({ has: page.getByText("Observer Agent", { exact: false }) }); await expect(messageRow.first()).toBeVisible({ timeout: 8_000 }); await messageRow.first().getByRole("button").first().click(); const profilePanel = page.getByTestId("user-profile-panel"); await expect(profilePanel).toBeVisible({ timeout: 10_000 }); // Click "View activity" to open the observer feed panel. const activityBtn = page.getByTestId( `user-profile-view-activity-${agentPubkey}`, ); await expect(activityBtn).toBeVisible({ timeout: 5_000 }); await activityBtn.click(); const feedPanel = page.getByTestId("agent-session-thread-panel"); await expect(feedPanel).toBeVisible({ timeout: 10_000 }); return feedPanel; } // Helper: seed observer events into the store and wait for the panel to update. async function seedObserverEvents( page: import("@playwright/test").Page, agentPubkey: string, events: Array<{ seq: number; timestamp: string; kind: string; agentIndex: number | null; channelId: string | null; sessionId: string | null; turnId: string | null; payload: unknown; }>, ) { await page.evaluate( ({ pubkey, evts }) => { window.__BUZZ_E2E_SEED_OBSERVER_EVENTS__?.({ agentPubkey: pubkey, events: evts, }); }, { pubkey: agentPubkey, evts: events }, ); // Let React re-render after the store update. await page.waitForTimeout(300); } async function settleAnimations(panel: import("@playwright/test").Locator) { // Only await finite animations — live surfaces (e.g. the turn liveness // indicator) run infinite loops whose `finished` promise never resolves. await panel.evaluate((el) => Promise.all( el .getAnimations({ subtree: true }) .filter((a) => { const timing = a.effect?.getTiming(); return timing?.iterations !== Number.POSITIVE_INFINITY; }) .map((a) => a.finished), ), ); } test.describe("observer feed screenshots", () => { test.use({ viewport: { width: 1280, height: 900 } }); test.beforeEach(async ({ page }) => { page.on("pageerror", (err) => { console.error( "PAGE ERROR:", err.message, err.stack?.split("\n").slice(0, 5).join("\n"), ); }); page.on("console", (msg) => { if (msg.type() === "error") { console.error("CONSOLE ERROR:", msg.text().slice(0, 500)); } }); }); test("01 — prompt context dialog (via Checks ingress)", async ({ page }) => { await installMockBridge(page, { managedAgents: MANAGED_AGENTS }); const feedPanel = await openObserverFeedPanel(page, OBSERVER_AGENT_PUBKEY); // session/prompt event: per-turn prompt context. The payload contains // sections that parsePromptText extracts; the transcript keeps them out // of the feed until the CheckCheck footer toggle opens the // PromptContextDialog modal. await seedObserverEvents(page, OBSERVER_AGENT_PUBKEY, [ { seq: 1, timestamp: NOW, kind: "acp_write", agentIndex: 0, channelId: CHANNEL_ID, sessionId: "session-001", turnId: "turn-001", payload: { jsonrpc: "2.0", id: 1, method: "session/prompt", params: { prompt: [ { type: "text", text: "[Buzz event: Kind 9]\nContent: @Observer Agent help me debug this", }, { type: "text", text: "[Thread context]\nThis is the thread history with 3 prior messages.", }, { type: "text", text: "[Channel context]\nYou are in #agents, a channel for AI coordination.", }, ], }, }, }, ]); // The context stays out of the feed until the CheckCheck ingress opens // the dialog. await expect(feedPanel.getByText("Prompt context")).toHaveCount(0); const contextToggle = feedPanel.getByTestId( "transcript-prompt-context-toggle", ); await expect(contextToggle).toBeVisible({ timeout: 5_000 }); await contextToggle.click(); const dialog = page.getByRole("dialog"); await expect(dialog).toBeVisible({ timeout: 5_000 }); await expect( dialog.getByTestId("transcript-prompt-context-sections"), ).toBeVisible({ timeout: 5_000 }); await settleAnimations(dialog); await dialog.screenshot({ path: `${SHOTS}/01-prompt-context-dialog.png`, }); }); test("02 — system prompt title restored", async ({ page }) => { await installMockBridge(page, { managedAgents: MANAGED_AGENTS }); const feedPanel = await openObserverFeedPanel(page, OBSERVER_AGENT_PUBKEY); // session/new event without a subsequent session/prompt: the system-prompt // item is never consumed by a turn bucket, so the grouper emits it as a // standalone single rendered by RawRailActivity with the "System prompt" // title. (This is the isolated test; see shot 11 for the full turn bundle.) await seedObserverEvents(page, OBSERVER_AGENT_PUBKEY, [ { seq: 1, timestamp: NOW, kind: "acp_write", agentIndex: 0, channelId: CHANNEL_ID, sessionId: "session-001", turnId: null, payload: { jsonrpc: "2.0", id: 1, method: "session/new", params: { systemPrompt: "[Base]\nYou are a helpful AI assistant running in Buzz.\n\n[System]\nYou are Observer Agent. You coordinate multi-agent workflows in the #agents channel.", }, }, }, ]); // The system prompt rail item should show the restored title. await expect(feedPanel.getByText("System prompt")).toBeVisible({ timeout: 5_000, }); await settleAnimations(feedPanel); await feedPanel.screenshot({ path: `${SHOTS}/02-system-prompt-title-restored.png`, }); }); test("03 — permission outcome (approved)", async ({ page }) => { await installMockBridge(page, { managedAgents: MANAGED_AGENTS }); const feedPanel = await openObserverFeedPanel(page, OBSERVER_AGENT_PUBKEY); // Permission request followed by a selected (approved) response, // correlated by JSON-RPC id (fix #3: was broken for numeric ids before). await seedObserverEvents(page, OBSERVER_AGENT_PUBKEY, [ // Request { seq: 1, timestamp: NOW, kind: "acp_read", agentIndex: 0, channelId: CHANNEL_ID, sessionId: "session-001", turnId: "turn-001", payload: { jsonrpc: "2.0", id: 42, method: "session/request_permission", params: { title: "Read file system", message: "Agent wants to read ~/.config/goose/config.yaml", options: [ { optionId: "opt-allow", kind: "allow_once", name: "Allow once" }, { optionId: "opt-deny", kind: "reject_once", name: "Deny" }, ], }, }, }, // Response (numeric id 42 — the exact case fix #3 restores) { seq: 2, timestamp: NOW, kind: "acp_write", agentIndex: 0, channelId: CHANNEL_ID, sessionId: "session-001", turnId: "turn-001", payload: { jsonrpc: "2.0", id: 42, result: { outcome: { outcome: "selected", optionId: "opt-allow", }, }, }, }, ]); // The permission row should show the "Approved (allow_once)" outcome. await expect(feedPanel.getByText(/Approved.*allow_once/)).toBeVisible({ timeout: 5_000, }); await settleAnimations(feedPanel); await feedPanel.screenshot({ path: `${SHOTS}/03-permission-approved.png`, }); }); test("04 — permission outcome (cancelled)", async ({ page }) => { await installMockBridge(page, { managedAgents: MANAGED_AGENTS }); const feedPanel = await openObserverFeedPanel(page, OBSERVER_AGENT_PUBKEY); await seedObserverEvents(page, OBSERVER_AGENT_PUBKEY, [ // Request { seq: 1, timestamp: NOW, kind: "acp_read", agentIndex: 0, channelId: CHANNEL_ID, sessionId: "session-001", turnId: "turn-001", payload: { jsonrpc: "2.0", id: "perm-cancelled-1", method: "session/request_permission", params: { title: "Write to output file", options: [ { optionId: "opt-yes", kind: "allow_once", name: "Allow" }, ], }, }, }, // Cancelled response { seq: 2, timestamp: NOW, kind: "acp_write", agentIndex: 0, channelId: CHANNEL_ID, sessionId: "session-001", turnId: "turn-001", payload: { jsonrpc: "2.0", id: "perm-cancelled-1", result: { outcome: { outcome: "cancelled", }, }, }, }, ]); await expect(feedPanel.getByText("Cancelled")).toBeVisible({ timeout: 5_000, }); await settleAnimations(feedPanel); await feedPanel.screenshot({ path: `${SHOTS}/04-permission-cancelled.png`, }); }); test("05 — prompt context dialog (sections expanded)", async ({ page }) => { await installMockBridge(page, { managedAgents: MANAGED_AGENTS }); const feedPanel = await openObserverFeedPanel(page, OBSERVER_AGENT_PUBKEY); await seedObserverEvents(page, OBSERVER_AGENT_PUBKEY, [ { seq: 1, timestamp: NOW, kind: "acp_write", agentIndex: 0, channelId: CHANNEL_ID, sessionId: "session-001", turnId: "turn-001", payload: { jsonrpc: "2.0", id: 1, method: "session/prompt", params: { prompt: [ { type: "text", text: "[Buzz event: Kind 9]\nContent: @Observer Agent help me debug this", }, { type: "text", text: "[Thread context]\nThis is the thread history with 3 prior messages.", }, { type: "text", text: "[Channel context]\nYou are in #agents, a channel for AI coordination.", }, ], }, }, }, ]); // Open the dialog via the CheckCheck ingress, then expand every section. const contextToggle = feedPanel.getByTestId( "transcript-prompt-context-toggle", ); await expect(contextToggle).toBeVisible({ timeout: 5_000 }); await contextToggle.click(); const dialog = page.getByRole("dialog"); await expect(dialog).toBeVisible({ timeout: 5_000 }); const sectionButtons = dialog .getByTestId("transcript-prompt-context-sections") .getByRole("button"); for (const btn of await sectionButtons.all()) { await btn.click(); } await settleAnimations(dialog); await dialog.screenshot({ path: `${SHOTS}/05-prompt-context-expanded.png`, }); }); test("06 — system prompt sections expanded", async ({ page }) => { await installMockBridge(page, { managedAgents: MANAGED_AGENTS }); const feedPanel = await openObserverFeedPanel(page, OBSERVER_AGENT_PUBKEY); await seedObserverEvents(page, OBSERVER_AGENT_PUBKEY, [ { seq: 1, timestamp: NOW, kind: "acp_write", agentIndex: 0, channelId: CHANNEL_ID, sessionId: "session-001", turnId: null, payload: { jsonrpc: "2.0", id: 1, method: "session/new", params: { systemPrompt: "[Base]\nYou are a helpful AI assistant running in Buzz.\n\n[System]\nYou are Observer Agent. You coordinate multi-agent workflows in the #agents channel.\n\n[Team Instructions]\nAlways tag on handoff.\n\n[Agent Memory — core]\nI am Observer Agent.\n## Lessons Learned\nAlways tag on handoff.\n\n[Channel Canvas]\nCanvas revision (event ID): a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2\nLast modified: 2026-07-11T10:00:00Z\nFetch current content with: buzz canvas get --channel 94a444a4-c0a3-5966-ab05-530c6ddc2301", }, }, }, ]); await expect(feedPanel.getByText("System prompt")).toBeVisible({ timeout: 5_000, }); // All five section headings must be present in the card — expand it first. await feedPanel.getByTestId("transcript-metadata-item").evaluate((el) => { if (el.tagName === "DETAILS") (el as HTMLDetailsElement).open = true; for (const details of el.querySelectorAll("details")) { details.open = true; } }); await expect(feedPanel.getByText("Base")).toBeVisible({ timeout: 5_000 }); await expect(feedPanel.getByText("System", { exact: true })).toBeVisible({ timeout: 5_000, }); await expect(feedPanel.getByText("Team Instructions")).toBeVisible({ timeout: 5_000, }); await expect(feedPanel.getByText("Core Memory")).toBeVisible({ timeout: 5_000, }); await expect(feedPanel.getByText("Channel Canvas")).toBeVisible({ timeout: 5_000, }); // Open the outer ActivityRow
to reveal the section content, // then click each section accordion button to expand it (mirrors shot 05 — // system-prompt sections now render as React button accordions, not native //
elements). await feedPanel.getByTestId("transcript-metadata-item").evaluate((el) => { if (el.tagName === "DETAILS") (el as HTMLDetailsElement).open = true; for (const details of el.querySelectorAll("details")) { details.open = true; } }); const sectionButtons = feedPanel .getByTestId("transcript-metadata-item") .getByTestId("transcript-prompt-context-sections") .getByRole("button"); const allSectionButtons = await sectionButtons.all(); expect(allSectionButtons.length).toBeGreaterThan(0); for (const btn of allSectionButtons) { await btn.click(); } await settleAnimations(feedPanel); await feedPanel.screenshot({ path: `${SHOTS}/06-system-prompt-expanded.png`, }); }); test("07 — current_mode_update lifecycle status line", async ({ page }) => { await installMockBridge(page, { managedAgents: MANAGED_AGENTS }); const feedPanel = await openObserverFeedPanel(page, OBSERVER_AGENT_PUBKEY); await seedObserverEvents(page, OBSERVER_AGENT_PUBKEY, [ { seq: 1, timestamp: NOW, kind: "acp_read", agentIndex: 0, channelId: CHANNEL_ID, sessionId: "session-007", turnId: "turn-007", payload: { method: "session/update", params: { sessionId: "session-007", update: { sessionUpdate: "current_mode_update", currentModeId: "plan", }, }, }, }, ]); await expect(feedPanel.getByText("Mode")).toBeVisible({ timeout: 5_000 }); await settleAnimations(feedPanel); await feedPanel.screenshot({ path: `${SHOTS}/07-current-mode-update.png`, }); }); test("08 — usage_update lifecycle status line (coalesced)", async ({ page, }) => { await installMockBridge(page, { managedAgents: MANAGED_AGENTS }); const feedPanel = await openObserverFeedPanel(page, OBSERVER_AGENT_PUBKEY); await seedObserverEvents(page, OBSERVER_AGENT_PUBKEY, [ // Two usage frames — only the last should be visible. { seq: 1, timestamp: NOW, kind: "acp_read", agentIndex: 0, channelId: CHANNEL_ID, sessionId: "session-008", turnId: "turn-008", payload: { method: "session/update", params: { sessionId: "session-008", update: { sessionUpdate: "usage_update", used: 1200, size: 8192, }, }, }, }, { seq: 2, timestamp: NOW, kind: "acp_read", agentIndex: 0, channelId: CHANNEL_ID, sessionId: "session-008", turnId: "turn-008", payload: { method: "session/update", params: { sessionId: "session-008", update: { sessionUpdate: "usage_update", used: 3450, size: 8192, cost: { amount: 0.0018, currency: "USD" }, }, }, }, }, ]); await expect(feedPanel.getByText("Usage")).toBeVisible({ timeout: 5_000 }); await settleAnimations(feedPanel); await feedPanel.screenshot({ path: `${SHOTS}/08-usage-update-coalesced.png`, }); }); test("09 — available_commands_update lifecycle status line", async ({ page, }) => { await installMockBridge(page, { managedAgents: MANAGED_AGENTS }); const feedPanel = await openObserverFeedPanel(page, OBSERVER_AGENT_PUBKEY); await seedObserverEvents(page, OBSERVER_AGENT_PUBKEY, [ { seq: 1, timestamp: NOW, kind: "acp_read", agentIndex: 0, channelId: CHANNEL_ID, sessionId: "session-009", turnId: "turn-009", payload: { method: "session/update", params: { sessionId: "session-009", update: { sessionUpdate: "available_commands_update", availableCommands: [ { name: "create_plan", description: "Create a structured plan for the task", }, { name: "research_codebase", description: "Research and understand the codebase", }, { name: "execute_steps", description: "Execute the planned steps", }, ], }, }, }, }, ]); await expect(feedPanel.getByText("Commands", { exact: true })).toBeVisible({ timeout: 5_000, }); await settleAnimations(feedPanel); await feedPanel.screenshot({ path: `${SHOTS}/09-available-commands-update.png`, }); }); test("10 — config_option_update lifecycle status line", async ({ page }) => { await installMockBridge(page, { managedAgents: MANAGED_AGENTS }); const feedPanel = await openObserverFeedPanel(page, OBSERVER_AGENT_PUBKEY); await seedObserverEvents(page, OBSERVER_AGENT_PUBKEY, [ { seq: 1, timestamp: NOW, kind: "acp_read", agentIndex: 0, channelId: CHANNEL_ID, sessionId: "session-010", turnId: "turn-010", payload: { method: "session/update", params: { sessionId: "session-010", update: { sessionUpdate: "config_option_update", configOptions: [ { id: "model", name: "Model", type: "select", currentValue: "gpt-4o", }, { id: "mode", name: "Mode", type: "select", currentValue: "auto", }, ], }, }, }, }, ]); await expect(feedPanel.getByText("Config")).toBeVisible({ timeout: 5_000 }); await settleAnimations(feedPanel); await feedPanel.screenshot({ path: `${SHOTS}/10-config-option-update.png`, }); }); test("11 — first-turn bundle: standalone system-prompt card + Checks ingress (per-turn context only)", async ({ page, }) => { await installMockBridge(page, { managedAgents: MANAGED_AGENTS }); const feedPanel = await openObserverFeedPanel(page, OBSERVER_AGENT_PUBKEY); // Full realistic pool.rs first-turn wire sequence for a team-pack agent: // turn_started → session/new → session_resolved → session/prompt // Verifies the consolidated presentation: session/new.systemPrompt always // renders as a standalone top-level "System prompt" card (never injected into // the CheckCheck bundle). The CheckCheck dialog contains only per-turn context // (Buzz event / Thread context) — no Base/System/Team Instructions/Core Memory/Channel Canvas sections. await seedObserverEvents(page, OBSERVER_AGENT_PUBKEY, [ { seq: 1, timestamp: NOW, kind: "turn_started", agentIndex: 0, channelId: CHANNEL_ID, sessionId: null, turnId: "turn-001", payload: { source: "channel", triggeringEventIds: [] }, }, { seq: 2, timestamp: NOW, kind: "acp_write", agentIndex: 0, channelId: CHANNEL_ID, sessionId: null, turnId: "turn-001", payload: { jsonrpc: "2.0", id: 1, method: "session/new", params: { systemPrompt: "[Base]\nYou are a helpful AI assistant running in Buzz.\n\n[System]\nYou are Observer Agent. You coordinate multi-agent workflows in the #agents channel.\n\n[Team Instructions]\nAlways tag on handoff.\n\n[Agent Memory — core]\nI am Observer Agent.\n## Lessons Learned\nAlways tag on handoff.\n\n[Channel Canvas]\nCanvas revision (event ID): a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2\nLast modified: 2026-07-11T10:00:00Z\nFetch current content with: buzz canvas get --channel 94a444a4-c0a3-5966-ab05-530c6ddc2301", }, }, }, { seq: 3, timestamp: NOW, kind: "session_resolved", agentIndex: 0, channelId: CHANNEL_ID, sessionId: "session-001", turnId: "turn-001", payload: { sessionId: "session-001", isNewSession: true }, }, { seq: 4, timestamp: NOW, kind: "acp_write", agentIndex: 0, channelId: CHANNEL_ID, sessionId: "session-001", turnId: "turn-001", payload: { jsonrpc: "2.0", id: 2, method: "session/prompt", params: { prompt: [ { type: "text", text: "[Buzz event: Kind 9]\nContent: @Observer Agent help me debug this", }, { type: "text", text: "[Thread context]\nThis is the thread history with 3 prior messages.", }, ], }, }, }, ]); // User message bubble should be visible (anchors the prompt bundle). await expect(feedPanel.getByTestId("transcript-prompt-bundle")).toBeVisible( { timeout: 5_000 }, ); // Scroll to the top of the feed to bring the standalone "System prompt" // card (which precedes the user bubble) into view. await feedPanel.evaluate((el) => el.scrollTo({ top: 0 })); // Standalone "System prompt" card is visible as a top-level feed row — // it must remain present even after the first prompt arrives. await expect(feedPanel.getByText("System prompt")).toBeVisible({ timeout: 5_000, }); // The standalone card shows "5 sections" collapsed — expand it to reveal // the section headings, then assert all five are present. await feedPanel.getByTestId("transcript-metadata-item").evaluate((el) => { if (el.tagName === "DETAILS") (el as HTMLDetailsElement).open = true; for (const details of el.querySelectorAll("details")) { details.open = true; } }); await expect(feedPanel.getByText("Base")).toBeVisible({ timeout: 5_000 }); await expect(feedPanel.getByText("System", { exact: true })).toBeVisible({ timeout: 5_000, }); await expect(feedPanel.getByText("Team Instructions")).toBeVisible({ timeout: 5_000, }); await expect(feedPanel.getByText("Core Memory")).toBeVisible({ timeout: 5_000, }); await expect(feedPanel.getByText("Channel Canvas")).toBeVisible({ timeout: 5_000, }); // Per-turn prompt context (Buzz event / Thread context) does NOT appear // as a standalone feed row — it lives behind the CheckCheck toggle. await expect(feedPanel.getByText("Prompt context")).toHaveCount(0); // Open the CheckCheck dialog: it contains ONLY per-turn context sections // (Buzz event, Thread context). Base/System/Team Instructions/Core Memory/Channel Canvas must NOT appear. await feedPanel.getByTestId("transcript-prompt-context-toggle").click(); const dialog = page.getByRole("dialog"); await expect(dialog).toBeVisible({ timeout: 5_000 }); const sectionArticles = dialog .getByTestId("transcript-prompt-context-sections") .locator("article"); const sectionTitles = await sectionArticles.allInnerTexts(); // Only per-turn context sections (Buzz event + Thread context) — no system-prompt sections. expect(sectionTitles.length).toBe(2); expect(sectionTitles[0]).toContain("Buzz event"); expect(sectionTitles[1]).toContain("Thread context"); // Collect all article heading text and assert none of the five // system-prompt section labels appear — including exact "System" which // would be ambiguous via substring search on the full dialog text. const forbidden = [ "Base", "System", "Team Instructions", "Core Memory", "Channel Canvas", ]; for (const title of sectionTitles) { for (const label of forbidden) { expect(title).not.toContain(label); } } await settleAnimations(dialog); await dialog.screenshot({ path: `${SHOTS}/11-first-turn-ordering.png`, }); }); });