import { expect, test } from "@playwright/test"; import { waitForAnimations } from "../helpers/animations"; import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge"; const SHOTS = "test-results/config-bridge"; // Use well-known test pubkeys that map to distinct config surface fixtures const GOOSE_PUBKEY = TEST_IDENTITIES.tyler.pubkey; const PRESPAWN_PUBKEY = TEST_IDENTITIES.bob.pubkey; const RUNTIME_OVERRIDE_PUBKEY = TEST_IDENTITIES.outsider.pubkey; // Synthetic agent whose config surface mixes four distinct provenance origins // (matches PUBKEY_MULTI_ORIGIN in e2eBridge buildMockConfigSurface). const MULTI_ORIGIN_PUBKEY = "abc1230000000000000000000000000000000000000000000000000000000def"; const BUZZ_AGENT_PUBKEY = "b0220000000000000000000000000000000000000000000000000000000000a9"; const MANAGED_AGENTS = [ { pubkey: GOOSE_PUBKEY, name: "Goose Agent", status: "running" as const, channelNames: ["agents"], }, { pubkey: PRESPAWN_PUBKEY, name: "Pre-Spawn Agent", status: "stopped" as const, channelNames: ["agents"], }, { pubkey: RUNTIME_OVERRIDE_PUBKEY, name: "Runtime Override Agent", status: "running" as const, channelNames: ["agents"], }, { pubkey: MULTI_ORIGIN_PUBKEY, name: "Multi-Origin Agent", status: "running" as const, channelNames: ["agents"], }, ]; async function waitForInvokeBridge(page: import("@playwright/test").Page) { await page.waitForFunction( () => { const tauriWindow = window as Window & { __BUZZ_E2E_INVOKE_MOCK_COMMAND__?: unknown; __TAURI_INTERNALS__?: { invoke?: unknown }; }; return ( typeof tauriWindow.__BUZZ_E2E_INVOKE_MOCK_COMMAND__ === "function" || typeof tauriWindow.__TAURI_INTERNALS__?.invoke === "function" ); }, null, { timeout: 5_000 }, ); } async function invokeMockCommand( page: import("@playwright/test").Page, command: string, payload?: Record, ): Promise { await waitForInvokeBridge(page); return page.evaluate( async ({ command: cmd, payload: pl }) => { const tauriWindow = window as Window & { __BUZZ_E2E_INVOKE_MOCK_COMMAND__?: ( command: string, payload?: Record, ) => Promise; __TAURI_INTERNALS__?: { invoke?: ( command: string, payload?: Record, ) => Promise; }; }; const invoke = tauriWindow.__BUZZ_E2E_INVOKE_MOCK_COMMAND__ ?? tauriWindow.__TAURI_INTERNALS__?.invoke; if (!invoke) throw new Error("Mock invoke bridge is unavailable."); return invoke(cmd, pl); }, { command, payload }, ); } async function activatePersonas(page: import("@playwright/test").Page) { for (const id of ["builtin:fizz"]) { await invokeMockCommand(page, "set_persona_active", { id, active: true }); } } /** * Open the #agents channel and click an agent's avatar in the message list * to open the profile side panel, then navigate to the requested tab. */ async function openAgentProfileFromChannel( page: import("@playwright/test").Page, agentName: string, { anchorText = "Model", scrollToBottom = true, tab = "Info", }: { anchorText?: string; scrollToBottom?: boolean; tab?: "Info" | "Runtime"; } = {}, ) { await page.goto("/", { waitUntil: "domcontentloaded" }); await waitForInvokeBridge(page); await activatePersonas(page); // Navigate to the #agents channel await page.getByTestId("channel-agents").click(); await expect(page.getByTestId("chat-title")).toHaveText("agents"); // Find the message row from this agent and click the avatar to open profile const messageRow = page.getByTestId("message-row").filter({ has: page.getByText(agentName, { exact: false }), }); await expect(messageRow.first()).toBeVisible({ timeout: 5_000 }); await messageRow.first().getByRole("button").first().click(); const panel = page.getByTestId("user-profile-panel"); await expect(panel).toBeVisible({ timeout: 10_000 }); await panel.getByRole("tab", { name: tab }).click(); const configAnchor = panel.getByText(anchorText, { exact: true }).first(); await expect(configAnchor).toBeVisible({ timeout: 10_000 }); // Scroll the panel's internal scroll container to the bottom so the // config section content is fully visible. await configAnchor.scrollIntoViewIfNeeded(); if (scrollToBottom) { await panel.evaluate((el) => { // The scrollable container is the profileBody div with overflow-y-auto. // Find it by checking which child actually scrolls. const scrollable = el.querySelector("[data-radix-scroll-area-viewport]") ?? Array.from(el.querySelectorAll("*")).find( (child) => child.scrollHeight > child.clientHeight + 10, ) ?? el; scrollable.scrollTop = scrollable.scrollHeight; }); } await panel.page().waitForTimeout(200); return panel; } // Settle any in-flight animations before capture. async function settleAnimations(panel: import("@playwright/test").Locator) { await waitForAnimations(panel.page()); } test.describe("config bridge 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 — folded config panel", async ({ page }) => { await installMockBridge(page, { managedAgents: MANAGED_AGENTS }); const panel = await openAgentProfileFromChannel(page, "Goose Agent", { scrollToBottom: false, tab: "Runtime", }); // The shared config panel shows only the effective value. await expect(panel.getByText("gpt-4o", { exact: true })).toBeVisible(); await expect(panel.getByText("gpt-4o-mini", { exact: true })).toHaveCount( 0, ); await expect(panel.getByText("Set in Buzz")).toHaveCount(0); await settleAnimations(panel); await panel.screenshot({ path: `${SHOTS}/01-folded-config-panel.png` }); }); test("02 — live runtime override", async ({ page }) => { await installMockBridge(page, { managedAgents: MANAGED_AGENTS }); const panel = await openAgentProfileFromChannel( page, "Runtime Override Agent", { tab: "Runtime" }, ); // A runtime override shows only the effective live model. await expect( panel.getByText("claude-opus-4-20250514", { exact: true }), ).toBeVisible(); await expect(panel.getByText("gpt-4o", { exact: true })).toHaveCount(0); await expect( panel.getByText("Live override (this session only)"), ).toHaveCount(0); await settleAnimations(panel); await panel.screenshot({ path: `${SHOTS}/02-live-runtime-override.png`, }); }); test("03 — effective values without provenance", async ({ page }) => { await installMockBridge(page, { managedAgents: MANAGED_AGENTS }); const panel = await openAgentProfileFromChannel( page, "Multi-Origin Agent", { tab: "Runtime", }, ); // Values from different sources use the same simple two-line hierarchy. await expect(panel.getByText("gpt-4o", { exact: true })).toBeVisible(); await expect(panel.getByText("openai", { exact: true })).toBeVisible(); await expect(panel.getByText("auto", { exact: true })).toBeVisible(); await expect(panel.getByText(/From config file/)).toHaveCount(0); await expect(panel.getByText(/Inherited from/)).toHaveCount(0); await expect(panel.getByText(/From environment variable/)).toHaveCount(0); await settleAnimations(panel); await panel.screenshot({ path: `${SHOTS}/03-effective-values.png`, }); }); test("04 — pre-spawn state", async ({ page }) => { await installMockBridge(page, { managedAgents: MANAGED_AGENTS }); const panel = await openAgentProfileFromChannel(page, "Pre-Spawn Agent", { tab: "Runtime", }); // Unknown pre-start values stay empty rather than adding an explanatory row. await expect(panel.getByText("Available after agent starts")).toHaveCount( 0, ); await expect(panel.getByText("—", { exact: true })).toHaveCount(2); await settleAnimations(panel); await panel.screenshot({ path: `${SHOTS}/04-pre-spawn-state.png` }); }); test("05 — advanced flat list", async ({ page }) => { await installMockBridge(page, { managedAgents: MANAGED_AGENTS }); const panel = await openAgentProfileFromChannel(page, "Goose Agent", { anchorText: "MCP servers", tab: "Runtime", }); // Advanced runtime fields render directly in the profile panel's flat list, // grouped under their own "Advanced" header. await expect(panel.getByText("active_provider")).toBeVisible(); await expect(panel.getByText("Advanced", { exact: true })).toHaveCount(1); // MCP servers render once each under their group label. await expect(panel.getByText("developer", { exact: true })).toHaveCount(1); await expect(panel.getByText("MCP servers", { exact: true })).toHaveCount( 1, ); await settleAnimations(panel); await panel.screenshot({ path: `${SHOTS}/05-advanced-expanded.png` }); }); test("06 — buzz-agent empty MCP servers", async ({ page }) => { await installMockBridge(page, { managedAgents: [ { pubkey: BUZZ_AGENT_PUBKEY, name: "Buzz Agent", status: "running" as const, channelNames: ["agents"], }, ], }); const panel = await openAgentProfileFromChannel(page, "Buzz Agent", { anchorText: "MCP servers", tab: "Runtime", }); await expect( panel.getByText("No custom servers configured.", { exact: true }), ).toBeVisible(); await expect(panel.getByText("MCP servers", { exact: true })).toHaveCount( 1, ); }); test("07 — profile side panel — Configuration section", async ({ page }) => { // charlie (554cef…) is the well-known test pubkey that the mock bridge // seeds as a bot owned by the test viewer, so isBot + isOwner + managedAgent // are all true — the Configuration section renders in the profile panel. await installMockBridge(page, { managedAgents: [ { pubkey: "554cef57437abac34522ac2c9f0490d685b72c80478cf9f7ed6f9570ee8624ea", name: "Charlie", status: "running" as const, channelNames: ["agents"], }, ], }); await page.goto("/", { waitUntil: "domcontentloaded" }); await waitForInvokeBridge(page); await page.getByTestId("channel-agents").click(); await expect(page.getByTestId("chat-title")).toHaveText("agents"); // Click an agent avatar in a full message row to open the profile side panel. await page .getByTestId("message-row") .filter({ has: page.locator('[data-testid^="message-avatar-"]') }) .last() .getByRole("button") .first() .click(); const panel = page.getByTestId("user-profile-panel"); await expect(panel).toBeVisible({ timeout: 10_000 }); // Editable configuration lives inside the Runtime tab. await panel.getByRole("tab", { name: "Runtime" }).click(); // Wait for the config section to render and scroll it into view so // it is fully visible before capture. const configAnchor = panel.getByText("Model").first(); await expect(configAnchor).toBeVisible({ timeout: 10_000 }); await configAnchor.scrollIntoViewIfNeeded(); // Scroll the panel's internal scroll container to the bottom so config // fields are fully visible, not just the heading at the edge. await panel.evaluate((el) => { const scrollable = el.querySelector("[data-radix-scroll-area-viewport]") ?? Array.from(el.querySelectorAll("*")).find( (child) => child.scrollHeight > child.clientHeight + 10, ) ?? el; scrollable.scrollTop = scrollable.scrollHeight; }); await panel.page().waitForTimeout(200); await settleAnimations(panel); await panel.screenshot({ path: `${SHOTS}/06-profile-side-panel-config.png`, }); }); });